From e9cbbf735bfcde53de7fca879488f94a64cb1be9 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Sat, 25 Apr 2026 12:19:14 +0200 Subject: [PATCH 1/9] implement --- ...lowQualityTransactionsFilterIntegration.ts | 37 ------------------- packages/react-router/src/server/sdk.ts | 9 ++++- 2 files changed, 7 insertions(+), 39 deletions(-) delete mode 100644 packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts diff --git a/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts b/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts deleted file mode 100644 index e4471167f7ce..000000000000 --- a/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { type Client, debug, defineIntegration, type Event, type EventHint } from '@sentry/core'; -import type { NodeOptions } from '@sentry/node'; - -/** - * Integration that filters out noisy http transactions such as requests to node_modules, favicon.ico, @id/ - * - */ - -function _lowQualityTransactionsFilterIntegration(options: NodeOptions): { - name: string; - processEvent: (event: Event, hint: EventHint, client: Client) => Event | null; -} { - const matchedRegexes = [/GET \/node_modules\//, /GET \/favicon\.ico/, /GET \/@id\//, /GET \/__manifest\?/]; - - return { - name: 'LowQualityTransactionsFilter', - - processEvent(event: Event, _hint: EventHint, _client: Client): Event | null { - if (event.type !== 'transaction' || !event.transaction) { - return event; - } - - const transaction = event.transaction; - - if (matchedRegexes.some(regex => transaction.match(regex))) { - options.debug && debug.log('[ReactRouter] Filtered node_modules transaction:', event.transaction); - return null; - } - - return event; - }, - }; -} - -export const lowQualityTransactionsFilterIntegration = defineIntegration((options: NodeOptions) => - _lowQualityTransactionsFilterIntegration(options), -); diff --git a/packages/react-router/src/server/sdk.ts b/packages/react-router/src/server/sdk.ts index 8c3954e4a418..37a75693ec68 100644 --- a/packages/react-router/src/server/sdk.ts +++ b/packages/react-router/src/server/sdk.ts @@ -3,7 +3,6 @@ import { applySdkMetadata, debug, setTag } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { getDefaultIntegrations as getNodeDefaultIntegrations, init as initNodeSdk } from '@sentry/node'; import { DEBUG_BUILD } from '../common/debug-build'; -import { lowQualityTransactionsFilterIntegration } from './integration/lowQualityTransactionsFilterIntegration'; import { reactRouterServerIntegration } from './integration/reactRouterServer'; /** @@ -13,11 +12,12 @@ import { reactRouterServerIntegration } from './integration/reactRouterServer'; export function getDefaultReactRouterServerIntegrations(options: NodeOptions): Integration[] { return [ ...getNodeDefaultIntegrations(options), - lowQualityTransactionsFilterIntegration(options), reactRouterServerIntegration(), ]; } +const LOW_QUALITY_TRANSACTIONS_REGEXES = [/GET \/node_modules\//, /GET \/favicon\.ico/, /GET \/@id\//, /GET \/__manifest\?/]; + /** * Initializes the server side of the React Router SDK */ @@ -27,6 +27,11 @@ export function init(options: NodeOptions): NodeClient | undefined { defaultIntegrations: getDefaultReactRouterServerIntegrations(options), }; + opts.ignoreSpans = [ + ...(opts.ignoreSpans || []), + ...LOW_QUALITY_TRANSACTIONS_REGEXES, + ]; + DEBUG_BUILD && debug.log('Initializing SDK...'); applySdkMetadata(opts, 'react-router', ['react-router', 'node']); From 828583daea01bdf686c6dcdd0b96dc5456cdb152 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Sat, 25 Apr 2026 12:45:36 +0200 Subject: [PATCH 2/9] implement --- .../low-quality-filter.server.test.ts | 29 ++++++++ packages/react-router/src/server/sdk.ts | 17 +++-- ...alityTransactionsFilterIntegration.test.ts | 67 ------------------- packages/react-router/test/server/sdk.test.ts | 38 +++++------ 4 files changed, 56 insertions(+), 95 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts delete mode 100644 packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts new file mode 100644 index 000000000000..1ebfeab737d9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts @@ -0,0 +1,29 @@ +import { test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { APP_NAME } from '../constants'; + +test.describe('low-quality transaction filter', () => { + test('does not send a server transaction for /__manifest? requests', async ({ page }) => { + // Positive anchor: the navigation transaction we know the framework emits + const navigationPromise = waitForTransaction(APP_NAME, async transactionEvent => { + return ( + transactionEvent.transaction === '/performance/ssr' && transactionEvent.contexts?.trace?.op === 'navigation' + ); + }); + + // Negative: throw if a server txn for /__manifest? sneaks through + waitForTransaction(APP_NAME, async evt => { + if (evt.transaction?.match(/GET \/__manifest\?/)) { + throw new Error('Filtered manifest server transaction should not be sent'); + } + return false; + }); + + await page.goto('/performance'); // pageload + await page.waitForTimeout(1000); + await page.getByRole('link', { name: 'SSR Page' }).click(); // navigation → fetches /__manifest? + + await navigationPromise; + await page.waitForTimeout(1000); // give late server txns a chance to flush + }); +}); diff --git a/packages/react-router/src/server/sdk.ts b/packages/react-router/src/server/sdk.ts index 37a75693ec68..9d9c8817c360 100644 --- a/packages/react-router/src/server/sdk.ts +++ b/packages/react-router/src/server/sdk.ts @@ -10,13 +10,15 @@ import { reactRouterServerIntegration } from './integration/reactRouterServer'; * @param options The options for the SDK. */ export function getDefaultReactRouterServerIntegrations(options: NodeOptions): Integration[] { - return [ - ...getNodeDefaultIntegrations(options), - reactRouterServerIntegration(), - ]; + return [...getNodeDefaultIntegrations(options), reactRouterServerIntegration()]; } -const LOW_QUALITY_TRANSACTIONS_REGEXES = [/GET \/node_modules\//, /GET \/favicon\.ico/, /GET \/@id\//, /GET \/__manifest\?/]; +const LOW_QUALITY_TRANSACTIONS_REGEXES = [ + /GET \/node_modules\//, + /GET \/favicon\.ico/, + /GET \/@id\//, + /GET \/__manifest\?/, +]; /** * Initializes the server side of the React Router SDK @@ -27,10 +29,7 @@ export function init(options: NodeOptions): NodeClient | undefined { defaultIntegrations: getDefaultReactRouterServerIntegrations(options), }; - opts.ignoreSpans = [ - ...(opts.ignoreSpans || []), - ...LOW_QUALITY_TRANSACTIONS_REGEXES, - ]; + opts.ignoreSpans = [...(opts.ignoreSpans || []), ...LOW_QUALITY_TRANSACTIONS_REGEXES]; DEBUG_BUILD && debug.log('Initializing SDK...'); diff --git a/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts b/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts deleted file mode 100644 index 7edd75c9e996..000000000000 --- a/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { Event, EventType } from '@sentry/core'; -import * as SentryCore from '@sentry/core'; -import * as SentryNode from '@sentry/node'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { lowQualityTransactionsFilterIntegration } from '../../src/server/integration/lowQualityTransactionsFilterIntegration'; - -const debugLoggerLogSpy = vi.spyOn(SentryCore.debug, 'log').mockImplementation(() => {}); - -describe('Low Quality Transactions Filter Integration', () => { - afterEach(() => { - vi.clearAllMocks(); - SentryNode.getGlobalScope().clear(); - }); - - describe('integration functionality', () => { - describe('filters out low quality transactions', () => { - it.each([ - ['node_modules requests', 'GET /node_modules/some-package/index.js'], - ['favicon.ico requests', 'GET /favicon.ico'], - ['@id/ requests', 'GET /@id/some-id'], - ['manifest requests', 'GET /__manifest?p=%2Fperformance%2Fserver-action'], - ])('%s', (description, transaction) => { - const integration = lowQualityTransactionsFilterIntegration({ debug: true }); - const event = { - type: 'transaction' as EventType, - transaction, - } as Event; - - const result = integration.processEvent!(event, {}, {} as SentryCore.Client); - - expect(result).toBeNull(); - - expect(debugLoggerLogSpy).toHaveBeenCalledWith('[ReactRouter] Filtered node_modules transaction:', transaction); - }); - }); - - describe('allows high quality transactions', () => { - it.each([ - ['normal page requests', 'GET /api/users'], - ['API endpoints', 'POST /data'], - ['app routes', 'GET /projects/123'], - ])('%s', (description, transaction) => { - const integration = lowQualityTransactionsFilterIntegration({}); - const event = { - type: 'transaction' as EventType, - transaction, - } as Event; - - const result = integration.processEvent!(event, {}, {} as SentryCore.Client); - - expect(result).toEqual(event); - }); - }); - - it('does not affect non-transaction events', () => { - const integration = lowQualityTransactionsFilterIntegration({}); - const event = { - type: 'error' as EventType, - transaction: 'GET /node_modules/some-package/index.js', - } as Event; - - const result = integration.processEvent!(event, {}, {} as SentryCore.Client); - - expect(result).toEqual(event); - }); - }); -}); diff --git a/packages/react-router/test/server/sdk.test.ts b/packages/react-router/test/server/sdk.test.ts index 6e1879f8e24b..743b36d4108e 100644 --- a/packages/react-router/test/server/sdk.test.ts +++ b/packages/react-router/test/server/sdk.test.ts @@ -3,7 +3,6 @@ import type { NodeClient } from '@sentry/node'; import * as SentryNode from '@sentry/node'; import { SDK_VERSION } from '@sentry/node'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import * as LowQualityModule from '../../src/server/integration/lowQualityTransactionsFilterIntegration'; import { init as reactRouterInit } from '../../src/server/sdk'; const nodeInit = vi.spyOn(SentryNode, 'init'); @@ -48,28 +47,29 @@ describe('React Router server SDK', () => { expect(client).not.toBeUndefined(); }); - it('adds the low quality transactions filter integration by default', () => { - const filterSpy = vi.spyOn(LowQualityModule, 'lowQualityTransactionsFilterIntegration'); - - reactRouterInit({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - }); - - expect(filterSpy).toHaveBeenCalled(); - - expect(nodeInit).toHaveBeenCalledTimes(1); - const initOptions = nodeInit.mock.calls[0]?.[0]; + it('configures ignoreSpans to drop low-quality transactions', () => { + reactRouterInit({}); - expect(initOptions).toBeDefined(); + expect(nodeInit).toHaveBeenCalledWith( + expect.objectContaining({ + ignoreSpans: expect.arrayContaining([ + /GET \/node_modules\//, + /GET \/favicon\.ico/, + /GET \/@id\//, + /GET \/__manifest\?/, + ]), + }), + ); + }); - const defaultIntegrations = initOptions?.defaultIntegrations as Integration[]; - expect(Array.isArray(defaultIntegrations)).toBe(true); + it('preserves user-provided ignoreSpans entries', () => { + reactRouterInit({ ignoreSpans: [/keep-me/] }); - const filterIntegration = defaultIntegrations.find( - integration => integration.name === 'LowQualityTransactionsFilter', + expect(nodeInit).toHaveBeenCalledWith( + expect.objectContaining({ + ignoreSpans: expect.arrayContaining([/keep-me/]), + }), ); - - expect(filterIntegration).toBeDefined(); }); it('adds reactRouterServer integration by default', () => { From 6f07617b573a7fbfadf5a2b489df610f99925830 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 27 Apr 2026 13:53:33 +0200 Subject: [PATCH 3/9] use beforeSetupg --- ...lowQualityTransactionsFilterIntegration.ts | 24 ++++++++++++ packages/react-router/src/server/sdk.ts | 16 +++----- ...alityTransactionsFilterIntegration.test.ts | 39 +++++++++++++++++++ packages/react-router/test/server/sdk.test.ts | 38 +++++++++--------- 4 files changed, 88 insertions(+), 29 deletions(-) create mode 100644 packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts create mode 100644 packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts diff --git a/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts b/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts new file mode 100644 index 000000000000..dcdd459a5193 --- /dev/null +++ b/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts @@ -0,0 +1,24 @@ +import type { Client, IntegrationFn } from '@sentry/core'; +import { defineIntegration } from '@sentry/core'; +import type { NodeOptions } from '@sentry/node'; + +const LOW_QUALITY_TRANSACTIONS_REGEXES = [ + /GET \/node_modules\//, + /GET \/favicon\.ico/, + /GET \/@id\//, + /GET \/__manifest\?/, +]; + +const _lowQualityTransactionsFilterIntegration = ((_options?: NodeOptions) => ({ + name: 'LowQualityTransactionsFilter', + beforeSetup(client: Client) { + const opts = client.getOptions(); + opts.ignoreSpans = [...(opts.ignoreSpans || []), ...LOW_QUALITY_TRANSACTIONS_REGEXES]; + }, +})) satisfies IntegrationFn; + +/** + * Integration that filters out noisy http transactions such as requests to node_modules, favicon.ico, @id/, __manifest. + * Adds regex entries to `ignoreSpans` so the filter applies in both static and streaming trace lifecycles. + */ +export const lowQualityTransactionsFilterIntegration = defineIntegration(_lowQualityTransactionsFilterIntegration); diff --git a/packages/react-router/src/server/sdk.ts b/packages/react-router/src/server/sdk.ts index 9d9c8817c360..8c3954e4a418 100644 --- a/packages/react-router/src/server/sdk.ts +++ b/packages/react-router/src/server/sdk.ts @@ -3,6 +3,7 @@ import { applySdkMetadata, debug, setTag } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { getDefaultIntegrations as getNodeDefaultIntegrations, init as initNodeSdk } from '@sentry/node'; import { DEBUG_BUILD } from '../common/debug-build'; +import { lowQualityTransactionsFilterIntegration } from './integration/lowQualityTransactionsFilterIntegration'; import { reactRouterServerIntegration } from './integration/reactRouterServer'; /** @@ -10,16 +11,13 @@ import { reactRouterServerIntegration } from './integration/reactRouterServer'; * @param options The options for the SDK. */ export function getDefaultReactRouterServerIntegrations(options: NodeOptions): Integration[] { - return [...getNodeDefaultIntegrations(options), reactRouterServerIntegration()]; + return [ + ...getNodeDefaultIntegrations(options), + lowQualityTransactionsFilterIntegration(options), + reactRouterServerIntegration(), + ]; } -const LOW_QUALITY_TRANSACTIONS_REGEXES = [ - /GET \/node_modules\//, - /GET \/favicon\.ico/, - /GET \/@id\//, - /GET \/__manifest\?/, -]; - /** * Initializes the server side of the React Router SDK */ @@ -29,8 +27,6 @@ export function init(options: NodeOptions): NodeClient | undefined { defaultIntegrations: getDefaultReactRouterServerIntegrations(options), }; - opts.ignoreSpans = [...(opts.ignoreSpans || []), ...LOW_QUALITY_TRANSACTIONS_REGEXES]; - DEBUG_BUILD && debug.log('Initializing SDK...'); applySdkMetadata(opts, 'react-router', ['react-router', 'node']); diff --git a/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts b/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts new file mode 100644 index 000000000000..085b204e639c --- /dev/null +++ b/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts @@ -0,0 +1,39 @@ +import type { Client, ClientOptions } from '@sentry/core'; +import { describe, expect, it } from 'vitest'; +import { lowQualityTransactionsFilterIntegration } from '../../src/server/integration/lowQualityTransactionsFilterIntegration'; + +function makeMockClient(initial: Partial = {}): Client { + const options = { ...initial } as ClientOptions; + return { getOptions: () => options } as Client; +} + +describe('lowQualityTransactionsFilterIntegration', () => { + it('appends the low-quality regexes to ignoreSpans', () => { + const integration = lowQualityTransactionsFilterIntegration({}); + const client = makeMockClient(); + + integration.beforeSetup!(client); + + expect(client.getOptions().ignoreSpans).toEqual([ + /GET \/node_modules\//, + /GET \/favicon\.ico/, + /GET \/@id\//, + /GET \/__manifest\?/, + ]); + }); + + it('preserves user-provided ignoreSpans entries', () => { + const integration = lowQualityTransactionsFilterIntegration({}); + const client = makeMockClient({ ignoreSpans: [/keep-me/] }); + + integration.beforeSetup!(client); + + expect(client.getOptions().ignoreSpans).toEqual([ + /keep-me/, + /GET \/node_modules\//, + /GET \/favicon\.ico/, + /GET \/@id\//, + /GET \/__manifest\?/, + ]); + }); +}); diff --git a/packages/react-router/test/server/sdk.test.ts b/packages/react-router/test/server/sdk.test.ts index 743b36d4108e..6e1879f8e24b 100644 --- a/packages/react-router/test/server/sdk.test.ts +++ b/packages/react-router/test/server/sdk.test.ts @@ -3,6 +3,7 @@ import type { NodeClient } from '@sentry/node'; import * as SentryNode from '@sentry/node'; import { SDK_VERSION } from '@sentry/node'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import * as LowQualityModule from '../../src/server/integration/lowQualityTransactionsFilterIntegration'; import { init as reactRouterInit } from '../../src/server/sdk'; const nodeInit = vi.spyOn(SentryNode, 'init'); @@ -47,29 +48,28 @@ describe('React Router server SDK', () => { expect(client).not.toBeUndefined(); }); - it('configures ignoreSpans to drop low-quality transactions', () => { - reactRouterInit({}); + it('adds the low quality transactions filter integration by default', () => { + const filterSpy = vi.spyOn(LowQualityModule, 'lowQualityTransactionsFilterIntegration'); - expect(nodeInit).toHaveBeenCalledWith( - expect.objectContaining({ - ignoreSpans: expect.arrayContaining([ - /GET \/node_modules\//, - /GET \/favicon\.ico/, - /GET \/@id\//, - /GET \/__manifest\?/, - ]), - }), - ); - }); + reactRouterInit({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }); - it('preserves user-provided ignoreSpans entries', () => { - reactRouterInit({ ignoreSpans: [/keep-me/] }); + expect(filterSpy).toHaveBeenCalled(); - expect(nodeInit).toHaveBeenCalledWith( - expect.objectContaining({ - ignoreSpans: expect.arrayContaining([/keep-me/]), - }), + expect(nodeInit).toHaveBeenCalledTimes(1); + const initOptions = nodeInit.mock.calls[0]?.[0]; + + expect(initOptions).toBeDefined(); + + const defaultIntegrations = initOptions?.defaultIntegrations as Integration[]; + expect(Array.isArray(defaultIntegrations)).toBe(true); + + const filterIntegration = defaultIntegrations.find( + integration => integration.name === 'LowQualityTransactionsFilter', ); + + expect(filterIntegration).toBeDefined(); }); it('adds reactRouterServer integration by default', () => { From 0d13abd42fd6f8d70d9e1112167ed6e65ae16c91 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 27 Apr 2026 14:00:04 +0200 Subject: [PATCH 4/9] . --- ...alityTransactionsFilterIntegration.test.ts | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts b/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts index 085b204e639c..b6901934e1ec 100644 --- a/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts +++ b/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts @@ -1,4 +1,5 @@ import type { Client, ClientOptions } from '@sentry/core'; +import { shouldIgnoreSpan } from '@sentry/core'; import { describe, expect, it } from 'vitest'; import { lowQualityTransactionsFilterIntegration } from '../../src/server/integration/lowQualityTransactionsFilterIntegration'; @@ -7,14 +8,16 @@ function makeMockClient(initial: Partial = {}): Client { return { getOptions: () => options } as Client; } +function setupIntegrationAndGetIgnoreSpans(initial: Partial = {}) { + const integration = lowQualityTransactionsFilterIntegration({}); + const client = makeMockClient(initial); + integration.beforeSetup!(client); + return client.getOptions().ignoreSpans!; +} + describe('lowQualityTransactionsFilterIntegration', () => { it('appends the low-quality regexes to ignoreSpans', () => { - const integration = lowQualityTransactionsFilterIntegration({}); - const client = makeMockClient(); - - integration.beforeSetup!(client); - - expect(client.getOptions().ignoreSpans).toEqual([ + expect(setupIntegrationAndGetIgnoreSpans()).toEqual([ /GET \/node_modules\//, /GET \/favicon\.ico/, /GET \/@id\//, @@ -23,12 +26,7 @@ describe('lowQualityTransactionsFilterIntegration', () => { }); it('preserves user-provided ignoreSpans entries', () => { - const integration = lowQualityTransactionsFilterIntegration({}); - const client = makeMockClient({ ignoreSpans: [/keep-me/] }); - - integration.beforeSetup!(client); - - expect(client.getOptions().ignoreSpans).toEqual([ + expect(setupIntegrationAndGetIgnoreSpans({ ignoreSpans: [/keep-me/] })).toEqual([ /keep-me/, /GET \/node_modules\//, /GET \/favicon\.ico/, @@ -36,4 +34,27 @@ describe('lowQualityTransactionsFilterIntegration', () => { /GET \/__manifest\?/, ]); }); + + describe('drops low-quality transactions', () => { + it.each([ + ['node_modules requests', 'GET /node_modules/some-package/index.js'], + ['favicon.ico requests', 'GET /favicon.ico'], + ['@id/ requests', 'GET /@id/some-id'], + ['manifest requests', 'GET /__manifest?p=%2Fperformance%2Fserver-action'], + ])('%s', (_label, name) => { + const ignoreSpans = setupIntegrationAndGetIgnoreSpans(); + expect(shouldIgnoreSpan({ description: name, op: 'http.server' }, ignoreSpans)).toBe(true); + }); + }); + + describe('keeps high-quality transactions', () => { + it.each([ + ['normal page requests', 'GET /api/users'], + ['API endpoints', 'POST /data'], + ['app routes', 'GET /projects/123'], + ])('%s', (_label, name) => { + const ignoreSpans = setupIntegrationAndGetIgnoreSpans(); + expect(shouldIgnoreSpan({ description: name, op: 'http.server' }, ignoreSpans)).toBe(false); + }); + }); }); From 8d7fac2e1745179aa2bf9489c1c1612899ffb2d9 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 27 Apr 2026 14:18:58 +0200 Subject: [PATCH 5/9] . --- .../low-quality-filter.server.test.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts index 1ebfeab737d9..cb01bbc992ad 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts @@ -1,29 +1,28 @@ -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; +import type { Event } from '@sentry/core'; import { APP_NAME } from '../constants'; test.describe('low-quality transaction filter', () => { test('does not send a server transaction for /__manifest? requests', async ({ page }) => { - // Positive anchor: the navigation transaction we know the framework emits + const serverTxns: Event[] = []; + const navigationPromise = waitForTransaction(APP_NAME, async transactionEvent => { return ( transactionEvent.transaction === '/performance/ssr' && transactionEvent.contexts?.trace?.op === 'navigation' ); }); - // Negative: throw if a server txn for /__manifest? sneaks through waitForTransaction(APP_NAME, async evt => { - if (evt.transaction?.match(/GET \/__manifest\?/)) { - throw new Error('Filtered manifest server transaction should not be sent'); - } + serverTxns.push(evt); return false; }); - await page.goto('/performance'); // pageload - await page.waitForTimeout(1000); - await page.getByRole('link', { name: 'SSR Page' }).click(); // navigation → fetches /__manifest? + await page.goto('/performance'); + await page.getByRole('link', { name: 'SSR Page' }).click(); await navigationPromise; - await page.waitForTimeout(1000); // give late server txns a chance to flush + + expect(serverTxns.some(t => t.transaction?.match(/GET \/__manifest\?/))).toBe(false); }); }); From 3cf652bb2b88f5c248e1419f55003132025a22e1 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 27 Apr 2026 14:29:21 +0200 Subject: [PATCH 6/9] flush pattern in e2e test --- .../react-router-7-framework/app/routes.ts | 1 + .../react-router-7-framework/app/routes/sentry-flush.tsx | 6 ++++++ .../tests/performance/low-quality-filter.server.test.ts | 7 +++++-- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/sentry-flush.tsx diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes.ts index 731081b54f52..1c5bb472d162 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes.ts @@ -2,6 +2,7 @@ import { type RouteConfig, index, prefix, route } from '@react-router/dev/routes export default [ index('routes/home.tsx'), + route('__sentry-flush', 'routes/sentry-flush.tsx'), ...prefix('errors', [ route('client', 'routes/errors/client.tsx'), route('client/:client-param', 'routes/errors/client-param.tsx'), diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/sentry-flush.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/sentry-flush.tsx new file mode 100644 index 000000000000..c72024185046 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/sentry-flush.tsx @@ -0,0 +1,6 @@ +import * as Sentry from '@sentry/react-router'; + +export async function loader() { + await Sentry.flush(2000); + return new Response(null, { status: 204 }); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts index cb01bbc992ad..47eda2922f75 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts @@ -1,11 +1,10 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import type { Event } from '@sentry/core'; import { APP_NAME } from '../constants'; test.describe('low-quality transaction filter', () => { test('does not send a server transaction for /__manifest? requests', async ({ page }) => { - const serverTxns: Event[] = []; + const serverTxns: Array<{ transaction?: string }> = []; const navigationPromise = waitForTransaction(APP_NAME, async transactionEvent => { return ( @@ -19,10 +18,14 @@ test.describe('low-quality transaction filter', () => { }); await page.goto('/performance'); + await page.waitForTimeout(1000); await page.getByRole('link', { name: 'SSR Page' }).click(); await navigationPromise; + // Force the server to flush any in-flight transactions before we assert + await page.evaluate(() => fetch('/__sentry-flush')); + expect(serverTxns.some(t => t.transaction?.match(/GET \/__manifest\?/))).toBe(false); }); }); From 5bafe7bff8c89ec8044ac279de051c153b0d6d46 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 27 Apr 2026 15:29:06 +0200 Subject: [PATCH 7/9] d. --- .../integration/lowQualityTransactionsFilterIntegration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts b/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts index dcdd459a5193..d213241f9c7b 100644 --- a/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts +++ b/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts @@ -9,9 +9,10 @@ const LOW_QUALITY_TRANSACTIONS_REGEXES = [ /GET \/__manifest\?/, ]; +// TODO(v11): Remove the `_options` parameter (unused and only kept for back-compat with the previous signature) const _lowQualityTransactionsFilterIntegration = ((_options?: NodeOptions) => ({ name: 'LowQualityTransactionsFilter', - beforeSetup(client: Client) { + beforeSetup(client) { const opts = client.getOptions(); opts.ignoreSpans = [...(opts.ignoreSpans || []), ...LOW_QUALITY_TRANSACTIONS_REGEXES]; }, From f8acc0c624784bafaf966cf49224efb9cc1a9941 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 28 Apr 2026 10:31:02 +0200 Subject: [PATCH 8/9] fix manifest bug --- .../low-quality-filter.server.test.ts | 7 +++++-- .../lowQualityTransactionsFilterIntegration.ts | 12 +++++++----- ...ualityTransactionsFilterIntegration.test.ts | 18 +++++++++--------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts index 47eda2922f75..0e5351a5704f 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts @@ -4,7 +4,7 @@ import { APP_NAME } from '../constants'; test.describe('low-quality transaction filter', () => { test('does not send a server transaction for /__manifest? requests', async ({ page }) => { - const serverTxns: Array<{ transaction?: string }> = []; + const serverTxns: Array<{ contexts?: { trace?: { data?: Record } } }> = []; const navigationPromise = waitForTransaction(APP_NAME, async transactionEvent => { return ( @@ -26,6 +26,9 @@ test.describe('low-quality transaction filter', () => { // Force the server to flush any in-flight transactions before we assert await page.evaluate(() => fetch('/__sentry-flush')); - expect(serverTxns.some(t => t.transaction?.match(/GET \/__manifest\?/))).toBe(false); + const targetIsManifest = (t: (typeof serverTxns)[number]) => + typeof t.contexts?.trace?.data?.['http.target'] === 'string' && + (t.contexts.trace.data['http.target'] as string).includes('/__manifest'); + expect(serverTxns.some(targetIsManifest)).toBe(false); }); }); diff --git a/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts b/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts index d213241f9c7b..b17627f4bb85 100644 --- a/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts +++ b/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts @@ -1,12 +1,14 @@ -import type { Client, IntegrationFn } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import type { NodeOptions } from '@sentry/node'; -const LOW_QUALITY_TRANSACTIONS_REGEXES = [ +const LOW_QUALITY_TRANSACTIONS_FILTERS = [ /GET \/node_modules\//, /GET \/favicon\.ico/, /GET \/@id\//, - /GET \/__manifest\?/, + // The span description for the `__manifest` endpoint is `GET *` (`http.route` resolves to `*`). + // Filter by `http.target` instead, which carries the raw request path. + { attributes: { 'http.target': /\/__manifest/ } }, ]; // TODO(v11): Remove the `_options` parameter (unused and only kept for back-compat with the previous signature) @@ -14,12 +16,12 @@ const _lowQualityTransactionsFilterIntegration = ((_options?: NodeOptions) => ({ name: 'LowQualityTransactionsFilter', beforeSetup(client) { const opts = client.getOptions(); - opts.ignoreSpans = [...(opts.ignoreSpans || []), ...LOW_QUALITY_TRANSACTIONS_REGEXES]; + opts.ignoreSpans = [...(opts.ignoreSpans || []), ...LOW_QUALITY_TRANSACTIONS_FILTERS]; }, })) satisfies IntegrationFn; /** * Integration that filters out noisy http transactions such as requests to node_modules, favicon.ico, @id/, __manifest. - * Adds regex entries to `ignoreSpans` so the filter applies in both static and streaming trace lifecycles. + * Adds entries to `ignoreSpans` so the filter applies in both static and streaming trace lifecycles. */ export const lowQualityTransactionsFilterIntegration = defineIntegration(_lowQualityTransactionsFilterIntegration); diff --git a/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts b/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts index b6901934e1ec..b64b850c9b94 100644 --- a/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts +++ b/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts @@ -16,12 +16,12 @@ function setupIntegrationAndGetIgnoreSpans(initial: Partial = {}) } describe('lowQualityTransactionsFilterIntegration', () => { - it('appends the low-quality regexes to ignoreSpans', () => { + it('appends the low-quality filters to ignoreSpans', () => { expect(setupIntegrationAndGetIgnoreSpans()).toEqual([ /GET \/node_modules\//, /GET \/favicon\.ico/, /GET \/@id\//, - /GET \/__manifest\?/, + { attributes: { 'http.target': /\/__manifest/ } }, ]); }); @@ -31,19 +31,19 @@ describe('lowQualityTransactionsFilterIntegration', () => { /GET \/node_modules\//, /GET \/favicon\.ico/, /GET \/@id\//, - /GET \/__manifest\?/, + { attributes: { 'http.target': /\/__manifest/ } }, ]); }); describe('drops low-quality transactions', () => { it.each([ - ['node_modules requests', 'GET /node_modules/some-package/index.js'], - ['favicon.ico requests', 'GET /favicon.ico'], - ['@id/ requests', 'GET /@id/some-id'], - ['manifest requests', 'GET /__manifest?p=%2Fperformance%2Fserver-action'], - ])('%s', (_label, name) => { + ['node_modules requests', { description: 'GET /node_modules/some-package/index.js' }], + ['favicon.ico requests', { description: 'GET /favicon.ico' }], + ['@id/ requests', { description: 'GET /@id/some-id' }], + ['manifest requests', { description: 'GET *', attributes: { 'http.target': '/__manifest?paths=foo' } }], + ])('%s', (_label, span) => { const ignoreSpans = setupIntegrationAndGetIgnoreSpans(); - expect(shouldIgnoreSpan({ description: name, op: 'http.server' }, ignoreSpans)).toBe(true); + expect(shouldIgnoreSpan({ op: 'http.server', ...span }, ignoreSpans)).toBe(true); }); }); From 7bacc1ebddf0b5d233635a94adb38be5498d352c Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 28 Apr 2026 11:03:02 +0200 Subject: [PATCH 9/9] bump --- .size-limit.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 611bcdbe7017..a1cec04c9d97 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -96,7 +96,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'), gzip: true, - limit: '88 KB', + limit: '89 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -310,7 +310,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.min.js'), gzip: false, brotli: false, - limit: '256 KB', + limit: '258 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -318,7 +318,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '260 KB', + limit: '261 KB', disablePlugins: ['@size-limit/esbuild'], }, {