Skip to content

Commit 7c15a97

Browse files
authored
fix(nextjs): Always initialize SDK with global hub (#4086)
In a nextjs app using `@sentry/nextjs`, the code which initializes the SDK is kicked off by the first incoming request[1]. Because we use domains to prevent scope bleed between requests, at the time when `Sentry.init()` is called, we're already in a domain, and as a result, @sentry/node's `init` function sets the domain's hub as the global hub, the hub from which all subsequent hubs will inherit. This means that - currently - all future hubs inherit data from that first request, which they shouldn't. This PR fixes that by essentially deactivating the domain while the SDK is initialized, so that all initialization code will act on the global hub. Then, because ideally the domain hub would have inherited from the global hub, the work done to the global hub is copied over to the domain hub (to mimic the effects of the inheritance) before the domain is made active again. [1] It's the request handler's loading of either `_app` or the API route handler (both of which we've prefaced with the user's `sentry.server.config.js`) which triggers the user's `Sentry.init` to run.
1 parent 9c25928 commit 7c15a97

File tree

3 files changed

+62
-7
lines changed

3 files changed

+62
-7
lines changed

packages/nextjs/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"dependencies": {
2020
"@sentry/core": "6.13.3",
21+
"@sentry/hub": "6.13.3",
2122
"@sentry/integrations": "6.13.3",
2223
"@sentry/node": "6.13.3",
2324
"@sentry/react": "6.13.3",

packages/nextjs/src/index.server.ts

+26
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { Carrier, getHubFromCarrier, getMainCarrier } from '@sentry/hub';
12
import { RewriteFrames } from '@sentry/integrations';
23
import { configureScope, getCurrentHub, init as nodeInit, Integrations } from '@sentry/node';
34
import { escapeStringForRegex, logger } from '@sentry/utils';
5+
import * as domainModule from 'domain';
46
import * as path from 'path';
57

68
import { instrumentServer } from './utils/instrumentServer';
@@ -15,6 +17,7 @@ export * from '@sentry/node';
1517
export { ErrorBoundary, withErrorBoundary } from '@sentry/react';
1618

1719
type GlobalWithDistDir = typeof global & { __rewriteFramesDistDir__: string };
20+
const domain = domainModule as typeof domainModule & { active: (domainModule.Domain & Carrier) | null };
1821

1922
/** Inits the Sentry NextJS SDK on node. */
2023
export function init(options: NextjsOptions): void {
@@ -36,11 +39,34 @@ export function init(options: NextjsOptions): void {
3639
// Right now we only capture frontend sessions for Next.js
3740
options.autoSessionTracking = false;
3841

42+
// In an ideal world, this init function would be called before any requests are handled. That way, every domain we
43+
// use to wrap a request would inherit its scope and client from the global hub. In practice, however, handling the
44+
// first request is what causes us to initialize the SDK, as the init code is injected into `_app` and all API route
45+
// handlers, and those are only accessed in the course of handling a request. As a result, we're already in a domain
46+
// when `init` is called. In order to compensate for this and mimic the ideal world scenario, we stash the active
47+
// domain, run `init` as normal, and then restore the domain afterwards, copying over data from the main hub as if we
48+
// really were inheriting.
49+
const activeDomain = domain.active;
50+
domain.active = null;
51+
3952
nodeInit(options);
53+
4054
configureScope(scope => {
4155
scope.setTag('runtime', 'node');
4256
});
4357

58+
if (activeDomain) {
59+
const globalHub = getHubFromCarrier(getMainCarrier());
60+
const domainHub = getHubFromCarrier(activeDomain);
61+
62+
// apply the changes made by `nodeInit` to the domain's hub also
63+
domainHub.bindClient(globalHub.getClient());
64+
domainHub.getScope()?.update(globalHub.getScope());
65+
66+
// restore the domain hub as the current one
67+
domain.active = activeDomain;
68+
}
69+
4470
logger.log('SDK successfully initialized');
4571
}
4672

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

+35-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { RewriteFrames } from '@sentry/integrations';
22
import * as SentryNode from '@sentry/node';
3+
import { getCurrentHub, NodeClient } from '@sentry/node';
34
import { Integration } from '@sentry/types';
45
import { getGlobalObject } from '@sentry/utils';
6+
import * as domain from 'domain';
57

6-
import { init, Scope } from '../src/index.server';
8+
import { init } from '../src/index.server';
79
import { NextjsOptions } from '../src/utils/nextjsOptions';
810

911
const { Integrations } = SentryNode;
@@ -13,14 +15,11 @@ const global = getGlobalObject();
1315
// normally this is set as part of the build process, so mock it here
1416
(global as typeof global & { __rewriteFramesDistDir__: string }).__rewriteFramesDistDir__ = '.next';
1517

16-
let configureScopeCallback: (scope: Scope) => void = () => undefined;
17-
jest.spyOn(SentryNode, 'configureScope').mockImplementation(callback => (configureScopeCallback = callback));
1818
const nodeInit = jest.spyOn(SentryNode, 'init');
1919

2020
describe('Server init()', () => {
2121
afterEach(() => {
2222
nodeInit.mockClear();
23-
configureScopeCallback = () => undefined;
2423
global.__SENTRY__.hub = undefined;
2524
});
2625

@@ -53,11 +52,40 @@ describe('Server init()', () => {
5352
});
5453

5554
it('sets runtime on scope', () => {
56-
const mockScope = new Scope();
55+
const currentScope = getCurrentHub().getScope();
56+
57+
// @ts-ignore need access to protected _tags attribute
58+
expect(currentScope._tags).toEqual({});
59+
5760
init({});
58-
configureScopeCallback(mockScope);
61+
5962
// @ts-ignore need access to protected _tags attribute
60-
expect(mockScope._tags).toEqual({ runtime: 'node' });
63+
expect(currentScope._tags).toEqual({ runtime: 'node' });
64+
});
65+
66+
it("initializes both global hub and domain hub when there's an active domain", () => {
67+
const globalHub = getCurrentHub();
68+
const local = domain.create();
69+
local.run(() => {
70+
const domainHub = getCurrentHub();
71+
72+
// they are in fact two different hubs, and neither one yet has a client
73+
expect(domainHub).not.toBe(globalHub);
74+
expect(globalHub.getClient()).toBeUndefined();
75+
expect(domainHub.getClient()).toBeUndefined();
76+
77+
// this tag should end up only in the domain hub
78+
domainHub.setTag('dogs', 'areGreat');
79+
80+
init({});
81+
82+
expect(globalHub.getClient()).toEqual(expect.any(NodeClient));
83+
expect(domainHub.getClient()).toBe(globalHub.getClient());
84+
// @ts-ignore need access to protected _tags attribute
85+
expect(globalHub.getScope()._tags).toEqual({ runtime: 'node' });
86+
// @ts-ignore need access to protected _tags attribute
87+
expect(domainHub.getScope()._tags).toEqual({ runtime: 'node', dogs: 'areGreat' });
88+
});
6189
});
6290

6391
describe('integrations', () => {

0 commit comments

Comments
 (0)