Unverified 提交 6aabe7c5 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

fix: capture IPC exceptions in PostHog (#3022)

## Summary - route main-process `$exception` telemetry through `posthog.captureException()` in the renderer - normalize exception fields in IPC telemetry payloads and strip them from extra context before capture - add focused unit coverage for exception reconstruction and context extraction ## Test plan - npm run fmt && npm run lint:fix && npm run ts - npm test 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/3022" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end -->
上级 b372103a
{ {
"name": "dyad", "name": "dyad",
"version": "0.39.0", "version": "0.40.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "dyad", "name": "dyad",
"version": "0.39.0", "version": "0.40.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.46", "@ai-sdk/amazon-bedrock": "^4.0.46",
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
"lucide-react": "^0.487.0", "lucide-react": "^0.487.0",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"perfect-freehand": "^1.2.2", "perfect-freehand": "^1.2.2",
"posthog-js": "^1.236.3", "posthog-js": "^1.265.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-i18next": "^16.5.4", "react-i18next": "^16.5.4",
...@@ -19356,9 +19356,9 @@ ...@@ -19356,9 +19356,9 @@
} }
}, },
"node_modules/posthog-js": { "node_modules/posthog-js": {
"version": "1.261.8", "version": "1.265.1",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.261.8.tgz", "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.265.1.tgz",
"integrity": "sha512-HohKQ5Fuvei/3ZLIdayq6lDpeXsG891t2y2izpHu6q/1SoCS+HlYjViz3WCu9KlE7AfjfpwvN1kjnFNNPWeOig==", "integrity": "sha512-FKZtHIOBZ8pH80qXmWhoc2qUCkmOnKRdwKiyGourG03h7uV2w74cEmpVDtBxs2jSEbjMCbRi554W5lp6GsREOA==",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"dependencies": { "dependencies": {
"@posthog/core": "1.0.2", "@posthog/core": "1.0.2",
......
...@@ -107,7 +107,7 @@ ...@@ -107,7 +107,7 @@
"lucide-react": "^0.487.0", "lucide-react": "^0.487.0",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"perfect-freehand": "^1.2.2", "perfect-freehand": "^1.2.2",
"posthog-js": "^1.236.3", "posthog-js": "^1.265.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-i18next": "^16.5.4", "react-i18next": "^16.5.4",
......
import { describe, expect, it } from "vitest";
import {
createExceptionFromTelemetry,
getExceptionTelemetryContext,
} from "@/lib/posthogTelemetry";
describe("createExceptionFromTelemetry", () => {
it("uses exception telemetry fields when present", () => {
const error = createExceptionFromTelemetry({
exception_name: "TypeError",
exception_message: "Boom",
exception_stack_trace: "TypeError: Boom\n at ipc-handler",
});
expect(error).toBeInstanceOf(Error);
expect(error.name).toBe("TypeError");
expect(error.message).toBe("Boom");
expect(error.stack).toBe("TypeError: Boom\n at ipc-handler");
});
it("falls back to a default message when telemetry is incomplete", () => {
const error = createExceptionFromTelemetry(undefined);
expect(error.name).toBe("Error");
expect(error.message).toBe("Unknown IPC exception");
});
});
describe("getExceptionTelemetryContext", () => {
it("removes exception payload fields before passing custom context to PostHog", () => {
expect(
getExceptionTelemetryContext({
exception_name: "TypeError",
exception_message: "Boom",
exception_stack_trace: "TypeError: Boom\n at ipc-handler",
ipc_channel: "window:minimize",
}),
).toEqual({
ipc_channel: "window:minimize",
});
});
it("returns undefined when there is no custom context", () => {
expect(
getExceptionTelemetryContext({
exception_name: "TypeError",
exception_message: "Boom",
}),
).toBeUndefined();
});
});
import { describe, expect, it } from "vitest";
import { shouldFilterTelemetryException } from "@/ipc/utils/telemetry";
describe("shouldFilterTelemetryException", () => {
it("filters the known Supabase auth noise message", () => {
expect(
shouldFilterTelemetryException(
new Error(
"Supabase access token not found. Please authenticate first.",
),
),
).toBe(true);
});
it("does not filter different Supabase auth failures", () => {
expect(
shouldFilterTelemetryException(
new Error(
"Supabase access token not found for organization acme. Please authenticate first.",
),
),
).toBe(false);
});
});
...@@ -3,6 +3,9 @@ import log from "electron-log"; ...@@ -3,6 +3,9 @@ import log from "electron-log";
import { TelemetryEventPayload } from "@/ipc/types"; import { TelemetryEventPayload } from "@/ipc/types";
const logger = log.scope("telemetry"); const logger = log.scope("telemetry");
const FILTERED_EXCEPTION_MESSAGES = new Set([
"Supabase access token not found. Please authenticate first.",
]);
/** /**
* Sends a telemetry event from the main process to the renderer, * Sends a telemetry event from the main process to the renderer,
...@@ -36,10 +39,22 @@ export function sendTelemetryException( ...@@ -36,10 +39,22 @@ export function sendTelemetryException(
error instanceof Error error instanceof Error
? error ? error
: new Error(String(error ?? "Unknown error")); : new Error(String(error ?? "Unknown error"));
if (shouldFilterTelemetryException(err)) {
return;
}
sendTelemetryEvent("$exception", { sendTelemetryEvent("$exception", {
$exception_type: err.name, exception_name: err.name,
$exception_message: err.message, exception_message: err.message,
$exception_stack_trace_raw: err.stack, exception_stack_trace: err.stack,
...context, ...context,
}); });
} }
export function shouldFilterTelemetryException(error: unknown): boolean {
const message =
error instanceof Error ? error.message : String(error ?? "Unknown error");
return FILTERED_EXCEPTION_MESSAGES.has(message);
}
type TelemetryProperties = Record<string, unknown> | undefined;
export function createExceptionFromTelemetry(properties: TelemetryProperties) {
const exception = new Error(
typeof properties?.exception_message === "string"
? properties.exception_message
: "Unknown IPC exception",
);
if (typeof properties?.exception_name === "string") {
exception.name = properties.exception_name;
}
if (typeof properties?.exception_stack_trace === "string") {
exception.stack = properties.exception_stack_trace;
}
return exception;
}
export function getExceptionTelemetryContext(properties: TelemetryProperties) {
if (!properties) {
return undefined;
}
const {
exception_name: _exceptionName,
exception_message: _exceptionMessage,
exception_stack_trace: _exceptionStackTrace,
...context
} = properties;
return Object.keys(context).length > 0 ? context : undefined;
}
...@@ -28,6 +28,10 @@ import { ...@@ -28,6 +28,10 @@ import {
} from "./atoms/chatAtoms"; } from "./atoms/chatAtoms";
import { pendingQuestionnaireAtom } from "./atoms/planAtoms"; import { pendingQuestionnaireAtom } from "./atoms/planAtoms";
import { queryKeys } from "./lib/queryKeys"; import { queryKeys } from "./lib/queryKeys";
import {
createExceptionFromTelemetry,
getExceptionTelemetryContext,
} from "./lib/posthogTelemetry";
// @ts-ignore // @ts-ignore
console.log("Running in mode:", import.meta.env.MODE); console.log("Running in mode:", import.meta.env.MODE);
...@@ -233,6 +237,14 @@ function App() { ...@@ -233,6 +237,14 @@ function App() {
useEffect(() => { useEffect(() => {
const unsubscribe = ipc.events.system.onTelemetryEvent( const unsubscribe = ipc.events.system.onTelemetryEvent(
({ eventName, properties }) => { ({ eventName, properties }) => {
if (eventName === "$exception") {
posthog.captureException(
createExceptionFromTelemetry(properties),
getExceptionTelemetryContext(properties),
);
return;
}
posthog.capture(eventName, properties); posthog.capture(eventName, properties);
}, },
); );
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论