Skip to content

Commit 9ca4c5b

Browse files
authored
ref(nextjs): Stop reinitializing the server SDK unnecessarily (#3860)
In the nextjs SDK, we bundle the user's `Sentry.init()` code with every API route and with the `_app` page, which is the basis of all user-provided pages. This is necessary because, when deployed on Vercel, each API route (and optionally each individual page) is turned into its own serverless function, so each one has to independently be able to start up the SDK. That said, a) not all nextjs apps are deployed to Vercel, and b) even those that are sometimes have multiple serverless API routes combined into a single serverless function. As a result, `Sentry.init()` can end up getting called over and over again, as different routes are hit on the same running server process. While we manage hubs and clients and scopes in such a way that this doesn't break anything, it's also a total waste of time - the startup code that we bundle with each route comes from a single source (`sentry.server.config.js`) and therefore is the same for every route; once the SDK is started with the given options, it's started. No reason to do it multiple times. This thus introduces a check when calling the server-side `Sentry.init()`: if there is a client already defined on the current hub, it means the startup process has already run, and so it bails. There are also changes to the tests. TL;DR, because I needed the real `init()` from the node SDK to run (in order to create an already-initialized client to check), I switched the mocking of `@sentry/node` from a direct mock to a pair of spies. I further did two renamings - `s/mockInit/nodeInit`, to make it clear which `init()` function we're talking about, and `s/reactInitOptions/nodeInitOptions`, since this is the backend initialization we're testing rather than the front end.
1 parent efa6c45 commit 9ca4c5b

File tree

2 files changed

+74
-51
lines changed

2 files changed

+74
-51
lines changed

packages/nextjs/src/index.server.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { RewriteFrames } from '@sentry/integrations';
2-
import { configureScope, init as nodeInit, Integrations } from '@sentry/node';
2+
import { configureScope, getCurrentHub, init as nodeInit, Integrations } from '@sentry/node';
3+
import { logger } from '@sentry/utils';
34

45
import { instrumentServer } from './utils/instrumentServer';
56
import { MetadataBuilder } from './utils/metadataBuilder';
@@ -14,6 +15,17 @@ export { ErrorBoundary, withErrorBoundary } from '@sentry/react';
1415

1516
/** Inits the Sentry NextJS SDK on node. */
1617
export function init(options: NextjsOptions): void {
18+
if (options.debug) {
19+
logger.enable();
20+
}
21+
22+
logger.log('Initializing SDK...');
23+
24+
if (sdkAlreadyInitialized()) {
25+
logger.log('SDK already initialized');
26+
return;
27+
}
28+
1729
const metadataBuilder = new MetadataBuilder(options, ['nextjs', 'node']);
1830
metadataBuilder.addSdkMetadata();
1931
options.environment = options.environment || process.env.NODE_ENV;
@@ -26,6 +38,13 @@ export function init(options: NextjsOptions): void {
2638
configureScope(scope => {
2739
scope.setTag('runtime', 'node');
2840
});
41+
42+
logger.log('SDK successfully initialized');
43+
}
44+
45+
function sdkAlreadyInitialized(): boolean {
46+
const hub = getCurrentHub();
47+
return !!hub.getClient();
2948
}
3049

3150
const SOURCEMAP_FILENAME_REGEX = /^.*\/\.next\//;

packages/nextjs/test/index.server.test.ts

Lines changed: 54 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,52 @@
11
import { RewriteFrames } from '@sentry/integrations';
2-
import { Integrations } from '@sentry/node';
2+
import * as SentryNode from '@sentry/node';
33
import { Integration } from '@sentry/types';
4+
import { getGlobalObject } from '@sentry/utils';
45

56
import { init, Scope } from '../src/index.server';
67
import { NextjsOptions } from '../src/utils/nextjsOptions';
78

8-
const mockInit = jest.fn();
9-
let configureScopeCallback: (scope: Scope) => void = () => undefined;
9+
const { Integrations } = SentryNode;
1010

11-
jest.mock('@sentry/node', () => {
12-
const actual = jest.requireActual('@sentry/node');
13-
return {
14-
...actual,
15-
init: (options: NextjsOptions) => {
16-
mockInit(options);
17-
},
18-
configureScope: (callback: (scope: Scope) => void) => {
19-
configureScopeCallback = callback;
20-
},
21-
};
22-
});
11+
const global = getGlobalObject();
12+
13+
let configureScopeCallback: (scope: Scope) => void = () => undefined;
14+
jest.spyOn(SentryNode, 'configureScope').mockImplementation(callback => (configureScopeCallback = callback));
15+
const nodeInit = jest.spyOn(SentryNode, 'init');
2316

2417
describe('Server init()', () => {
2518
afterEach(() => {
26-
mockInit.mockClear();
19+
nodeInit.mockClear();
2720
configureScopeCallback = () => undefined;
21+
global.__SENTRY__.hub = undefined;
2822
});
2923

3024
it('inits the Node SDK', () => {
31-
expect(mockInit).toHaveBeenCalledTimes(0);
25+
expect(nodeInit).toHaveBeenCalledTimes(0);
3226
init({});
33-
expect(mockInit).toHaveBeenCalledTimes(1);
34-
expect(mockInit).toHaveBeenLastCalledWith({
35-
_metadata: {
36-
sdk: {
37-
name: 'sentry.javascript.nextjs',
38-
version: expect.any(String),
39-
packages: expect.any(Array),
27+
expect(nodeInit).toHaveBeenCalledTimes(1);
28+
expect(nodeInit).toHaveBeenLastCalledWith(
29+
expect.objectContaining({
30+
_metadata: {
31+
sdk: {
32+
name: 'sentry.javascript.nextjs',
33+
version: expect.any(String),
34+
packages: expect.any(Array),
35+
},
4036
},
41-
},
42-
autoSessionTracking: false,
43-
environment: 'test',
44-
integrations: [expect.any(RewriteFrames)],
45-
});
37+
autoSessionTracking: false,
38+
environment: 'test',
39+
integrations: [expect.any(RewriteFrames)],
40+
}),
41+
);
42+
});
43+
44+
it("doesn't reinitialize the node SDK if already initialized", () => {
45+
expect(nodeInit).toHaveBeenCalledTimes(0);
46+
init({});
47+
expect(nodeInit).toHaveBeenCalledTimes(1);
48+
init({});
49+
expect(nodeInit).toHaveBeenCalledTimes(1);
4650
});
4751

4852
it('sets runtime on scope', () => {
@@ -57,45 +61,45 @@ describe('Server init()', () => {
5761
it('adds RewriteFrames integration by default', () => {
5862
init({});
5963

60-
const reactInitOptions: NextjsOptions = mockInit.mock.calls[0][0];
61-
expect(reactInitOptions.integrations).toHaveLength(1);
62-
const integrations = reactInitOptions.integrations as Integration[];
64+
const nodeInitOptions: NextjsOptions = nodeInit.mock.calls[0][0]!;
65+
expect(nodeInitOptions.integrations).toHaveLength(1);
66+
const integrations = nodeInitOptions.integrations as Integration[];
6367
expect(integrations[0]).toEqual(expect.any(RewriteFrames));
6468
});
6569

6670
it('adds Http integration by default if tracesSampleRate is set', () => {
6771
init({ tracesSampleRate: 1.0 });
6872

69-
const reactInitOptions: NextjsOptions = mockInit.mock.calls[0][0];
70-
expect(reactInitOptions.integrations).toHaveLength(2);
71-
const integrations = reactInitOptions.integrations as Integration[];
73+
const nodeInitOptions: NextjsOptions = nodeInit.mock.calls[0][0]!;
74+
expect(nodeInitOptions.integrations).toHaveLength(2);
75+
const integrations = nodeInitOptions.integrations as Integration[];
7276
expect(integrations[1]).toEqual(expect.any(Integrations.Http));
7377
});
7478

7579
it('adds Http integration by default if tracesSampler is set', () => {
7680
init({ tracesSampler: () => true });
7781

78-
const reactInitOptions: NextjsOptions = mockInit.mock.calls[0][0];
79-
expect(reactInitOptions.integrations).toHaveLength(2);
80-
const integrations = reactInitOptions.integrations as Integration[];
82+
const nodeInitOptions: NextjsOptions = nodeInit.mock.calls[0][0]!;
83+
expect(nodeInitOptions.integrations).toHaveLength(2);
84+
const integrations = nodeInitOptions.integrations as Integration[];
8185
expect(integrations[1]).toEqual(expect.any(Integrations.Http));
8286
});
8387

8488
it('adds Http integration with tracing true', () => {
8589
init({ tracesSampleRate: 1.0 });
86-
const reactInitOptions: NextjsOptions = mockInit.mock.calls[0][0];
87-
expect(reactInitOptions.integrations).toHaveLength(2);
90+
const nodeInitOptions: NextjsOptions = nodeInit.mock.calls[0][0]!;
91+
expect(nodeInitOptions.integrations).toHaveLength(2);
8892

89-
const integrations = reactInitOptions.integrations as Integration[];
93+
const integrations = nodeInitOptions.integrations as Integration[];
9094
expect((integrations[1] as any)._tracing).toBe(true);
9195
});
9296

9397
it('supports passing integration through options', () => {
9498
init({ tracesSampleRate: 1.0, integrations: [new Integrations.Console()] });
95-
const reactInitOptions: NextjsOptions = mockInit.mock.calls[0][0];
96-
expect(reactInitOptions.integrations).toHaveLength(3);
99+
const nodeInitOptions: NextjsOptions = nodeInit.mock.calls[0][0]!;
100+
expect(nodeInitOptions.integrations).toHaveLength(3);
97101

98-
const integrations = reactInitOptions.integrations as Integration[];
102+
const integrations = nodeInitOptions.integrations as Integration[];
99103
expect(integrations).toEqual([
100104
expect.any(Integrations.Console),
101105
expect.any(RewriteFrames),
@@ -110,9 +114,9 @@ describe('Server init()', () => {
110114
integrations: [new Integrations.Http({ tracing: false })],
111115
});
112116

113-
const reactInitOptions: NextjsOptions = mockInit.mock.calls[0][0];
114-
expect(reactInitOptions.integrations).toHaveLength(2);
115-
const integrations = reactInitOptions.integrations as Integration[];
117+
const nodeInitOptions: NextjsOptions = nodeInit.mock.calls[0][0]!;
118+
expect(nodeInitOptions.integrations).toHaveLength(2);
119+
const integrations = nodeInitOptions.integrations as Integration[];
116120
expect(integrations[0] as InstanceType<typeof Integrations.Http>).toEqual(
117121
expect.objectContaining({ _breadcrumbs: true, _tracing: true, name: 'Http' }),
118122
);
@@ -124,9 +128,9 @@ describe('Server init()', () => {
124128
integrations: [new Integrations.Http({ tracing: false })],
125129
});
126130

127-
const reactInitOptions: NextjsOptions = mockInit.mock.calls[0][0];
128-
expect(reactInitOptions.integrations).toHaveLength(2);
129-
const integrations = reactInitOptions.integrations as Integration[];
131+
const nodeInitOptions: NextjsOptions = nodeInit.mock.calls[0][0]!;
132+
expect(nodeInitOptions.integrations).toHaveLength(2);
133+
const integrations = nodeInitOptions.integrations as Integration[];
130134
expect(integrations[0] as InstanceType<typeof Integrations.Http>).toEqual(
131135
expect.objectContaining({ _breadcrumbs: true, _tracing: true, name: 'Http' }),
132136
);

0 commit comments

Comments
 (0)