From 348c8cbc48314ce242e25b514044821ec723e059 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 17 Apr 2026 13:03:43 +0200 Subject: [PATCH 1/2] feat(nextjs): Filtering unwanted segments in span streaming --- packages/nextjs/src/client/index.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index fb8b7acea878..b7f9c482b816 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -74,19 +74,17 @@ export function init(options: BrowserOptions): Client | undefined { applyTunnelRouteOption(opts); applySdkMetadata(opts, 'nextjs', ['nextjs', 'react']); - const client = reactInit(opts); - - const filterTransactions: EventProcessor = event => - event.type === 'transaction' && event.transaction === '/404' ? null : event; - filterTransactions.id = 'NextClient404Filter'; - addEventProcessor(filterTransactions); + opts.ignoreSpans = [ + ...(opts.ignoreSpans || []), + // we filter out segment spans for /404 pages + /^\/404$/, + // segment spans where we didn't get a reasonable transaction name + // in this case, constructing a dynamic RegExp is fine because the variable is a constant + // we need to ensure to exact-match, so a string match isn't safe (same for /404 above) + new RegExp(`^${INCOMPLETE_APP_ROUTER_INSTRUMENTATION_TRANSACTION_NAME}$`), + ]; - const filterIncompleteNavigationTransactions: EventProcessor = event => - event.type === 'transaction' && event.transaction === INCOMPLETE_APP_ROUTER_INSTRUMENTATION_TRANSACTION_NAME - ? null - : event; - filterIncompleteNavigationTransactions.id = 'IncompleteTransactionFilter'; - addEventProcessor(filterIncompleteNavigationTransactions); + const client = reactInit(opts); const filterNextRedirectError: EventProcessor = (event, hint) => isRedirectNavigationError(hint?.originalException) || event.exception?.values?.[0]?.value === 'NEXT_REDIRECT' From 6b5bb8b3ba05137b3cdbb7f8929ce0bbc803fa8f Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 24 Apr 2026 11:31:10 +0200 Subject: [PATCH 2/2] add tests --- packages/nextjs/test/clientSdk.test.ts | 71 ++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index 873aa5a2511e..090ef61fe5cd 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -1,10 +1,11 @@ import type { Integration } from '@sentry/core'; -import { debug, getGlobalScope, getIsolationScope } from '@sentry/core'; +import { debug, getGlobalScope, getIsolationScope, SentryNonRecordingSpan } from '@sentry/core'; import * as SentryReact from '@sentry/react'; import { getClient, getCurrentScope, WINDOW } from '@sentry/react'; import { JSDOM } from 'jsdom'; import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'; import { breadcrumbsIntegration, browserTracingIntegration, init } from '../src/client'; +import { INCOMPLETE_APP_ROUTER_INSTRUMENTATION_TRANSACTION_NAME } from '../src/client/routing/appRouterRoutingInstrumentation'; const reactInit = vi.spyOn(SentryReact, 'init'); const debugLogSpy = vi.spyOn(debug, 'log'); @@ -83,20 +84,68 @@ describe('Client init()', () => { ); }); - it('adds 404 transaction filter', () => { - init({ - dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', - tracesSampleRate: 1.0, + describe('transaction filtering', () => { + const TEST_DSN_404 = 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012'; + + it('drops /404 transactions', () => { + init({ dsn: TEST_DSN_404, tracesSampleRate: 1.0 }); + const transportSend = vi.spyOn(getClient()!.getTransport()!, 'send'); + + // Ensure we have no current span, so our next span is a transaction + SentryReact.withActiveSpan(null, () => { + SentryReact.startInactiveSpan({ name: '/404' })?.end(); + }); + + expect(transportSend).not.toHaveBeenCalled(); + expect(debugLogSpy).toHaveBeenCalledWith(expect.stringContaining('matches `ignoreSpans`')); }); - const transportSend = vi.spyOn(getClient()!.getTransport()!, 'send'); - // Ensure we have no current span, so our next span is a transaction - SentryReact.withActiveSpan(null, () => { - SentryReact.startInactiveSpan({ name: '/404' })?.end(); + it('drops incomplete navigation transactions', () => { + init({ dsn: TEST_DSN_404, tracesSampleRate: 1.0 }); + const transportSend = vi.spyOn(getClient()!.getTransport()!, 'send'); + + // Ensure we have no current span, so our next span is a transaction + SentryReact.withActiveSpan(null, () => { + SentryReact.startInactiveSpan({ name: INCOMPLETE_APP_ROUTER_INSTRUMENTATION_TRANSACTION_NAME })?.end(); + }); + + expect(transportSend).not.toHaveBeenCalled(); + expect(debugLogSpy).toHaveBeenCalledWith(expect.stringContaining('matches `ignoreSpans`')); }); - expect(transportSend).not.toHaveBeenCalled(); - expect(debugLogSpy).toHaveBeenCalledWith('An event processor returned `null`, will not send event.'); + describe('span streaming', () => { + it('drops /404 segment spans', () => { + init({ dsn: TEST_DSN_404, tracesSampleRate: 1.0, traceLifecycle: 'stream' }); + + // Ensure we have no current span, so our next span is a segment span + const span = SentryReact.withActiveSpan(null, () => SentryReact.startInactiveSpan({ name: '/404' })); + + expect(span).toBeInstanceOf(SentryNonRecordingSpan); + expect(debugLogSpy).toHaveBeenCalledWith(expect.stringContaining('matches `ignoreSpans`')); + }); + + it('drops incomplete navigation segment spans', () => { + init({ dsn: TEST_DSN_404, tracesSampleRate: 1.0, traceLifecycle: 'stream' }); + + // Ensure we have no current span, so our next span is a segment span + const span = SentryReact.withActiveSpan(null, () => + SentryReact.startInactiveSpan({ name: INCOMPLETE_APP_ROUTER_INSTRUMENTATION_TRANSACTION_NAME }), + ); + + expect(span).toBeInstanceOf(SentryNonRecordingSpan); + expect(debugLogSpy).toHaveBeenCalledWith(expect.stringContaining('matches `ignoreSpans`')); + }); + + it('drops /404 non-segment spans', () => { + init({ dsn: TEST_DSN_404, tracesSampleRate: 1.0, traceLifecycle: 'stream' }); + + SentryReact.startSpan({ name: 'parent' }, parent => { + expect(parent).not.toBeInstanceOf(SentryNonRecordingSpan); + const child = SentryReact.startInactiveSpan({ name: '/404' }); + expect(child).toBeInstanceOf(SentryNonRecordingSpan); + }); + }); + }); }); describe('integrations', () => {