fix(ui): stop reporting cancelled queries to Sentry (#20430)

React Query's CancelledError keeps the default Error name, so the
name-based abort check never matched it. A query cancelled with
revert: false (for example by the persisted cache when an issue read
is denied or the account changes) reached the query cache onError as
an "unknown" error and was sent to Sentry. Recognize it by class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Devin FoleyandClaude Opus 5.5 authored and GitHub committed 2026-11-09 17:43:34 -08:00
1 parent 18697b8d21
commit 5570debc5b
3 files changed
+18 -2

No files matched your search

+2
View File
@@ -1,3 +1,4 @@
import { CancelledError } from "@tanstack/react-query";
import { describe, expect, it } from "vitest";
import { ApiError } from "./api-error";
import {
@@ -37,6 +38,7 @@ describe("classifyError", () => {
["422", apiError(422), "invalid"],
["500", apiError(500, { error: "boom" }), "unknown"],
["an abort", new DOMException("Aborted", "AbortError"), "aborted"],
["a React Query cancellation", new CancelledError({ revert: false }), "aborted"],
["a programming TypeError", new TypeError("Cannot read properties of undefined"), "unknown"],
["a plain Error", new Error("nope"), "unknown"],
["undefined", undefined, "unknown"],
+5 -2
View File
@@ -12,6 +12,8 @@
* by hand-rolled fetches (health, uploads) classify the same way.
*/
import { CancelledError } from "@tanstack/react-query";
export type ErrorKind =
| "transient"
| "auth"
@@ -181,8 +183,9 @@ function readableServerMessage(err: unknown): string | null {
// --- Classification ---------------------------------------------------------------
function isAbortError(err: unknown): boolean {
const name = record(err)?.name;
return name === "AbortError" || name === "CancelledError";
// React Query's `CancelledError` keeps the default `name` ("Error"), so it
// can only be recognized by class.
return record(err)?.name === "AbortError" || err instanceof CancelledError;
}
function isNetworkTypeError(err: unknown): boolean {
+11
View File
@@ -278,6 +278,17 @@ describe("createAppQueryClient", () => {
unsubscribe();
});
it("does not report a query that was cancelled without reverting", async () => {
const queryFn = vi.fn(() => new Promise<string>(() => {}));
const observer = new QueryObserver(client, { queryKey: ["issues", "detail", "PAP-1"], queryFn });
const unsubscribe = observer.subscribe(() => {});
await vi.advanceTimersByTimeAsync(0);
await client.cancelQueries({ queryKey: ["issues", "detail"] }, { revert: false });
expect(observer.getCurrentResult().error?.message).toBe("CancelledError");
expect(reportError).not.toHaveBeenCalled();
unsubscribe();
});
it("leaves a cache-only alias stale instead of refetching it without a queryFn", async () => {
client.setQueryData(["issues", "detail", "issue-uuid"], { id: "issue-uuid", title: "Seeded" });
const queryFn = vi.fn(async () => "fetched");