Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/sentry-filter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { shouldDropInjectedBrowserEvent } from "./sentry-injected-browser";

// Surgical client-side Sentry noise filtering.
//
// This module holds the *decision* of whether a browser error event is one of
Expand Down Expand Up @@ -34,6 +36,7 @@ interface FilterableFrame {
filename?: string;
abs_path?: string;
module?: string;
function?: string;
}

interface FilterableException {
Expand Down Expand Up @@ -94,6 +97,8 @@ function frameIsKaltura(frame: FilterableFrame): boolean {
* otherwise. Pure and side-effect-free.
*/
export function shouldDropClientEvent(event: FilterableEvent): boolean {
if (shouldDropInjectedBrowserEvent(event)) return true;

const exceptions = event.exception?.values ?? [];

// 1. Kaltura player bundle frames (TRANSCRIPTS-2K, 2A). If any exception in
Expand Down
122 changes: 122 additions & 0 deletions lib/sentry-injected-browser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, expect, it } from "vitest";
import { shouldDropClientEvent } from "./sentry-filter";

const page = { filename: "app:///en/login", function: "global code" };
const bridge = {
filename: "app://navigation_performance_logger_android",
function: "sendDataToNative",
};
const app = { filename: "app:///lib/hooks/use-timezone.tsx", function: "read" };
const signatures = [
["ReferenceError", "Can't find variable: __firefox__", page],
[
"TypeError",
"undefined is not an object (evaluating 'window.__firefox__.reader')",
page,
],
[
"TypeError",
"undefined is not an object (evaluating 'window.__firefox__.refresh_youtube_quality_C85C5288CC2B4BDCBB20C5778007F667')",
page,
],
[
"Error",
"MetaMask extension not found",
{ filename: "app:///scripts/inpage.js" },
],
[
"i",
"Failed to connect to MetaMask",
{ filename: "app:///scripts/inpage.js" },
],
["Error", "Error invoking postMessage: Java object is gone", bridge],
] as const;

function exception(type: string, value: string, frame = app) {
return { type, value, stacktrace: { frames: [frame] } };
}

describe("confirmed injected browser signatures", () => {
it.each(signatures)(
"drops %s: %s only at its external source",
(type, value, frame) => {
const error = { type, value, stacktrace: { frames: [frame] } };
expect(shouldDropClientEvent({ exception: { values: [error] } })).toBe(
true,
);
},
);

it.each(signatures)("retains %s: %s from application code", (type, value) => {
expect(
shouldDropClientEvent({
exception: { values: [exception(type, value)] },
}),
).toBe(false);
});

it("retains application callers in a native logger stack", () => {
const error = exception("Error", signatures[5][1], bridge);
error.stacktrace.frames.unshift(app);
expect(shouldDropClientEvent({ exception: { values: [error] } })).toBe(
false,
);
});

it("drops the captured multi-frame native logger signature", () => {
const frames = ["sendINPMessage", "sendDataToNative"].map((name) => ({
filename: bridge.filename,
function: name,
}));
const error = {
type: "Error",
value: signatures[5][1],
stacktrace: { frames },
};
expect(shouldDropClientEvent({ exception: { values: [error] } })).toBe(
true,
);
});

it("retains a chain containing an application failure", () => {
const external = exception("Error", signatures[5][1], bridge);
const firstParty = exception(
"TypeError",
"Cannot read properties of undefined (reading 'name')",
);
expect(
shouldDropClientEvent({ exception: { values: [external, firstParty] } }),
).toBe(false);
});

it.each([
"Cannot read properties of undefined (reading 'M_ID')",
"Failed to fetch",
"The operation is insecure.",
"Object captured as promise rejection with keys: category, code, data, severity",
"This operation was aborted",
"controller[kState].transformAlgorithm is not a function",
])("retains unconfirmed or actionable failure: %s", (value) => {
expect(
shouldDropClientEvent({
exception: { values: [exception("Error", value)] },
}),
).toBe(false);
});

it("does not classify stackless errors or unrelated bridge errors as injection", () => {
const value = signatures[5][1];
expect(
shouldDropClientEvent({
exception: { values: [{ type: "Error", value }] },
}),
).toBe(false);
expect(
shouldDropClientEvent({
exception: {
values: [exception("Error", "Unexpected response", bridge)],
},
}),
).toBe(false);
});
});
77 changes: 77 additions & 0 deletions lib/sentry-injected-browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Only signatures attributed to injected browser scripts in the Sentry audit.
// Unknown executors (including M_ID), storage and media errors stay visible.
interface Frame {
filename?: string;
abs_path?: string;
function?: string;
}

interface Exception {
type?: string;
value?: string;
stacktrace?: { frames?: Frame[] };
}

interface BrowserEvent {
exception?: { values?: Exception[] };
}

const FIREFOX_MESSAGES = new Set([
"Can't find variable: __firefox__",
"undefined is not an object (evaluating 'window.__firefox__.reader')",
"undefined is not an object (evaluating 'window.__firefox__.refresh_youtube_quality_C85C5288CC2B4BDCBB20C5778007F667')",
]);

function source(frame: Frame): string {
return frame.abs_path ?? frame.filename ?? "";
}

function isInjectedFirefox(error: Exception, frames: Frame[]): boolean {
if (!FIREFOX_MESSAGES.has(error.value ?? "")) return false;
return frames.every(
(frame) =>
frame.function === "global code" &&
/^app:\/\/\/(en|fr|es|ar|ru|zh)(\/|$)/.test(source(frame)),
);
}

function isInjectedMetaMask(error: Exception, frames: Frame[]): boolean {
const known = [
"MetaMask extension not found",
"Failed to connect to MetaMask",
];
return (
known.includes(error.value ?? "") &&
frames.every((frame) => source(frame) === "app:///scripts/inpage.js")
);
}

function isInjectedPerformanceLogger(
error: Exception,
frames: Frame[],
): boolean {
return (
error.value === "Error invoking postMessage: Java object is gone" &&
frames.every(
(frame) =>
source(frame) === "app://navigation_performance_logger_android",
) &&
frames[frames.length - 1].function === "sendDataToNative"
);
}

function isKnownInjection(error: Exception): boolean {
const frames = error.stacktrace?.frames ?? [];
if (frames.length === 0) return false;
return (
isInjectedFirefox(error, frames) ||
isInjectedMetaMask(error, frames) ||
isInjectedPerformanceLogger(error, frames)
);
}

export function shouldDropInjectedBrowserEvent(event: BrowserEvent): boolean {
const values = event.exception?.values ?? [];
// Keep mixed exception chains: an injected cause does not excuse an app bug.
return values.length > 0 && values.every(isKnownInjection);
}
Loading