Skip to content

Commit e741dd1

Browse files
authored
feat(node): Adds domain implementation of AsyncContextStrategy (#7767)
1 parent 3f084ac commit e741dd1

File tree

4 files changed

+142
-8
lines changed

4 files changed

+142
-8
lines changed

packages/core/src/hub.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const DEFAULT_BREADCRUMBS = 100;
5555
*/
5656
export interface AsyncContextStrategy {
5757
getCurrentHub: () => Hub | undefined;
58-
runWithAsyncContext<T, A>(callback: (hub: Hub, ...args: A[]) => T, ...args: A[]): T;
58+
runWithAsyncContext<T>(callback: (hub: Hub) => T, ...args: unknown[]): T;
5959
}
6060

6161
/**
@@ -536,25 +536,44 @@ export function getCurrentHub(): Hub {
536536
}
537537
}
538538

539+
// Prefer domains over global if they are there (applicable only to Node environment)
540+
if (isNodeEnv()) {
541+
return getHubFromActiveDomain(registry);
542+
}
543+
544+
// Return hub that lives on a global object
545+
return getGlobalHub(registry);
546+
}
547+
548+
function getGlobalHub(registry: Carrier = getMainCarrier()): Hub {
539549
// If there's no hub, or its an old API, assign a new one
540550
if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) {
541551
setHubOnCarrier(registry, new Hub());
542552
}
543553

544-
// Prefer domains over global if they are there (applicable only to Node environment)
545-
if (isNodeEnv()) {
546-
return getHubFromActiveDomain(registry);
547-
}
548554
// Return hub that lives on a global object
549555
return getHubFromCarrier(registry);
550556
}
551557

558+
/**
559+
* @private Private API with no semver guarantees!
560+
*
561+
* If the carrier does not contain a hub, a new hub is created with the global hub client and scope.
562+
*/
563+
export function ensureHubOnCarrier(carrier: Carrier): void {
564+
// If there's no hub on current domain, or it's an old API, assign a new one
565+
if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) {
566+
const globalHubTopStack = getGlobalHub().getStackTop();
567+
setHubOnCarrier(carrier, new Hub(globalHubTopStack.client, Scope.clone(globalHubTopStack.scope)));
568+
}
569+
}
570+
552571
/**
553572
* @private Private API with no semver guarantees!
554573
*
555574
* Sets the global async context strategy
556575
*/
557-
export function setAsyncContextStrategy(strategy: AsyncContextStrategy): void {
576+
export function setAsyncContextStrategy(strategy: AsyncContextStrategy | undefined): void {
558577
// Get main carrier (global for every environment)
559578
const registry = getMainCarrier();
560579
registry.__SENTRY__ = registry.__SENTRY__ || {};
@@ -566,15 +585,15 @@ export function setAsyncContextStrategy(strategy: AsyncContextStrategy): void {
566585
*
567586
* Runs the given callback function with the global async context strategy
568587
*/
569-
export function runWithAsyncContext<T, A>(callback: (hub: Hub, ...args: A[]) => T, ...args: A[]): T {
588+
export function runWithAsyncContext<T>(callback: (hub: Hub) => T, ...args: unknown[]): T {
570589
const registry = getMainCarrier();
571590

572591
if (registry.__SENTRY__ && registry.__SENTRY__.acs) {
573592
return registry.__SENTRY__.acs.runWithAsyncContext(callback, ...args);
574593
}
575594

576595
// if there was no strategy, fallback to just calling the callback
577-
return callback(getCurrentHub(), ...args);
596+
return callback(getCurrentHub());
578597
}
579598

580599
/**

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export {
2626
getMainCarrier,
2727
runWithAsyncContext,
2828
setHubOnCarrier,
29+
ensureHubOnCarrier,
2930
setAsyncContextStrategy,
3031
} from './hub';
3132
export { makeSession, closeSession, updateSession } from './session';

packages/node/src/async/domain.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Carrier, Hub } from '@sentry/core';
2+
import {
3+
ensureHubOnCarrier,
4+
getCurrentHub as getCurrentHubCore,
5+
getHubFromCarrier,
6+
setAsyncContextStrategy,
7+
} from '@sentry/core';
8+
import * as domain from 'domain';
9+
import { EventEmitter } from 'events';
10+
11+
function getCurrentHub(): Hub | undefined {
12+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
13+
const activeDomain = (domain as any).active as Carrier;
14+
15+
// If there's no active domain, just return undefined and the global hub will be used
16+
if (!activeDomain) {
17+
return undefined;
18+
}
19+
20+
ensureHubOnCarrier(activeDomain);
21+
22+
return getHubFromCarrier(activeDomain);
23+
}
24+
25+
function runWithAsyncContext<T, A>(callback: (hub: Hub) => T, ...args: A[]): T {
26+
const local = domain.create();
27+
28+
for (const emitter of args) {
29+
if (emitter instanceof EventEmitter) {
30+
local.add(emitter);
31+
}
32+
}
33+
34+
return local.bind(() => {
35+
const hub = getCurrentHubCore();
36+
return callback(hub);
37+
})();
38+
}
39+
40+
/**
41+
* Sets the async context strategy to use Node.js domains.
42+
*/
43+
export function setDomainAsyncContextStrategy(): void {
44+
setAsyncContextStrategy({ getCurrentHub, runWithAsyncContext });
45+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { getCurrentHub, Hub, runWithAsyncContext, setAsyncContextStrategy } from '@sentry/core';
2+
import * as domain from 'domain';
3+
4+
import { setDomainAsyncContextStrategy } from '../../src/async/domain';
5+
6+
describe('domains', () => {
7+
afterAll(() => {
8+
// clear the strategy
9+
setAsyncContextStrategy(undefined);
10+
});
11+
12+
test('without domain', () => {
13+
// @ts-ignore property active does not exist on domain
14+
expect(domain.active).toBeFalsy();
15+
const hub = getCurrentHub();
16+
expect(hub).toEqual(new Hub());
17+
});
18+
19+
test('domain hub scope inheritance', () => {
20+
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);
28+
});
29+
});
30+
31+
test('domain hub single instance', () => {
32+
setDomainAsyncContextStrategy();
33+
34+
runWithAsyncContext(hub => {
35+
expect(hub).toBe(getCurrentHub());
36+
});
37+
});
38+
39+
test('concurrent domain hubs', done => {
40+
setDomainAsyncContextStrategy();
41+
42+
let d1done = false;
43+
let d2done = false;
44+
45+
runWithAsyncContext(hub => {
46+
hub.getStack().push({ client: 'process' } as any);
47+
expect(hub.getStack()[1]).toEqual({ client: 'process' });
48+
// Just in case so we don't have to worry which one finishes first
49+
// (although it always should be d2)
50+
setTimeout(() => {
51+
d1done = true;
52+
if (d2done) {
53+
done();
54+
}
55+
});
56+
});
57+
58+
runWithAsyncContext(hub => {
59+
hub.getStack().push({ client: 'local' } as any);
60+
expect(hub.getStack()[1]).toEqual({ client: 'local' });
61+
setTimeout(() => {
62+
d2done = true;
63+
if (d1done) {
64+
done();
65+
}
66+
});
67+
});
68+
});
69+
});

0 commit comments

Comments
 (0)