diff --git a/packages/nextjs/src/index.server.ts b/packages/nextjs/src/index.server.ts index c7b36642b0d7..2370fa21efc6 100644 --- a/packages/nextjs/src/index.server.ts +++ b/packages/nextjs/src/index.server.ts @@ -1,5 +1,5 @@ import { RewriteFrames } from '@sentry/integrations'; -import { configureScope, init as nodeInit } from '@sentry/node'; +import { configureScope, init as nodeInit, Integrations } from '@sentry/node'; import { instrumentServer } from './utils/instrumentServer'; import { MetadataBuilder } from './utils/metadataBuilder'; @@ -37,12 +37,20 @@ const defaultRewriteFramesIntegration = new RewriteFrames({ }, }); +const defaultHttpTracingIntegration = new Integrations.Http({ tracing: true }); + function addServerIntegrations(options: NextjsOptions): void { if (options.integrations) { options.integrations = addIntegration(defaultRewriteFramesIntegration, options.integrations); } else { options.integrations = [defaultRewriteFramesIntegration]; } + + if (options.tracesSampleRate !== undefined || options.tracesSampler !== undefined) { + options.integrations = addIntegration(defaultHttpTracingIntegration, options.integrations, { + Http: { keyPath: '_tracing', value: true }, + }); + } } export { withSentryConfig } from './utils/config'; diff --git a/packages/nextjs/test/index.server.test.ts b/packages/nextjs/test/index.server.test.ts new file mode 100644 index 000000000000..c701c599508a --- /dev/null +++ b/packages/nextjs/test/index.server.test.ts @@ -0,0 +1,136 @@ +import { RewriteFrames } from '@sentry/integrations'; +import { Integrations } from '@sentry/node'; +import { Integration } from '@sentry/types'; + +import { init, Scope } from '../src/index.server'; +import { NextjsOptions } from '../src/utils/nextjsOptions'; + +const mockInit = jest.fn(); +let configureScopeCallback: (scope: Scope) => void = () => undefined; + +jest.mock('@sentry/node', () => { + const actual = jest.requireActual('@sentry/node'); + return { + ...actual, + init: (options: NextjsOptions) => { + mockInit(options); + }, + configureScope: (callback: (scope: Scope) => void) => { + configureScopeCallback = callback; + }, + }; +}); + +describe('Server init()', () => { + afterEach(() => { + mockInit.mockClear(); + configureScopeCallback = () => undefined; + }); + + it('inits the Node SDK', () => { + expect(mockInit).toHaveBeenCalledTimes(0); + init({}); + expect(mockInit).toHaveBeenCalledTimes(1); + expect(mockInit).toHaveBeenLastCalledWith({ + _metadata: { + sdk: { + name: 'sentry.javascript.nextjs', + version: expect.any(String), + packages: expect.any(Array), + }, + }, + autoSessionTracking: false, + environment: 'test', + integrations: [expect.any(RewriteFrames)], + }); + }); + + it('sets runtime on scope', () => { + const mockScope = new Scope(); + init({}); + configureScopeCallback(mockScope); + // @ts-ignore need access to protected _tags attribute + expect(mockScope._tags).toEqual({ runtime: 'node' }); + }); + + describe('integrations', () => { + it('adds RewriteFrames integration by default', () => { + init({}); + + const reactInitOptions: NextjsOptions = mockInit.mock.calls[0][0]; + expect(reactInitOptions.integrations).toHaveLength(1); + const integrations = reactInitOptions.integrations as Integration[]; + expect(integrations[0]).toEqual(expect.any(RewriteFrames)); + }); + + it('adds Http integration by default if tracesSampleRate is set', () => { + init({ tracesSampleRate: 1.0 }); + + const reactInitOptions: NextjsOptions = mockInit.mock.calls[0][0]; + expect(reactInitOptions.integrations).toHaveLength(2); + const integrations = reactInitOptions.integrations as Integration[]; + expect(integrations[1]).toEqual(expect.any(Integrations.Http)); + }); + + it('adds Http integration by default if tracesSampler is set', () => { + init({ tracesSampler: () => true }); + + const reactInitOptions: NextjsOptions = mockInit.mock.calls[0][0]; + expect(reactInitOptions.integrations).toHaveLength(2); + const integrations = reactInitOptions.integrations as Integration[]; + expect(integrations[1]).toEqual(expect.any(Integrations.Http)); + }); + + it('adds Http integration with tracing true', () => { + init({ tracesSampleRate: 1.0 }); + const reactInitOptions: NextjsOptions = mockInit.mock.calls[0][0]; + expect(reactInitOptions.integrations).toHaveLength(2); + + const integrations = reactInitOptions.integrations as Integration[]; + expect((integrations[1] as any)._tracing).toBe(true); + }); + + it('supports passing integration through options', () => { + init({ tracesSampleRate: 1.0, integrations: [new Integrations.Console()] }); + const reactInitOptions: NextjsOptions = mockInit.mock.calls[0][0]; + expect(reactInitOptions.integrations).toHaveLength(3); + + const integrations = reactInitOptions.integrations as Integration[]; + expect(integrations).toEqual([ + expect.any(Integrations.Console), + expect.any(RewriteFrames), + expect.any(Integrations.Http), + ]); + }); + + describe('custom Http integration', () => { + it('sets tracing to true if tracesSampleRate is set', () => { + init({ + tracesSampleRate: 1.0, + integrations: [new Integrations.Http({ tracing: false })], + }); + + const reactInitOptions: NextjsOptions = mockInit.mock.calls[0][0]; + expect(reactInitOptions.integrations).toHaveLength(2); + const integrations = reactInitOptions.integrations as Integration[]; + expect(integrations[0] as InstanceType).toEqual( + expect.objectContaining({ _breadcrumbs: true, _tracing: true, name: 'Http' }), + ); + }); + + it('sets tracing to true if tracesSampler is set', () => { + init({ + tracesSampler: () => true, + integrations: [new Integrations.Http({ tracing: false })], + }); + + const reactInitOptions: NextjsOptions = mockInit.mock.calls[0][0]; + expect(reactInitOptions.integrations).toHaveLength(2); + const integrations = reactInitOptions.integrations as Integration[]; + expect(integrations[0] as InstanceType).toEqual( + expect.objectContaining({ _breadcrumbs: true, _tracing: true, name: 'Http' }), + ); + }); + }); + }); +});