Skip to content
Merged

wip #2121

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
188 changes: 188 additions & 0 deletions __tests__/instrumentation-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
const mockInit = jest.fn();
const mockReplayIntegration = jest.fn(() => ({ name: "replay" }));
const mockCaptureRouterTransitionStart = jest.fn();

jest.mock("@sentry/nextjs", () => ({
__esModule: true,
init: mockInit,
replayIntegration: mockReplayIntegration,
captureRouterTransitionStart: mockCaptureRouterTransitionStart,
}));

describe("instrumentation-client beforeSendTransaction", () => {
const loadBeforeSendTransaction = () => {
jest.isolateModules(() => {
require("@/instrumentation-client");
});

const config = mockInit.mock.calls[0]?.[0];
expect(config).toBeDefined();
expect(typeof config.beforeSendTransaction).toBe("function");

return config.beforeSendTransaction as (event: Record<string, unknown>) => {
spans?: Array<{
description?: string | undefined;
data?: Record<string, unknown> | undefined;
}>;
tags?: Record<string, unknown>;
extra?: Record<string, unknown>;
};
};

beforeEach(() => {
jest.resetModules();
mockInit.mockReset();
mockReplayIntegration.mockReset();
mockReplayIntegration.mockImplementation(() => ({ name: "replay" }));
mockCaptureRouterTransitionStart.mockReset();
});

it("removes only the known noisy third-party telemetry spans", () => {
const beforeSendTransaction = loadBeforeSendTransaction();
const event = {
type: "transaction",
transaction: "/waves",
spans: [
{
op: "http.client",
description: "GET https://6529.io/waves",
data: {
"http.url": "https://6529.io/waves?_rsc=vusbg",
"http.response.status_code": 200,
"url.same_origin": true,
},
},
{
op: "http.client",
description:
"GET https://api.6529.io/api/waves/b6128077-ea78-4dd9-b381-52c4eadb2077",
data: {
"http.url":
"https://api.6529.io/api/waves/b6128077-ea78-4dd9-b381-52c4eadb2077",
"http.response.status_code": 200,
"url.same_origin": false,
},
},
{
op: "ui.long-animation-frame",
description: "Main UI thread blocked",
data: {
"code.filepath":
"https://dnclu2fna0b2b.cloudfront.net/_next/static/chunks/722c02d231c5c0f1.js",
},
},
{
op: "http.client",
description: "POST https://region1.google-analytics.com/g/collect",
data: {
"http.url": "https://region1.google-analytics.com/g/collect",
"http.response.status_code": 0,
"url.same_origin": false,
},
},
{
op: "http.client",
description: "POST https://cca-lite.coinbase.com/metrics",
data: {
"http.url": "https://cca-lite.coinbase.com/metrics",
"http.response.status_code": 0,
"url.same_origin": false,
},
},
{
op: "resource.beacon",
description: "https://cca-lite.coinbase.com/amp",
data: {
"http.response.status_code": 0,
"http.response_transfer_size": 0,
"url.same_origin": false,
},
},
],
};

const result = beforeSendTransaction(event);
const remainingDescriptions = result.spans?.map(
(span) => span.description ?? span.data?.["http.url"]
);

expect(remainingDescriptions).toEqual(
expect.arrayContaining([
"GET https://6529.io/waves",
"GET https://api.6529.io/api/waves/b6128077-ea78-4dd9-b381-52c4eadb2077",
"Main UI thread blocked",
])
);
expect(remainingDescriptions).not.toEqual(
expect.arrayContaining([
"POST https://region1.google-analytics.com/g/collect",
"POST https://cca-lite.coinbase.com/metrics",
"https://cca-lite.coinbase.com/amp",
])
);
expect(result.tags).toEqual(
expect.objectContaining({
third_party_span_noise_filtered: "true",
})
);
expect(result.extra).toEqual(
expect.objectContaining({
filteredThirdPartySpanCount: 3,
filteredThirdPartySpanKeys: [
"cca-lite.coinbase.com/amp",
"cca-lite.coinbase.com/metrics",
"region1.google-analytics.com/g/collect",
],
})
);
});

it("does not add audit metadata when no spans were filtered", () => {
const beforeSendTransaction = loadBeforeSendTransaction();
const event = {
type: "transaction",
transaction: "/waves",
spans: [
{
op: "http.client",
description: "GET https://6529.io/waves",
data: {
"http.url": "https://6529.io/waves?_rsc=vusbg",
"http.response.status_code": 200,
"url.same_origin": true,
},
},
{
op: "http.client",
description: "POST https://api-js.mixpanel.com/track/",
data: {
"http.url": "https://api-js.mixpanel.com/track/",
"http.response.status_code": 200,
"url.same_origin": false,
},
},
],
};

const result = beforeSendTransaction(event);

expect(result.spans).toHaveLength(2);
expect(result.tags?.["third_party_span_noise_filtered"]).toBeUndefined();
expect(result.extra?.["filteredThirdPartySpanCount"]).toBeUndefined();
expect(result.extra?.["filteredThirdPartySpanKeys"]).toBeUndefined();
});

it("is a no-op when the transaction has no spans", () => {
const beforeSendTransaction = loadBeforeSendTransaction();
const event = {
type: "transaction",
transaction: "/waves",
};

const result = beforeSendTransaction(event);

expect(result.spans).toBeUndefined();
expect(result.tags).toBeUndefined();
expect(result.extra).toBeUndefined();
});
});
89 changes: 89 additions & 0 deletions __tests__/utils/sentry-client-filters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@ import {
__testing,
shouldFilterByFilenameExceptions,
shouldFilterInjectedWalletCollision,
shouldFilterThirdPartyTelemetrySpan,
shouldFilterTwitterConfigReferenceError,
} from "@/utils/sentry-client-filters";

describe("sentry-client-filters", () => {
const buildSpan = (overrides: Record<string, unknown> = {}) =>
({
op: "http.client",
data: {
"http.url": "https://region1.google-analytics.com/g/collect",
"http.response.status_code": 0,
"url.same_origin": false,
},
...overrides,
}) as any;

const createTwitterConfigEvent = (overrides: Record<string, unknown> = {}) =>
({
exception: {
Expand Down Expand Up @@ -190,6 +202,83 @@ describe("sentry-client-filters", () => {
expect(result).toContain(expected);
});

it("filters failed Google Analytics collect spans with exact target matching", () => {
const result = shouldFilterThirdPartyTelemetrySpan(buildSpan());

expect(result).toBe(true);
});

it("filters failed Coinbase metrics spans with exact target matching", () => {
const result = shouldFilterThirdPartyTelemetrySpan(
buildSpan({
data: {
"http.url": "https://cca-lite.coinbase.com/metrics",
"http.response.status_code": 0,
"url.same_origin": false,
},
})
);

expect(result).toBe(true);
});

it("filters failed Coinbase amp beacons with zero transfer size", () => {
const result = shouldFilterThirdPartyTelemetrySpan({
op: "resource.beacon",
description: "https://cca-lite.coinbase.com/amp",
data: {
"http.response.status_code": 0,
"http.response_transfer_size": 0,
"url.same_origin": false,
},
});

expect(result).toBe(true);
});

it("does not filter first-party spans", () => {
const result = shouldFilterThirdPartyTelemetrySpan(
buildSpan({
data: {
"http.url":
"https://api.6529.io/api/waves/b6128077-ea78-4dd9-b381-52c4eadb2077",
"http.response.status_code": 0,
"url.same_origin": false,
},
})
);

expect(result).toBe(false);
});

it("does not filter allowlisted third-party targets when the request succeeded", () => {
const result = shouldFilterThirdPartyTelemetrySpan(
buildSpan({
data: {
"http.url": "https://region1.google-analytics.com/g/collect",
"http.response.status_code": 200,
"url.same_origin": false,
},
})
);

expect(result).toBe(false);
});

it("does not filter broader third-party domains outside the exact allowlist", () => {
const result = shouldFilterThirdPartyTelemetrySpan(
buildSpan({
data: {
"http.url": "https://api-js.mixpanel.com/track/",
"http.response.status_code": 0,
"url.same_origin": false,
},
})
);

expect(result).toBe(false);
});

it("filters Twitter CONFIG reference errors with app URI frames", () => {
// Arrange
const event = createTwitterConfigEvent();
Expand Down
68 changes: 67 additions & 1 deletion instrumentation-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ import {
sanitizeUrlString,
} from "@/utils/sentry-sanitizer";
import {
getThirdPartyTelemetrySpanTargetKey,
shouldFilterByFilenameExceptions,
shouldFilterInjectedWalletCollision,
shouldFilterThirdPartyTelemetrySpan,
shouldFilterTwitterConfigReferenceError,
type SentryTransactionSpan,
} from "@/utils/sentry-client-filters";
import * as Sentry from "@sentry/nextjs";

Expand All @@ -34,6 +37,11 @@ const noisyPatterns = [
const referenceErrors = ["__firefox__"];

const URL_REGEX = /\(([^)]+?)\)/;
type SentryTransactionEvent = Sentry.Event & {
spans?: SentryTransactionSpan[] | undefined;
tags?: Record<string, unknown> | undefined;
extra?: Record<string, unknown> | undefined;
};

function getFallbackMessage(hint?: Sentry.EventHint): string {
if (typeof hint?.originalException === "string") {
Expand Down Expand Up @@ -88,6 +96,62 @@ function shouldFilterEvent(
return shouldFilterByFilenameExceptions(frames, hint);
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

function filterNoisyThirdPartyTransactionSpans(
event: Sentry.Event
): Sentry.Event {
const transactionEvent = event as SentryTransactionEvent;
const spans = transactionEvent.spans;
if (!Array.isArray(spans) || spans.length === 0) {
return event;
}

const filteredSpanKeys = new Set<string>();
const keptSpans = spans.filter((span) => {
if (!shouldFilterThirdPartyTelemetrySpan(span)) {
return true;
}

const targetKey = getThirdPartyTelemetrySpanTargetKey(span);
if (targetKey) {
filteredSpanKeys.add(targetKey);
}
return false;
});

if (keptSpans.length === spans.length) {
return event;
}

const existingTags = isRecord(transactionEvent.tags)
? transactionEvent.tags
: {};
const existingExtra = isRecord(transactionEvent.extra)
? transactionEvent.extra
: {};

const nextEvent: SentryTransactionEvent = {
...transactionEvent,
spans: keptSpans,
tags: {
...existingTags,
third_party_span_noise_filtered: "true",
},
extra: {
...existingExtra,
filteredThirdPartySpanCount: spans.length - keptSpans.length,
filteredThirdPartySpanKeys: Array.from(filteredSpanKeys).sort(
(left, right) => left.localeCompare(right)
),
},
};

return nextEvent;
}

function handleIndexedDBError(event: Sentry.Event): void {
event.level = "warning";
event.tags = {
Expand Down Expand Up @@ -212,7 +276,9 @@ Sentry.init({
},

beforeSendTransaction(event) {
return sanitizeSentryEvent(event as any);
return sanitizeSentryEvent(
filterNoisyThirdPartyTransactionSpans(event) as any
);
},
Comment thread
simo6529 marked this conversation as resolved.
});

Expand Down
Loading
Loading