Skip to content

Commit 715876b

Browse files
authored
feat(core): Extend AsyncContextStrategy to allow reuse of existing context (#7778)
1 parent f0f9b8a commit 715876b

File tree

4 files changed

+53
-11
lines changed

4 files changed

+53
-11
lines changed

packages/core/src/hub.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,27 @@ export const API_VERSION = 4;
5050
*/
5151
const DEFAULT_BREADCRUMBS = 100;
5252

53+
export interface RunWithAsyncContextOptions {
54+
/** Whether to reuse an existing async context if one exists. Defaults to false. */
55+
reuseExisting?: boolean;
56+
/** Instances that should be referenced and retained in the new context */
57+
args?: unknown[];
58+
}
59+
5360
/**
61+
* @private Private API with no semver guarantees!
62+
*
5463
* Strategy used to track async context.
5564
*/
5665
export interface AsyncContextStrategy {
66+
/**
67+
* Gets the current async context. Returns undefined if there is no current async context.
68+
*/
5769
getCurrentHub: () => Hub | undefined;
58-
runWithAsyncContext<T>(callback: (hub: Hub) => T, ...args: unknown[]): T;
70+
/**
71+
* Runs the supplied callback in its own async context.
72+
*/
73+
runWithAsyncContext<T>(callback: (hub: Hub) => T, options: RunWithAsyncContextOptions): T;
5974
}
6075

6176
/**
@@ -583,13 +598,13 @@ export function setAsyncContextStrategy(strategy: AsyncContextStrategy | undefin
583598
/**
584599
* @private Private API with no semver guarantees!
585600
*
586-
* Runs the given callback function with the global async context strategy
601+
* Runs the supplied callback in its own async context.
587602
*/
588-
export function runWithAsyncContext<T>(callback: (hub: Hub) => T, ...args: unknown[]): T {
603+
export function runWithAsyncContext<T>(callback: (hub: Hub) => T, options: RunWithAsyncContextOptions = {}): T {
589604
const registry = getMainCarrier();
590605

591606
if (registry.__SENTRY__ && registry.__SENTRY__.acs) {
592-
return registry.__SENTRY__.acs.runWithAsyncContext(callback, ...args);
607+
return registry.__SENTRY__.acs.runWithAsyncContext(callback, options);
593608
}
594609

595610
// if there was no strategy, fallback to just calling the callback

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export type { ClientClass } from './sdk';
2-
export type { AsyncContextStrategy, Carrier, Layer } from './hub';
2+
export type { AsyncContextStrategy, Carrier, Layer, RunWithAsyncContextOptions } from './hub';
33
export type { OfflineStore, OfflineTransportOptions } from './transports/offline';
44

55
export * from './tracing';

packages/node/src/async/domain.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Carrier, Hub } from '@sentry/core';
1+
import type { Carrier, Hub, RunWithAsyncContextOptions } from '@sentry/core';
22
import {
33
ensureHubOnCarrier,
44
getCurrentHub as getCurrentHubCore,
@@ -8,9 +8,13 @@ import {
88
import * as domain from 'domain';
99
import { EventEmitter } from 'events';
1010

11-
function getCurrentHub(): Hub | undefined {
11+
function getActiveDomain<T>(): T | undefined {
1212
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
13-
const activeDomain = (domain as any).active as Carrier;
13+
return (domain as any).active as T | undefined;
14+
}
15+
16+
function getCurrentHub(): Hub | undefined {
17+
const activeDomain = getActiveDomain<Carrier>();
1418

1519
// If there's no active domain, just return undefined and the global hub will be used
1620
if (!activeDomain) {
@@ -22,10 +26,10 @@ function getCurrentHub(): Hub | undefined {
2226
return getHubFromCarrier(activeDomain);
2327
}
2428

25-
function runWithAsyncContext<T, A>(callback: (hub: Hub) => T, ...args: A[]): T {
26-
const local = domain.create();
29+
function runWithAsyncContext<T>(callback: (hub: Hub) => T, options: RunWithAsyncContextOptions): T {
30+
const local = options?.reuseExisting ? getActiveDomain<domain.Domain>() || domain.create() : domain.create();
2731

28-
for (const emitter of args) {
32+
for (const emitter of options.args || []) {
2933
if (emitter instanceof EventEmitter) {
3034
local.add(emitter);
3135
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,29 @@ describe('domains', () => {
3636
});
3737
});
3838

39+
test('domain within a domain not reused', () => {
40+
setDomainAsyncContextStrategy();
41+
42+
runWithAsyncContext(hub1 => {
43+
runWithAsyncContext(hub2 => {
44+
expect(hub1).not.toBe(hub2);
45+
});
46+
});
47+
});
48+
49+
test('domain within a domain reused when requested', () => {
50+
setDomainAsyncContextStrategy();
51+
52+
runWithAsyncContext(hub1 => {
53+
runWithAsyncContext(
54+
hub2 => {
55+
expect(hub1).toBe(hub2);
56+
},
57+
{ reuseExisting: true },
58+
);
59+
});
60+
});
61+
3962
test('concurrent domain hubs', done => {
4063
setDomainAsyncContextStrategy();
4164

0 commit comments

Comments
 (0)