Skip to content

Commit 27db660

Browse files
authored
fix(node): Fix domain scope inheritance (#7799)
1 parent 1ad518f commit 27db660

File tree

4 files changed

+75
-39
lines changed

4 files changed

+75
-39
lines changed

packages/core/src/hub.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -568,10 +568,10 @@ function getGlobalHub(registry: Carrier = getMainCarrier()): Hub {
568568
*
569569
* If the carrier does not contain a hub, a new hub is created with the global hub client and scope.
570570
*/
571-
export function ensureHubOnCarrier(carrier: Carrier): void {
571+
export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub()): void {
572572
// If there's no hub on current domain, or it's an old API, assign a new one
573573
if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) {
574-
const globalHubTopStack = getGlobalHub().getStackTop();
574+
const globalHubTopStack = parent.getStackTop();
575575
setHubOnCarrier(carrier, new Hub(globalHubTopStack.client, Scope.clone(globalHubTopStack.scope)));
576576
}
577577
}

packages/node/src/async/domain.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import type { Carrier, Hub, RunWithAsyncContextOptions } from '@sentry/core';
2-
import {
3-
ensureHubOnCarrier,
4-
getCurrentHub as getCurrentHubCore,
5-
getHubFromCarrier,
6-
setAsyncContextStrategy,
7-
} from '@sentry/core';
2+
import { ensureHubOnCarrier, getHubFromCarrier, setAsyncContextStrategy, setHubOnCarrier } from '@sentry/core';
83
import * as domain from 'domain';
94
import { EventEmitter } from 'events';
105

@@ -26,33 +21,40 @@ function getCurrentHub(): Hub | undefined {
2621
return getHubFromCarrier(activeDomain);
2722
}
2823

24+
function createNewHub(parent: Hub | undefined): Hub {
25+
const carrier: Carrier = {};
26+
ensureHubOnCarrier(carrier, parent);
27+
return getHubFromCarrier(carrier);
28+
}
29+
2930
function runWithAsyncContext<T>(callback: (hub: Hub) => T, options: RunWithAsyncContextOptions): T {
30-
if (options?.reuseExisting) {
31-
const activeDomain = getActiveDomain<domain.Domain & Carrier>();
31+
const activeDomain = getActiveDomain<domain.Domain & Carrier>();
3232

33-
if (activeDomain) {
34-
for (const emitter of options.emitters || []) {
35-
if (emitter instanceof EventEmitter) {
36-
activeDomain.add(emitter);
37-
}
33+
if (activeDomain && options?.reuseExisting) {
34+
for (const emitter of options.emitters || []) {
35+
if (emitter instanceof EventEmitter) {
36+
activeDomain.add(emitter);
3837
}
39-
40-
// We're already in a domain, so we don't need to create a new one, just call the callback with the current hub
41-
return callback(getHubFromCarrier(activeDomain));
4238
}
39+
40+
// We're already in a domain, so we don't need to create a new one, just call the callback with the current hub
41+
return callback(getHubFromCarrier(activeDomain));
4342
}
4443

45-
const local = domain.create();
44+
const local = domain.create() as domain.Domain & Carrier;
4645

4746
for (const emitter of options.emitters || []) {
4847
if (emitter instanceof EventEmitter) {
4948
local.add(emitter);
5049
}
5150
}
5251

52+
const parentHub = activeDomain ? getHubFromCarrier(activeDomain) : undefined;
53+
const newHub = createNewHub(parentHub);
54+
setHubOnCarrier(local, newHub);
55+
5356
return local.bind(() => {
54-
const hub = getCurrentHubCore();
55-
return callback(hub);
57+
return callback(newHub);
5658
})();
5759
}
5860

packages/node/test/async/domain.test.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,37 @@ describe('domains', () => {
1616
expect(hub).toEqual(new Hub());
1717
});
1818

19-
test('domain hub scope inheritance', () => {
19+
test('hub scope inheritance', () => {
20+
setDomainAsyncContextStrategy();
21+
2022
const globalHub = getCurrentHub();
21-
globalHub.configureScope(scope => {
22-
scope.setExtra('a', 'b');
23-
scope.setTag('a', 'b');
24-
scope.addBreadcrumb({ message: 'a' });
25-
});
26-
runWithAsyncContext(hub => {
27-
expect(globalHub).toEqual(hub);
23+
globalHub.setExtra('a', 'b');
24+
25+
runWithAsyncContext(hub1 => {
26+
expect(hub1).toEqual(globalHub);
27+
28+
hub1.setExtra('c', 'd');
29+
expect(hub1).not.toEqual(globalHub);
30+
31+
runWithAsyncContext(hub2 => {
32+
expect(hub2).toEqual(hub1);
33+
expect(hub2).not.toEqual(globalHub);
34+
35+
hub2.setExtra('e', 'f');
36+
expect(hub2).not.toEqual(hub1);
37+
});
2838
});
2939
});
3040

31-
test('domain hub single instance', () => {
41+
test('hub single instance', () => {
3242
setDomainAsyncContextStrategy();
3343

3444
runWithAsyncContext(hub => {
3545
expect(hub).toBe(getCurrentHub());
3646
});
3747
});
3848

39-
test('domain within a domain not reused', () => {
49+
test('within a domain not reused', () => {
4050
setDomainAsyncContextStrategy();
4151

4252
runWithAsyncContext(hub1 => {
@@ -46,7 +56,7 @@ describe('domains', () => {
4656
});
4757
});
4858

49-
test('domain within a domain reused when requested', () => {
59+
test('within a domain reused when requested', () => {
5060
setDomainAsyncContextStrategy();
5161

5262
runWithAsyncContext(hub1 => {
@@ -59,7 +69,7 @@ describe('domains', () => {
5969
});
6070
});
6171

62-
test('concurrent domain hubs', done => {
72+
test('concurrent hub contexts', done => {
6373
setDomainAsyncContextStrategy();
6474

6575
let d1done = false;

packages/node/test/handlers.test.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
1+
import type { Hub } from '@sentry/core';
12
import * as sentryCore from '@sentry/core';
2-
import { Transaction } from '@sentry/core';
3+
import { setAsyncContextStrategy, Transaction } from '@sentry/core';
34
import type { Event } from '@sentry/types';
45
import { SentryError } from '@sentry/utils';
56
import * as http from 'http';
67

7-
import { setDomainAsyncContextStrategy } from '../src/async/domain';
88
import { NodeClient } from '../src/client';
99
import { errorHandler, requestHandler, tracingHandler } from '../src/handlers';
1010
import * as SDK from '../src/sdk';
1111
import { getDefaultNodeClientOptions } from './helper/node-client-options';
1212

13-
setDomainAsyncContextStrategy();
13+
function mockAsyncContextStrategy(getHub: () => Hub): void {
14+
function getCurrentHub(): Hub | undefined {
15+
return getHub();
16+
}
17+
18+
function runWithAsyncContext<T>(fn: (hub: Hub) => T): T {
19+
return fn(getHub());
20+
}
21+
22+
setAsyncContextStrategy({ getCurrentHub, runWithAsyncContext });
23+
}
1424

1525
describe('requestHandler', () => {
1626
const headers = { ears: 'furry', nose: 'wet', tongue: 'spotted', cookie: 'favorite=zukes' };
@@ -52,6 +62,7 @@ describe('requestHandler', () => {
5262
const hub = new sentryCore.Hub(client);
5363

5464
jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
65+
mockAsyncContextStrategy(() => hub);
5566

5667
sentryRequestMiddleware(req, res, next);
5768

@@ -65,6 +76,7 @@ describe('requestHandler', () => {
6576
const hub = new sentryCore.Hub(client);
6677

6778
jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
79+
mockAsyncContextStrategy(() => hub);
6880

6981
sentryRequestMiddleware(req, res, next);
7082

@@ -78,6 +90,7 @@ describe('requestHandler', () => {
7890
const hub = new sentryCore.Hub(client);
7991

8092
jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
93+
mockAsyncContextStrategy(() => hub);
8194

8295
const captureRequestSession = jest.spyOn<any, any>(client, '_captureRequestSession');
8396

@@ -97,7 +110,9 @@ describe('requestHandler', () => {
97110
const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '1.2' });
98111
client = new NodeClient(options);
99112
const hub = new sentryCore.Hub(client);
113+
100114
jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
115+
mockAsyncContextStrategy(() => hub);
101116

102117
const captureRequestSession = jest.spyOn<any, any>(client, '_captureRequestSession');
103118

@@ -142,6 +157,7 @@ describe('requestHandler', () => {
142157
it('stores request and request data options in `sdkProcessingMetadata`', () => {
143158
const hub = new sentryCore.Hub(new NodeClient(getDefaultNodeClientOptions()));
144159
jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
160+
mockAsyncContextStrategy(() => hub);
145161

146162
const requestHandlerOptions = { include: { ip: false } };
147163
const sentryRequestMiddleware = requestHandler(requestHandlerOptions);
@@ -177,6 +193,7 @@ describe('tracingHandler', () => {
177193
beforeEach(() => {
178194
hub = new sentryCore.Hub(new NodeClient(getDefaultNodeClientOptions({ tracesSampleRate: 1.0 })));
179195
jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
196+
mockAsyncContextStrategy(() => hub);
180197
req = {
181198
headers,
182199
method,
@@ -274,6 +291,8 @@ describe('tracingHandler', () => {
274291
const tracesSampler = jest.fn();
275292
const options = getDefaultNodeClientOptions({ tracesSampler });
276293
const hub = new sentryCore.Hub(new NodeClient(options));
294+
mockAsyncContextStrategy(() => hub);
295+
277296
hub.run(() => {
278297
sentryTracingMiddleware(req, res, next);
279298

@@ -296,6 +315,7 @@ describe('tracingHandler', () => {
296315
const hub = new sentryCore.Hub(new NodeClient(options));
297316

298317
jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
318+
mockAsyncContextStrategy(() => hub);
299319

300320
sentryTracingMiddleware(req, res, next);
301321

@@ -502,14 +522,17 @@ describe('errorHandler()', () => {
502522
client.initSessionFlusher();
503523
const scope = new sentryCore.Scope();
504524
const hub = new sentryCore.Hub(client, scope);
525+
mockAsyncContextStrategy(() => hub);
505526

506527
jest.spyOn<any, any>(client, '_captureRequestSession');
507528

508529
hub.run(() => {
509530
scope?.setRequestSession({ status: 'ok' });
510-
sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, next);
511-
const requestSession = scope?.getRequestSession();
512-
expect(requestSession).toEqual({ status: 'crashed' });
531+
sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, () => {
532+
const scope = sentryCore.getCurrentHub().getScope();
533+
const requestSession = scope?.getRequestSession();
534+
expect(requestSession).toEqual({ status: 'crashed' });
535+
});
513536
});
514537
});
515538

@@ -535,6 +558,7 @@ describe('errorHandler()', () => {
535558
client = new NodeClient(options);
536559

537560
const hub = new sentryCore.Hub(client);
561+
mockAsyncContextStrategy(() => hub);
538562
sentryCore.makeMain(hub);
539563

540564
// `sentryErrorMiddleware` uses `withScope`, and we need access to the temporary scope it creates, so monkeypatch

0 commit comments

Comments
 (0)