From 3942fff462779dd0039080cf0fd5528b4878874c Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Thu, 12 Nov 2026 09:39:31 +0000 Subject: [PATCH 1/2] Group language server request failures by request and message in Sentry Every failed request is rejected from the same frame in vscode-jsonrpc, so PYTHON-EDITOR-NEXT-3SJ mixed start-up network failures with stack overflows inside Pyright. Fingerprint on the request method and the server's message, with script URLs normalised as they vary by locale and build. --- src/logging/sentry.test.ts | 52 ++++++++++++++++++++++++++++++++++++-- src/logging/sentry.ts | 25 ++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/logging/sentry.test.ts b/src/logging/sentry.test.ts index fbbd4bfe2..c4ac6cc44 100644 --- a/src/logging/sentry.test.ts +++ b/src/logging/sentry.test.ts @@ -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(), @@ -94,3 +94,51 @@ 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 '' failed to load.", + ]); + expect( + failure( + "blob:https://python.microbit.org/pyright-locale-en-100-ddc72e6b2c1f5de1a885.worker.js" + ) + ).toEqual(fr); + }); + + it("leaves other errors to default grouping", () => { + expect(fingerprint("boom")).toBeUndefined(); + }); +}); diff --git a/src/logging/sentry.ts b/src/logging/sentry.ts index af5ecfc2d..19285b84d 100644 --- a/src/logging/sentry.ts +++ b/src/logging/sentry.ts @@ -6,6 +6,7 @@ import { addBreadcrumb as sentryAddBreadcrumb, captureException as sentryCaptureException, + ErrorEvent, init as sentryInit, } from "@sentry/browser"; @@ -31,6 +32,7 @@ export const initSentry = (env: Record): string | undefined => { // Low consequence and a big chunk of quota. "ResizeObserver loop completed with undelivered notifications", ], + beforeSend: fingerprintLanguageServerRequestFailure, }); } catch (e) { console.error(e); @@ -38,6 +40,29 @@ export const initSentry = (env: Record): string | undefined => { 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. + serverMessage.replace(/'(?:https?|blob):[^']*'/g, "''"), + ]; + } + return event; +}; + /** * Report an error to Sentry (if configured) and the console. * From 192fcc153df8b5648b7b229ae90d1a672f50be4e Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Thu, 12 Nov 2026 09:53:14 +0000 Subject: [PATCH 2/2] Keep blob URL script failures as a separate Sentry group The script path was resolved against a blob rather than our URL, so these can't be fixed by retrying and are worth tracking apart from network failures. --- src/logging/sentry.test.ts | 14 +++++++++++++- src/logging/sentry.ts | 8 ++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/logging/sentry.test.ts b/src/logging/sentry.test.ts index c4ac6cc44..3e969ca6b 100644 --- a/src/logging/sentry.test.ts +++ b/src/logging/sentry.test.ts @@ -133,11 +133,23 @@ describe("language server request failure grouping", () => { ]); expect( failure( - "blob:https://python.microbit.org/pyright-locale-en-100-ddc72e6b2c1f5de1a885.worker.js" + "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 '' failed to load.", + ]); + }); + it("leaves other errors to default grouping", () => { expect(fingerprint("boom")).toBeUndefined(); }); diff --git a/src/logging/sentry.ts b/src/logging/sentry.ts index 19285b84d..150078646 100644 --- a/src/logging/sentry.ts +++ b/src/logging/sentry.ts @@ -56,8 +56,12 @@ const fingerprintLanguageServerRequestFailure = ( event.fingerprint = [ "language-server-request-failed", method, - // Script URLs vary by locale and build. - serverMessage.replace(/'(?:https?|blob):[^']*'/g, "''"), + // 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 ? "''" : "''" + ), ]; } return event;