Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 62 additions & 2 deletions src/logging/sentry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
*
* SPDX-License-Identifier: MIT
*/
import { captureException } from "@sentry/browser";
import { captureException, ErrorEvent, init } from "@sentry/browser";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { reportError } from "./sentry";
import { initSentry, reportError } from "./sentry";

vi.mock("@sentry/browser", () => ({
addBreadcrumb: vi.fn(),
Expand Down Expand Up @@ -94,3 +94,63 @@ describe("reportError", () => {
expect(hint).toBeUndefined();
});
});

describe("language server request failure grouping", () => {
const fingerprint = (message: string) => {
initSentry({ VITE_SENTRY_DSN: dsn });
const { beforeSend } = vi.mocked(init).mock.lastCall![0]!;
const event: ErrorEvent = {
type: undefined,
exception: { values: [{ type: "Error", value: message }] },
};
return (beforeSend!(event, {}) as ErrorEvent).fingerprint;
};

it("groups by request and message", () => {
expect(
fingerprint(
"Request textDocument/completion failed with message: Maximum call stack size exceeded"
)
).toEqual([
"language-server-request-failed",
"textDocument/completion",
"Maximum call stack size exceeded",
]);
});

it("ignores which script failed to load", () => {
const failure = (url: string) =>
fingerprint(
`Request initialize failed with message: Failed to execute 'importScripts' on 'WorkerGlobalScope': The script at '${url}' failed to load.`
);
const fr = failure(
"https://python.microbit.org/v/3/workers/pyright-locale-fr-231-e938f9c58b9ac7c8a0c8.worker.js"
);
expect(fr).toEqual([
"language-server-request-failed",
"initialize",
"Failed to execute 'importScripts' on 'WorkerGlobalScope': The script at '<url>' failed to load.",
]);
expect(
failure(
"https://python.microbit.org/v/3/workers/pyright-locale-en-100-ddc72e6b2c1f5de1a885.worker.js"
)
).toEqual(fr);
});

it("separates scripts resolved against a blob URL", () => {
expect(
fingerprint(
"Request initialize failed with message: Failed to execute 'importScripts' on 'WorkerGlobalScope': The script at 'blob:https://python.microbit.org/pyright-locale-en-100-ddc72e6b2c1f5de1a885.worker.js' failed to load."
)
).toEqual([
"language-server-request-failed",
"initialize",
"Failed to execute 'importScripts' on 'WorkerGlobalScope': The script at '<blob url>' failed to load.",
]);
});

it("leaves other errors to default grouping", () => {
expect(fingerprint("boom")).toBeUndefined();
});
});
29 changes: 29 additions & 0 deletions src/logging/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import {
addBreadcrumb as sentryAddBreadcrumb,
captureException as sentryCaptureException,
ErrorEvent,
init as sentryInit,
} from "@sentry/browser";

Expand All @@ -31,13 +32,41 @@ export const initSentry = (env: Record<string, string>): string | undefined => {
// Low consequence and a big chunk of quota.
"ResizeObserver loop completed with undelivered notifications",
],
beforeSend: fingerprintLanguageServerRequestFailure,
});
} catch (e) {
console.error(e);
}
return dsn;
};

/**
* Every failed language server request is rejected from the same place in
* vscode-jsonrpc, so Sentry's stack-based grouping puts unrelated failures
* in one issue. Group by the request and the server's message instead.
*/
const fingerprintLanguageServerRequestFailure = (
event: ErrorEvent
): ErrorEvent => {
const message = event.exception?.values?.[0]?.value;
const match =
message && /^Request (\S+) failed with message: (.*)$/s.exec(message);
if (match) {
const [, method, serverMessage] = match;
event.fingerprint = [
"language-server-request-failed",
method,
// Script URLs vary by locale and build. Blob URLs stay distinct: the
// script's path was resolved against a blob rather than our URL, so
// unlike a network failure it can't succeed on retry.
serverMessage.replace(/'(blob:)?https?:[^']*'/g, (_, blob) =>
blob ? "'<blob url>'" : "'<url>'"
),
];
}
return event;
};

/**
* Report an error to Sentry (if configured) and the console.
*
Expand Down
Loading