Skip to content

Commit dc8726a

Browse files
authored
feat: Implement new Async Context Strategy (#10647)
This updates the ACS to not rely on hubs (only) anymore. Now, instead of the ACS providing only `getCurrentHub` and `runWithAsyncContext`, this is changed a bit: 1. There is always a strategy, even if running in browser or similar. we just use the default (=stack) strategy in that case. 2. The ACS always returns a hub/scope/etc, not `undefined` - so the strategy must take care of falling back to the global hub itself (to make it easier to implement hub<>scope interop). The ACS defines the following methods now: * `getCurrentScope` * `getIsolationScope` * `withScope` * `withSetScope` - a variant that takes a scope and makes it the active one. I decided to make this a dedicated method on the ACS instead of overloading `withScope` because the types for that are rather tricky to repeat in strategies... * `withIsolationScope` * `withSetIsolationScope` - not we do not use this yet, but to keep the door open for us this is already required. * `getCurrentHub` --> for now, for backwards compatibility The methods themselves are pretty straightforward now! We basically always create a new hub, and decide if we want to fork the current/isolation scope based on what method has been used. We still use a hub everywhere under the hood for now.
1 parent cbf64ef commit dc8726a

File tree

80 files changed

+1215
-839
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+1215
-839
lines changed

dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ export const config = {
55
};
66

77
export default async function handler() {
8-
// Without `runWithAsyncContext` and a working async context strategy the two spans created by `Sentry.startSpan()` would be nested.
8+
// Without a working async context strategy the two spans created by `Sentry.startSpan()` would be nested.
99

10-
const outerSpanPromise = Sentry.runWithAsyncContext(() => {
10+
const outerSpanPromise = Sentry.withIsolationScope(() => {
1111
return Sentry.startSpan({ name: 'outer-span' }, () => {
1212
return new Promise<void>(resolve => setTimeout(resolve, 300));
1313
});
1414
});
1515

1616
setTimeout(() => {
17-
Sentry.runWithAsyncContext(() => {
17+
Sentry.withIsolationScope(() => {
1818
return Sentry.startSpan({ name: 'inner-span' }, () => {
1919
return new Promise<void>(resolve => setTimeout(resolve, 100));
2020
});

dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Sentry.withScope(scope => {
2121
Sentry.captureMessage('inner');
2222
});
2323

24-
Sentry.runWithAsyncContext(() => {
24+
Sentry.withIsolationScope(() => {
2525
Sentry.getIsolationScope().setExtra('ff', 'ff');
2626
Sentry.getCurrentScope().setExtra('gg', 'gg');
2727
Sentry.captureMessage('inner_async_context');

packages/astro/src/server/middleware.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {
55
getActiveSpan,
66
getClient,
77
getCurrentScope,
8-
runWithAsyncContext,
98
startSpan,
9+
withIsolationScope,
1010
} from '@sentry/node';
1111
import type { Client, Scope, Span } from '@sentry/types';
1212
import { addNonEnumerableProperty, objectify, stripUrlQueryAndFragment } from '@sentry/utils';
@@ -74,7 +74,7 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH
7474
if (getActiveSpan()) {
7575
return instrumentRequest(ctx, next, handlerOptions);
7676
}
77-
return runWithAsyncContext(() => {
77+
return withIsolationScope(() => {
7878
return instrumentRequest(ctx, next, handlerOptions);
7979
});
8080
};

packages/astro/test/client/sdk.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import type { BrowserClient } from '@sentry/browser';
22
import { getActiveSpan } from '@sentry/browser';
33
import { browserTracingIntegration } from '@sentry/browser';
44
import * as SentryBrowser from '@sentry/browser';
5-
import { SDK_VERSION, WINDOW, getClient } from '@sentry/browser';
5+
import { SDK_VERSION, getClient } from '@sentry/browser';
66
import { vi } from 'vitest';
77

8-
import { getIsolationScope } from '@sentry/core';
8+
import { getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core';
99
import { init } from '../../../astro/src/client/sdk';
1010

1111
const browserInit = vi.spyOn(SentryBrowser, 'init');
@@ -14,7 +14,11 @@ describe('Sentry client SDK', () => {
1414
describe('init', () => {
1515
afterEach(() => {
1616
vi.clearAllMocks();
17-
WINDOW.__SENTRY__.hub = undefined;
17+
18+
getCurrentScope().clear();
19+
getCurrentScope().setClient(undefined);
20+
getIsolationScope().clear();
21+
getGlobalScope().clear();
1822
});
1923

2024
it('adds Astro metadata to the SDK options', () => {

packages/astro/test/server/middleware.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,10 @@ describe('sentryMiddleware', () => {
260260
});
261261

262262
describe('async context isolation', () => {
263-
const runWithAsyncContextSpy = vi.spyOn(SentryNode, 'runWithAsyncContext');
263+
const withIsolationScopeSpy = vi.spyOn(SentryNode, 'withIsolationScope');
264264
afterEach(() => {
265265
vi.clearAllMocks();
266-
runWithAsyncContextSpy.mockRestore();
266+
withIsolationScopeSpy.mockRestore();
267267
});
268268

269269
it('starts a new async context if no span is active', async () => {
@@ -279,7 +279,7 @@ describe('sentryMiddleware', () => {
279279
// this is fine, it's not required to pass in this test
280280
}
281281

282-
expect(runWithAsyncContextSpy).toHaveBeenCalledTimes(1);
282+
expect(withIsolationScopeSpy).toHaveBeenCalledTimes(1);
283283
});
284284

285285
it("doesn't start a new async context if a span is active", async () => {
@@ -297,7 +297,7 @@ describe('sentryMiddleware', () => {
297297
// this is fine, it's not required to pass in this test
298298
}
299299

300-
expect(runWithAsyncContextSpy).not.toHaveBeenCalled();
300+
expect(withIsolationScopeSpy).not.toHaveBeenCalled();
301301
});
302302
});
303303
});

packages/astro/test/server/sdk.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe('Sentry server SDK', () => {
1414
SentryNode.getGlobalScope().clear();
1515
SentryNode.getIsolationScope().clear();
1616
SentryNode.getCurrentScope().clear();
17+
SentryNode.getCurrentScope().setClient(undefined);
1718
});
1819

1920
it('adds Astro metadata to the SDK options', () => {

packages/browser/src/exports.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export {
4141
getGlobalScope,
4242
Hub,
4343
// eslint-disable-next-line deprecation/deprecation
44-
// eslint-disable-next-line deprecation/deprecation
4544
makeMain,
4645
setCurrentClient,
4746
Scope,

packages/browser/test/unit/index.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { InboundFilters, SDK_VERSION, getReportDialogEndpoint } from '@sentry/core';
1+
import { InboundFilters, SDK_VERSION, getGlobalScope, getIsolationScope, getReportDialogEndpoint } from '@sentry/core';
22
import type { WrappedFunction } from '@sentry/types';
33
import * as utils from '@sentry/utils';
44

@@ -39,7 +39,11 @@ describe('SentryBrowser', () => {
3939
const beforeSend = jest.fn(event => event);
4040

4141
beforeEach(() => {
42-
WINDOW.__SENTRY__ = { hub: undefined, logger: undefined, globalEventProcessors: [] };
42+
getGlobalScope().clear();
43+
getIsolationScope().clear();
44+
getCurrentScope().clear();
45+
getCurrentScope().setClient(undefined);
46+
4347
init({
4448
beforeSend,
4549
dsn,

packages/bun/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export {
4747
// eslint-disable-next-line deprecation/deprecation
4848
makeMain,
4949
setCurrentClient,
50+
// eslint-disable-next-line deprecation/deprecation
5051
runWithAsyncContext,
5152
Scope,
5253
// eslint-disable-next-line deprecation/deprecation

packages/bun/src/integrations/bunserver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {
77
convertIntegrationFnToClass,
88
defineIntegration,
99
getCurrentScope,
10-
runWithAsyncContext,
1110
setHttpStatus,
1211
startSpan,
12+
withIsolationScope,
1313
} from '@sentry/core';
1414
import type { IntegrationFn } from '@sentry/types';
1515
import { getSanitizedUrlString, parseUrl } from '@sentry/utils';
@@ -53,7 +53,7 @@ export function instrumentBunServe(): void {
5353
function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[0]): void {
5454
serveOptions.fetch = new Proxy(serveOptions.fetch, {
5555
apply(fetchTarget, fetchThisArg, fetchArgs: Parameters<typeof serveOptions.fetch>) {
56-
return runWithAsyncContext(() => {
56+
return withIsolationScope(() => {
5757
const request = fetchArgs[0];
5858
const upperCaseMethod = request.method.toUpperCase();
5959
if (upperCaseMethod === 'OPTIONS' || upperCaseMethod === 'HEAD') {

0 commit comments

Comments
 (0)