From 72092281e9d66edcd062f58a1cd46719885a2381 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 25 Oct 2021 19:30:11 -0700 Subject: [PATCH 1/3] stash domain during init --- packages/nextjs/src/index.server.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/nextjs/src/index.server.ts b/packages/nextjs/src/index.server.ts index 1578085c25a6..2ed6ce1ec0fc 100644 --- a/packages/nextjs/src/index.server.ts +++ b/packages/nextjs/src/index.server.ts @@ -1,6 +1,8 @@ +import { Carrier, getHubFromCarrier, getMainCarrier } from '@sentry/hub'; import { RewriteFrames } from '@sentry/integrations'; import { configureScope, getCurrentHub, init as nodeInit, Integrations } from '@sentry/node'; import { escapeStringForRegex, logger } from '@sentry/utils'; +import * as domainModule from 'domain'; import * as path from 'path'; import { instrumentServer } from './utils/instrumentServer'; @@ -15,6 +17,7 @@ export * from '@sentry/node'; export { ErrorBoundary, withErrorBoundary } from '@sentry/react'; type GlobalWithDistDir = typeof global & { __rewriteFramesDistDir__: string }; +const domain = domainModule as typeof domainModule & { active: (domainModule.Domain & Carrier) | null }; /** Inits the Sentry NextJS SDK on node. */ export function init(options: NextjsOptions): void { @@ -36,11 +39,34 @@ export function init(options: NextjsOptions): void { // Right now we only capture frontend sessions for Next.js options.autoSessionTracking = false; + // In an ideal world, this init function would be called before any requests are handled. That way, every domain we + // use to wrap a request would inherit its scope and client from the global hub. In practice, however, handling the + // first request is what causes us to initialize the SDK, as the init code is injected into `_app` and all API route + // handlers, and those are only accessed in the course of handling a request. As a result, we're already in a domain + // when `init` is called. In order to compensate for this and mimic the ideal world scenario, we stash the active + // domain, run `init` as normal, and then restore the domain afterwards, copying over data from the main hub as if we + // really were inheriting. + const activeDomain = domain.active; + domain.active = null; + nodeInit(options); + configureScope(scope => { scope.setTag('runtime', 'node'); }); + if (activeDomain) { + const globalHub = getHubFromCarrier(getMainCarrier()); + const domainHub = getHubFromCarrier(activeDomain); + + // apply the changes made by `nodeInit` to the domain's hub also + domainHub.bindClient(globalHub.getClient()); + domainHub.getScope()?.update(globalHub.getScope()); + + // restore the domain hub as the current one + domain.active = activeDomain; + } + logger.log('SDK successfully initialized'); } From 61f9078236af35a31efa75f127ebd60728edba65 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 25 Oct 2021 19:30:26 -0700 Subject: [PATCH 2/3] add @sentry/hub as dependency --- packages/nextjs/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index f117df981e2c..bb431d41728a 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@sentry/core": "6.13.3", + "@sentry/hub": "6.13.3", "@sentry/integrations": "6.13.3", "@sentry/node": "6.13.3", "@sentry/react": "6.13.3", From 520bbfecd3e305972d6b3877ab841ffd51bc3c4c Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 25 Oct 2021 23:17:34 -0700 Subject: [PATCH 3/3] add test --- packages/nextjs/test/index.server.test.ts | 42 +++++++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/nextjs/test/index.server.test.ts b/packages/nextjs/test/index.server.test.ts index 972e4ed57336..43b597b4c435 100644 --- a/packages/nextjs/test/index.server.test.ts +++ b/packages/nextjs/test/index.server.test.ts @@ -1,9 +1,11 @@ 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 * as domain from 'domain'; -import { init, Scope } from '../src/index.server'; +import { init } from '../src/index.server'; import { NextjsOptions } from '../src/utils/nextjsOptions'; const { Integrations } = SentryNode; @@ -13,14 +15,11 @@ const global = getGlobalObject(); // normally this is set as part of the build process, so mock it here (global as typeof global & { __rewriteFramesDistDir__: string }).__rewriteFramesDistDir__ = '.next'; -let configureScopeCallback: (scope: Scope) => void = () => undefined; -jest.spyOn(SentryNode, 'configureScope').mockImplementation(callback => (configureScopeCallback = callback)); const nodeInit = jest.spyOn(SentryNode, 'init'); describe('Server init()', () => { afterEach(() => { nodeInit.mockClear(); - configureScopeCallback = () => undefined; global.__SENTRY__.hub = undefined; }); @@ -53,11 +52,40 @@ describe('Server init()', () => { }); it('sets runtime on scope', () => { - const mockScope = new Scope(); + const currentScope = getCurrentHub().getScope(); + + // @ts-ignore need access to protected _tags attribute + expect(currentScope._tags).toEqual({}); + init({}); - configureScopeCallback(mockScope); + // @ts-ignore need access to protected _tags attribute - expect(mockScope._tags).toEqual({ runtime: 'node' }); + expect(currentScope._tags).toEqual({ runtime: 'node' }); + }); + + it("initializes both global hub and domain hub when there's an active domain", () => { + const globalHub = getCurrentHub(); + const local = domain.create(); + local.run(() => { + const domainHub = getCurrentHub(); + + // they are in fact two different hubs, and neither one yet has a client + expect(domainHub).not.toBe(globalHub); + expect(globalHub.getClient()).toBeUndefined(); + expect(domainHub.getClient()).toBeUndefined(); + + // this tag should end up only in the domain hub + domainHub.setTag('dogs', 'areGreat'); + + init({}); + + expect(globalHub.getClient()).toEqual(expect.any(NodeClient)); + expect(domainHub.getClient()).toBe(globalHub.getClient()); + // @ts-ignore need access to protected _tags attribute + expect(globalHub.getScope()._tags).toEqual({ runtime: 'node' }); + // @ts-ignore need access to protected _tags attribute + expect(domainHub.getScope()._tags).toEqual({ runtime: 'node', dogs: 'areGreat' }); + }); }); describe('integrations', () => {