mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-13 13:18:56 +02:00
* feat(analytics): capture JS exceptions to PostHog Turn on posthog-js exception autocapture (window.onerror + unhandled rejections, with stack) and add a buffered captureException() wrapper for boundary-caught React errors those handlers can't see. Wire the web route-level global-error boundary to report through it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(diagnostics): add shared freeze watchdog Long-task observer (>=2s) emits client_unresponsive via captureEvent; client_type super-property tags desktop vs web for free. Installed once in CoreProvider so web and desktop share one in-thread, SSR-safe detector for recoverable freezes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(desktop): report true hangs and crashes via breadcrumb A real hang or crashed renderer can't report itself. The main process now persists a breadcrumb on unresponsive / render-process-gone, and the next renderer boot flushes it to PostHog (client_unresponsive / client_crash). A recovered hang clears its breadcrumb so it isn't double-counted by the in-thread watchdog. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(analytics): scrub PII from $exception before send Error messages can interpolate user input (typed values, URLs with tokens). Add a before_send hook that redacts emails, URL query strings, and long opaque tokens from the exception message and $exception_list values, keeping type + stack frames (code locations, not user data). Addresses the privacy gap from leaving capture_exceptions on with no sanitizer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: cover breadcrumb state machine and freeze watchdog The breadcrumb persist/clear orchestration is the correctness-critical part and was untested. Cover: hang->write, recover->clear (no double-count), recover-before-delay->no-op, force-quit->retained, crash->write-and-never- clear, clean-exit->no-write. Add watchdog tests (threshold, idempotent, SSR/PerformanceObserver no-op) via a fake observer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(desktop): breadcrumb field precedence + document limits Spread the persisted context FIRST so explicit event fields (source, recovered) always win over a future colliding context key. Document why preload-error skips the breadcrumb and the single-slot last-write-wins undercount limitation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 lines
2.2 KiB
TypeScript
69 lines
2.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { redactText, redactExceptionProperties } from "./redact-exception";
|
|
|
|
describe("redactText", () => {
|
|
it("redacts email addresses", () => {
|
|
expect(redactText("Invalid email: alice@example.com")).toBe(
|
|
"Invalid email: [redacted]",
|
|
);
|
|
});
|
|
|
|
it("strips URL query strings that may carry tokens, keeping host + path", () => {
|
|
expect(
|
|
redactText("fetch failed https://api.multica.ai/issues?token=abc123secret"),
|
|
).toBe("fetch failed https://api.multica.ai/issues?[redacted]");
|
|
});
|
|
|
|
it("redacts long opaque tokens (JWT / API key / uuid)", () => {
|
|
expect(redactText("auth header eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")).toBe(
|
|
"auth header [redacted]",
|
|
);
|
|
});
|
|
|
|
it("keeps the non-sensitive part of a message intact", () => {
|
|
expect(redactText("Cannot read property 'x' of undefined")).toBe(
|
|
"Cannot read property 'x' of undefined",
|
|
);
|
|
});
|
|
|
|
it("passes through non-strings unchanged", () => {
|
|
expect(redactText(undefined)).toBeUndefined();
|
|
expect(redactText(42)).toBe(42);
|
|
});
|
|
});
|
|
|
|
describe("redactExceptionProperties", () => {
|
|
it("scrubs the message and each $exception_list value, leaving frames untouched", () => {
|
|
const props = {
|
|
$exception_message: "Bad email bob@corp.com",
|
|
$exception_list: [
|
|
{
|
|
type: "TypeError",
|
|
value: "Token leaked: ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
stacktrace: { frames: [{ filename: "app.tsx", lineno: 5, function: "render" }] },
|
|
},
|
|
],
|
|
};
|
|
|
|
redactExceptionProperties(props);
|
|
|
|
const entry = props.$exception_list[0]!;
|
|
expect(props.$exception_message).toBe("Bad email [redacted]");
|
|
expect(entry.value).toBe("Token leaked: [redacted]");
|
|
// Frames are code locations, not user data — left intact.
|
|
expect(entry.stacktrace.frames[0]).toEqual({
|
|
filename: "app.tsx",
|
|
lineno: 5,
|
|
function: "render",
|
|
});
|
|
expect(entry.type).toBe("TypeError");
|
|
});
|
|
|
|
it("is safe on undefined / malformed properties", () => {
|
|
expect(redactExceptionProperties(undefined)).toBeUndefined();
|
|
expect(() =>
|
|
redactExceptionProperties({ $exception_list: "not-an-array" as unknown as [] }),
|
|
).not.toThrow();
|
|
});
|
|
});
|