mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
feat(analytics): drop benign ResizeObserver exceptions from telemetry
ResizeObserver "loop ..." errors are the dominant `$exception` bucket (~31k events / ~1.5k users). They are a benign, self-recovering browser quirk with no actionable signal and otherwise drown real failures and burn the event budget. Drop them entirely in before_send (ahead of redaction and the dedupe fuse, which only caps repeats). The match is narrow — only the benign "loop" phrasing — so a genuine ResizeObserver bug still reports. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
72
packages/core/analytics/benign-exceptions.test.ts
Normal file
72
packages/core/analytics/benign-exceptions.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isBenignException } from "./benign-exceptions";
|
||||
|
||||
describe("isBenignException", () => {
|
||||
it("drops ResizeObserver loop errors via $exception_list value", () => {
|
||||
expect(
|
||||
isBenignException({
|
||||
$exception_list: [
|
||||
{
|
||||
type: "Error",
|
||||
value: "ResizeObserver loop completed with undelivered notifications.",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("drops the older 'loop limit exceeded' phrasing", () => {
|
||||
expect(
|
||||
isBenignException({
|
||||
$exception_list: [
|
||||
{ type: "Error", value: "ResizeObserver loop limit exceeded" },
|
||||
],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("drops when the signal is on the top-level $exception_message", () => {
|
||||
expect(
|
||||
isBenignException({
|
||||
$exception_message: "ResizeObserver loop limit exceeded",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("matches case-insensitively", () => {
|
||||
expect(
|
||||
isBenignException({ $exception_message: "resizeobserver LOOP limit exceeded" }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps real errors", () => {
|
||||
expect(
|
||||
isBenignException({
|
||||
$exception_list: [
|
||||
{
|
||||
type: "TypeError",
|
||||
value: "Cannot read properties of undefined (reading 'split')",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not match an unrelated mention of ResizeObserver", () => {
|
||||
// Only the benign "loop" phrasing is silenced; a genuine bug in
|
||||
// ResizeObserver usage must still be reported.
|
||||
expect(
|
||||
isBenignException({
|
||||
$exception_message: "ResizeObserver is not defined",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("fails open on missing or malformed properties", () => {
|
||||
expect(isBenignException(undefined)).toBe(false);
|
||||
expect(isBenignException({})).toBe(false);
|
||||
expect(isBenignException({ $exception_list: "not-an-array" })).toBe(false);
|
||||
expect(isBenignException({ $exception_list: [null, 42, {}] })).toBe(false);
|
||||
expect(isBenignException({ $exception_message: 123 })).toBe(false);
|
||||
});
|
||||
});
|
||||
52
packages/core/analytics/benign-exceptions.ts
Normal file
52
packages/core/analytics/benign-exceptions.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// Known-benign browser exceptions that are pure noise in `$exception`
|
||||
// telemetry. These are dropped ENTIRELY in `before_send` (not merely deduped by
|
||||
// exception-dedupe.ts) — they carry no actionable signal, the browser
|
||||
// self-recovers, and at scale they dominate the error stream, drowning real
|
||||
// failures and burning the billed event budget.
|
||||
//
|
||||
// ResizeObserver "loop ..." errors are the canonical case: the spec fires them
|
||||
// when observation callbacks don't settle within a single animation frame. The
|
||||
// browser resumes delivery on the next frame, so nothing actually breaks. Every
|
||||
// app that uses ResizeObserver (directly or via a UI library) emits them. The
|
||||
// CSSWG explicitly considers them benign — see w3c/csswg-drafts#5023. Across
|
||||
// Chrome versions the message is either "ResizeObserver loop limit exceeded"
|
||||
// (older) or "ResizeObserver loop completed with undelivered notifications"
|
||||
// (newer); both contain "ResizeObserver loop".
|
||||
//
|
||||
// The bar for adding a pattern here is high: it must be a benign,
|
||||
// self-recovering error with no actionable signal. A real bug must never be
|
||||
// silenced — when unsure, leave it to the dedupe fuse, which only caps repeats.
|
||||
|
||||
const BENIGN_MESSAGE_PATTERNS: RegExp[] = [/ResizeObserver loop/i];
|
||||
|
||||
/**
|
||||
* Whether this `$exception` event is known-benign browser noise that should be
|
||||
* dropped entirely. Reads the message from the (pre-redaction) event
|
||||
* properties — the matched messages carry no PII, so reading them raw is safe,
|
||||
* and matching before redaction avoids any chance of a scrub mangling the
|
||||
* signal. Never throws: any unexpected shape returns `false` (keep the event),
|
||||
* the fail-open direction `before_send` requires.
|
||||
*/
|
||||
export function isBenignException(
|
||||
properties: Record<string, unknown> | undefined,
|
||||
): boolean {
|
||||
if (!properties || typeof properties !== "object") return false;
|
||||
|
||||
const messages: unknown[] = [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) {
|
||||
messages.push((entry as { value: unknown }).value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const message of messages) {
|
||||
if (typeof message !== "string") continue;
|
||||
for (const pattern of BENIGN_MESSAGE_PATTERNS) {
|
||||
if (pattern.test(message)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
import posthog from "posthog-js";
|
||||
import { redactExceptionProperties } from "./redact-exception";
|
||||
import { shouldDropException } from "./exception-dedupe";
|
||||
import { isBenignException } from "./benign-exceptions";
|
||||
|
||||
export const EVENT_SCHEMA_VERSION = 2;
|
||||
|
||||
@@ -166,6 +167,11 @@ export function initAnalytics(config: AnalyticsConfig | null | undefined): boole
|
||||
capture_exceptions: true,
|
||||
before_send: (event) => {
|
||||
if (event && event.event === "$exception") {
|
||||
// Drop known-benign browser noise (e.g. ResizeObserver loop) entirely
|
||||
// — checked on the raw message before redaction. These dominate the
|
||||
// stream and carry no signal, so they skip both redaction and the
|
||||
// dedupe fuse. See benign-exceptions.ts.
|
||||
if (isBenignException(event.properties)) return null;
|
||||
redactExceptionProperties(event.properties);
|
||||
if (shouldDropException(event.properties)) return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user