From d5bfc79112c04a0c4beb8df5abd10cf5083703d3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 6 Apr 2026 16:20:25 +0100 Subject: [PATCH] fix(discord): preserve stack hints for empty gateway type errors --- .../src/monitor/gateway-supervisor.test.ts | 26 ++++++++++---- .../discord/src/monitor/gateway-supervisor.ts | 36 +++++++++++++++---- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/extensions/discord/src/monitor/gateway-supervisor.test.ts b/extensions/discord/src/monitor/gateway-supervisor.test.ts index c35d22d022b..42ed517ea69 100644 --- a/extensions/discord/src/monitor/gateway-supervisor.test.ts +++ b/extensions/discord/src/monitor/gateway-supervisor.test.ts @@ -8,6 +8,8 @@ import { describe("classifyDiscordGatewayEvent", () => { it("maps current Carbon gateway errors onto domain events", () => { + const transientTypeError = new TypeError(); + transientTypeError.stack = "TypeError\n at gatewayCrash (discord-gateway.js:12:34)"; const reconnectEvent = classifyDiscordGatewayEvent({ err: new Error("Max reconnect attempts (0) reached after close code 1006"), isDisallowedIntentsError: () => false, @@ -21,7 +23,7 @@ describe("classifyDiscordGatewayEvent", () => { isDisallowedIntentsError: (err) => String(err).includes("4014"), }); const transientEvent = classifyDiscordGatewayEvent({ - err: new TypeError(), + err: transientTypeError, isDisallowedIntentsError: () => false, }); @@ -30,20 +32,24 @@ describe("classifyDiscordGatewayEvent", () => { expect(fatalEvent.type).toBe("fatal"); expect(disallowedEvent.type).toBe("disallowed-intents"); expect(transientEvent.type).toBe("fatal"); - expect(transientEvent.message).toBe("TypeError"); + expect(transientEvent.message).toBe("TypeError @ gatewayCrash (discord-gateway.js:12:34)"); expect(transientEvent.shouldStopLifecycle).toBe(true); }); it("wraps fatal lifecycle stops with discord-specific context", () => { + const transientTypeError = new TypeError(); + transientTypeError.stack = "TypeError\n at gatewayCrash (discord-gateway.js:12:34)"; const event = classifyDiscordGatewayEvent({ - err: new TypeError(), + err: transientTypeError, isDisallowedIntentsError: () => false, }); const wrapped = new DiscordGatewayLifecycleError(event); expect(wrapped.name).toBe("DiscordGatewayLifecycleError"); - expect(wrapped.message).toBe("discord gateway fatal: TypeError"); + expect(wrapped.message).toBe( + "discord gateway fatal: TypeError @ gatewayCrash (discord-gateway.js:12:34)", + ); expect(wrapped.eventType).toBe("fatal"); expect(wrapped.cause).toBeInstanceOf(TypeError); }); @@ -127,12 +133,18 @@ describe("createDiscordGatewaySupervisor", () => { }); supervisor.dispose(); - emitter.emit("error", new TypeError()); - emitter.emit("error", new TypeError()); + const first = new TypeError(); + first.stack = "TypeError\n at gatewayCrash (discord-gateway.js:12:34)"; + const second = new TypeError(); + second.stack = "TypeError\n at gatewayCrash (discord-gateway.js:12:34)"; + emitter.emit("error", first); + emitter.emit("error", second); expect(runtime.error).toHaveBeenCalledTimes(1); expect(runtime.error).toHaveBeenCalledWith( - expect.stringContaining("suppressed late gateway fatal error after dispose: TypeError"), + expect.stringContaining( + "suppressed late gateway fatal error after dispose: TypeError @ gatewayCrash (discord-gateway.js:12:34)", + ), ); }); }); diff --git a/extensions/discord/src/monitor/gateway-supervisor.ts b/extensions/discord/src/monitor/gateway-supervisor.ts index e2ebcbee8ec..71f7f5874ae 100644 --- a/extensions/discord/src/monitor/gateway-supervisor.ts +++ b/extensions/discord/src/monitor/gateway-supervisor.ts @@ -41,16 +41,40 @@ export type DiscordGatewaySupervisor = { type GatewaySupervisorPhase = "active" | "buffering" | "disposed" | "teardown"; +function readFirstStackFrame(err: Error): string | undefined { + const stack = err.stack; + if (!stack) { + return undefined; + } + const frame = stack + .split("\n") + .slice(1) + .map((line) => line.trim()) + .find(Boolean); + return frame ? frame.replace(/^at\s+/, "") : undefined; +} + +function formatDiscordGatewayErrorMessage(err: unknown): string { + if (!(err instanceof Error)) { + return formatErrorMessage(err); + } + if (err.message) { + const detail = formatErrorMessage(err); + return err.name ? `${err.name}: ${detail}` : detail; + } + const detail = formatErrorMessage(err); + const firstFrame = readFirstStackFrame(err); + if (firstFrame && detail === (err.name || "Error")) { + return `${detail} @ ${firstFrame}`; + } + return detail; +} + export function classifyDiscordGatewayEvent(params: { err: unknown; isDisallowedIntentsError: (err: unknown) => boolean; }): DiscordGatewayEvent { - const message = - params.err instanceof Error - ? params.err.message - ? `${params.err.name}: ${params.err.message}` - : params.err.name || "Error" - : formatErrorMessage(params.err); + const message = formatDiscordGatewayErrorMessage(params.err); if (params.isDisallowedIntentsError(params.err)) { return { type: "disallowed-intents",