diff --git a/packages/nextjs/src/index.client.ts b/packages/nextjs/src/index.client.ts index e23464cf07d1..970cadb00a06 100644 --- a/packages/nextjs/src/index.client.ts +++ b/packages/nextjs/src/index.client.ts @@ -30,6 +30,7 @@ export function init(options: NextjsOptions): void { }); configureScope(scope => { scope.setTag('runtime', 'browser'); + scope.addEventProcessor(event => (event.type === 'transaction' && event.transaction === '/404' ? null : event)); }); } diff --git a/packages/nextjs/src/index.server.ts b/packages/nextjs/src/index.server.ts index 0c7e19690877..6fd4e1fce468 100644 --- a/packages/nextjs/src/index.server.ts +++ b/packages/nextjs/src/index.server.ts @@ -1,6 +1,7 @@ import { Carrier, getHubFromCarrier, getMainCarrier } from '@sentry/hub'; import { RewriteFrames } from '@sentry/integrations'; import { configureScope, getCurrentHub, init as nodeInit, Integrations } from '@sentry/node'; +import { Event } from '@sentry/types'; import { escapeStringForRegex, logger } from '@sentry/utils'; import * as domainModule from 'domain'; import * as path from 'path'; @@ -56,6 +57,8 @@ export function init(options: NextjsOptions): void { if (process.env.VERCEL) { scope.setTag('vercel', true); } + + scope.addEventProcessor(filterTransactions); }); if (activeDomain) { @@ -65,6 +68,8 @@ export function init(options: NextjsOptions): void { // apply the changes made by `nodeInit` to the domain's hub also domainHub.bindClient(globalHub.getClient()); domainHub.getScope()?.update(globalHub.getScope()); + // `scope.update()` doesn’t copy over event processors, so we have to add it manually + domainHub.getScope()?.addEventProcessor(filterTransactions); // restore the domain hub as the current one domain.active = activeDomain; @@ -107,6 +112,10 @@ function addServerIntegrations(options: NextjsOptions): void { } } +function filterTransactions(event: Event): Event | null { + return event.type === 'transaction' && event.transaction === '/404' ? null : event; +} + export { withSentryConfig } from './config'; export { withSentry } from './utils/withSentry'; diff --git a/packages/nextjs/test/index.client.test.ts b/packages/nextjs/test/index.client.test.ts index 0f906df79b2d..19171bc4346c 100644 --- a/packages/nextjs/test/index.client.test.ts +++ b/packages/nextjs/test/index.client.test.ts @@ -1,8 +1,9 @@ +import { BaseClient } from '@sentry/core'; import { getCurrentHub } from '@sentry/hub'; import * as SentryReact from '@sentry/react'; import { Integrations as TracingIntegrations } from '@sentry/tracing'; import { Integration } from '@sentry/types'; -import { getGlobalObject } from '@sentry/utils'; +import { getGlobalObject, logger, SentryError } from '@sentry/utils'; import { init, Integrations, nextRouterInstrumentation } from '../src/index.client'; import { NextjsOptions } from '../src/utils/nextjsOptions'; @@ -12,10 +13,12 @@ const { BrowserTracing } = TracingIntegrations; const global = getGlobalObject(); const reactInit = jest.spyOn(SentryReact, 'init'); +const captureEvent = jest.spyOn(BaseClient.prototype, 'captureEvent'); +const logError = jest.spyOn(logger, 'error'); describe('Client init()', () => { afterEach(() => { - reactInit.mockClear(); + jest.clearAllMocks(); global.__SENTRY__.hub = undefined; }); @@ -50,6 +53,22 @@ describe('Client init()', () => { expect(currentScope._tags).toEqual({ runtime: 'browser' }); }); + it('adds 404 transaction filter', () => { + init({ + dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', + tracesSampleRate: 1.0, + }); + const hub = getCurrentHub(); + const sendEvent = jest.spyOn(hub.getClient()!.getTransport!(), 'sendEvent'); + + const transaction = hub.startTransaction({ name: '/404' }); + transaction.finish(); + + expect(sendEvent).not.toHaveBeenCalled(); + expect(captureEvent.mock.results[0].value).toBeUndefined(); + expect(logError).toHaveBeenCalledWith(new SentryError('An event processor returned null, will not send event.')); + }); + describe('integrations', () => { it('does not add BrowserTracing integration by default if tracesSampleRate is not set', () => { init({}); diff --git a/packages/nextjs/test/index.server.test.ts b/packages/nextjs/test/index.server.test.ts index 337e70345c52..d3fc53a194ed 100644 --- a/packages/nextjs/test/index.server.test.ts +++ b/packages/nextjs/test/index.server.test.ts @@ -1,8 +1,9 @@ +import { BaseClient } from '@sentry/core'; import { RewriteFrames } from '@sentry/integrations'; import * as SentryNode from '@sentry/node'; import { getCurrentHub, NodeClient } from '@sentry/node'; import { Integration } from '@sentry/types'; -import { getGlobalObject } from '@sentry/utils'; +import { getGlobalObject, logger, SentryError } from '@sentry/utils'; import * as domain from 'domain'; import { init } from '../src/index.server'; @@ -16,10 +17,12 @@ const global = getGlobalObject(); (global as typeof global & { __rewriteFramesDistDir__: string }).__rewriteFramesDistDir__ = '.next'; const nodeInit = jest.spyOn(SentryNode, 'init'); +const captureEvent = jest.spyOn(BaseClient.prototype, 'captureEvent'); +const logError = jest.spyOn(logger, 'error'); describe('Server init()', () => { afterEach(() => { - nodeInit.mockClear(); + jest.clearAllMocks(); global.__SENTRY__.hub = undefined; }); @@ -87,6 +90,22 @@ describe('Server init()', () => { expect(currentScope._tags.vercel).toBeUndefined(); }); + it('adds 404 transaction filter', () => { + init({ + dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', + tracesSampleRate: 1.0, + }); + const hub = getCurrentHub(); + const sendEvent = jest.spyOn(hub.getClient()!.getTransport!(), 'sendEvent'); + + const transaction = hub.startTransaction({ name: '/404' }); + transaction.finish(); + + expect(sendEvent).not.toHaveBeenCalled(); + expect(captureEvent.mock.results[0].value).toBeUndefined(); + expect(logError).toHaveBeenCalledWith(new SentryError('An event processor returned null, will not send event.')); + }); + it("initializes both global hub and domain hub when there's an active domain", () => { const globalHub = getCurrentHub(); const local = domain.create();