mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-12 12:18:55 +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>
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
// PII scrubbing for `$exception` events before they leave the client.
|
|
//
|
|
// Exception autocapture (`capture_exceptions: true`) sends the error message
|
|
// and stack. Stack frames are code locations (file / line / function) and are
|
|
// safe, but a message often interpolates user input — a validation error with
|
|
// the typed value, a parse error with the raw text, a network error with a URL
|
|
// that may carry a token. We keep the diagnostic shape (type + frames + the
|
|
// non-sensitive part of the message) and redact the patterns that carry user
|
|
// data. Wired as posthog-js `before_send`; see initAnalytics.
|
|
|
|
const REDACTED = "[redacted]";
|
|
|
|
// Order matters: strip query strings before the generic long-token rule, so a
|
|
// URL's host isn't itself shredded.
|
|
const PATTERNS: Array<[RegExp, string]> = [
|
|
// Emails.
|
|
[/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/gi, REDACTED],
|
|
// URL query/fragment (may carry tokens / PII) — keep scheme+host+path.
|
|
[/((?:https?|file|multica):\/\/[^\s?#]*)[?#]\S*/gi, `$1?${REDACTED}`],
|
|
// Long opaque tokens: JWTs, API keys, UUIDs, session ids (24+ chars).
|
|
[/\b[A-Za-z0-9_-]{24,}\b/g, REDACTED],
|
|
];
|
|
|
|
/** Redact PII-ish substrings from a free-text string. */
|
|
export function redactText(input: unknown): unknown {
|
|
if (typeof input !== "string" || input.length === 0) return input;
|
|
let out = input;
|
|
for (const [pattern, replacement] of PATTERNS) {
|
|
out = out.replace(pattern, replacement);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Redact the user-facing strings on a `$exception` event's properties in
|
|
* place: the top-level message and every entry's `value` in `$exception_list`.
|
|
* Types and stack frames are left untouched (code locations, not user data).
|
|
* Returns the same properties object for chaining.
|
|
*/
|
|
export function redactExceptionProperties(
|
|
properties: Record<string, unknown> | undefined,
|
|
): Record<string, unknown> | undefined {
|
|
if (!properties || typeof properties !== "object") return properties;
|
|
|
|
if ("$exception_message" in properties) {
|
|
properties.$exception_message = redactText(properties.$exception_message);
|
|
}
|
|
|
|
const list = properties.$exception_list;
|
|
if (Array.isArray(list)) {
|
|
for (const entry of list) {
|
|
if (entry && typeof entry === "object" && "value" in entry) {
|
|
(entry as { value: unknown }).value = redactText(
|
|
(entry as { value: unknown }).value,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return properties;
|
|
}
|