diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index adcf95527364..73dce6f0fa7f 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -27,6 +27,8 @@ export { getCurrentHub, getClient, getCurrentScope, + getGlobalScope, + getIsolationScope, Hub, makeMain, Scope, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 499e969f3843..0166b43d137d 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -44,6 +44,8 @@ export { getCurrentHub, getClient, getCurrentScope, + getGlobalScope, + getIsolationScope, Hub, lastEventId, makeMain, diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index fc0258167530..865fdcbd14b5 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -49,6 +49,7 @@ import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; import { DEBUG_BUILD } from './debug-build'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; import { getClient } from './exports'; +import { getIsolationScope } from './hub'; import type { IntegrationIndex } from './integration'; import { setupIntegration, setupIntegrations } from './integration'; import { createMetricEnvelope } from './metrics/envelope'; @@ -588,7 +589,12 @@ export abstract class BaseClient implements Client { * @param scope A scope containing event metadata. * @returns A new event with more information. */ - protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + protected _prepareEvent( + event: Event, + hint: EventHint, + scope?: Scope, + isolationScope = getIsolationScope(), + ): PromiseLike { const options = this.getOptions(); const integrations = Object.keys(this._integrations); if (!hint.integrations && integrations.length > 0) { @@ -597,7 +603,7 @@ export abstract class BaseClient implements Client { this.emit('preprocessEvent', event, hint); - return prepareEvent(options, event, hint, scope, this).then(evt => { + return prepareEvent(options, event, hint, scope, this, isolationScope).then(evt => { if (evt === null) { return evt; } diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 75960550081a..29e9d5af3956 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -104,6 +104,8 @@ export class Hub implements HubInterface { /** Contains the last event id of a captured event. */ private _lastEventId?: string; + private _isolationScope: Scope; + /** * Creates a new instance of the hub, will push one {@link Layer} into the * internal stack on creation. @@ -112,11 +114,18 @@ export class Hub implements HubInterface { * @param scope bound to the hub. * @param version number, higher number means higher priority. */ - public constructor(client?: Client, scope: Scope = new Scope(), private readonly _version: number = API_VERSION) { + public constructor( + client?: Client, + scope: Scope = new Scope(), + isolationScope = new Scope(), + private readonly _version: number = API_VERSION, + ) { this._stack = [{ scope }]; if (client) { this.bindClient(client); } + + this._isolationScope = isolationScope; } /** @@ -188,6 +197,11 @@ export class Hub implements HubInterface { return this.getStackTop().scope; } + /** @inheritdoc */ + public getIsolationScope(): Scope { + return this._isolationScope; + } + /** Returns the scope stack for domains or the process. */ public getStack(): Layer[] { return this._stack; @@ -567,6 +581,15 @@ export function getCurrentHub(): Hub { return getGlobalHub(registry); } +/** + * Get the currently active isolation scope. + * The isolation scope is active for the current exection context, + * meaning that it will remain stable for the same Hub. + */ +export function getIsolationScope(): Scope { + return getCurrentHub().getIsolationScope(); +} + function getGlobalHub(registry: Carrier = getMainCarrier()): Hub { // If there's no hub, or its an old API, assign a new one if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) { @@ -585,8 +608,10 @@ function getGlobalHub(registry: Carrier = getMainCarrier()): Hub { export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub()): void { // If there's no hub on current domain, or it's an old API, assign a new one if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) { - const globalHubTopStack = parent.getStackTop(); - setHubOnCarrier(carrier, new Hub(globalHubTopStack.client, globalHubTopStack.scope.clone())); + const client = parent.getClient(); + const scope = parent.getScope(); + const isolationScope = parent.getIsolationScope(); + setHubOnCarrier(carrier, new Hub(client, scope.clone(), isolationScope.clone())); } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index dbf18135ef67..3954dc6a7005 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -31,6 +31,7 @@ export { } from './exports'; export { getCurrentHub, + getIsolationScope, getHubFromCarrier, Hub, makeMain, diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 9cffaca15faf..f4abd134223f 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -216,7 +216,12 @@ export class ServerRuntimeClient< /** * @inheritDoc */ - protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + protected _prepareEvent( + event: Event, + hint: EventHint, + scope?: Scope, + isolationScope?: Scope, + ): PromiseLike { if (this._options.platform) { event.platform = event.platform || this._options.platform; } @@ -232,7 +237,7 @@ export class ServerRuntimeClient< event.server_name = event.server_name || this._options.serverName; } - return super._prepareEvent(event, hint, scope); + return super._prepareEvent(event, hint, scope, isolationScope); } /** Extract trace information from scope */ diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index a4584c34066a..6f448df8496d 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -48,6 +48,7 @@ export function prepareEvent( hint: EventHint, scope?: Scope, client?: Client, + isolationScope?: Scope, ): PromiseLike { const { normalizeDepth = 3, normalizeMaxBreadth = 1_000 } = options; const prepared: Event = { @@ -80,6 +81,11 @@ export function prepareEvent( // Merge scope data together const data = getGlobalScope().getScopeData(); + if (isolationScope) { + const isolationData = isolationScope.getScopeData(); + mergeScopeData(data, isolationData); + } + if (finalScope) { const finalScopeData = finalScope.getScopeData(); mergeScopeData(data, finalScopeData); diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index 09c48afffc35..98895ba31256 100644 --- a/packages/core/test/lib/prepareEvent.test.ts +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -9,7 +9,7 @@ import type { ScopeContext, } from '@sentry/types'; import { GLOBAL_OBJ, createStackParser } from '@sentry/utils'; -import { setGlobalScope } from '../../src'; +import { getCurrentHub, getIsolationScope, setGlobalScope } from '../../src'; import { Scope, getGlobalScope } from '../../src/scope'; import { @@ -192,6 +192,7 @@ describe('parseEventHintOrCaptureContext', () => { describe('prepareEvent', () => { beforeEach(() => { setGlobalScope(undefined); + getCurrentHub().getIsolationScope().clear(); }); it('works without any scope data', async () => { @@ -240,12 +241,15 @@ describe('prepareEvent', () => { const breadcrumb1 = { message: '1', timestamp: 111 } as Breadcrumb; const breadcrumb2 = { message: '2', timestamp: 222 } as Breadcrumb; const breadcrumb3 = { message: '3', timestamp: 123 } as Breadcrumb; + const breadcrumb4 = { message: '4', timestamp: 123 } as Breadcrumb; const eventProcessor1 = jest.fn((a: unknown) => a) as EventProcessor; const eventProcessor2 = jest.fn((b: unknown) => b) as EventProcessor; + const eventProcessor3 = jest.fn((b: unknown) => b) as EventProcessor; const attachment1 = { filename: '1' } as Attachment; const attachment2 = { filename: '2' } as Attachment; + const attachment3 = { filename: '3' } as Attachment; const scope = new Scope(); scope.update({ @@ -261,13 +265,19 @@ describe('prepareEvent', () => { scope.addAttachment(attachment1); const globalScope = getGlobalScope(); + const isolationScope = getIsolationScope(); globalScope.addBreadcrumb(breadcrumb2); globalScope.addEventProcessor(eventProcessor2); globalScope.setSDKProcessingMetadata({ aa: 'aa' }); globalScope.addAttachment(attachment2); - const event = { message: 'foo', breadcrumbs: [breadcrumb3], fingerprint: ['dd'] }; + isolationScope.addBreadcrumb(breadcrumb3); + isolationScope.addEventProcessor(eventProcessor3); + isolationScope.setSDKProcessingMetadata({ bb: 'bb' }); + isolationScope.addAttachment(attachment3); + + const event = { message: 'foo', breadcrumbs: [breadcrumb4], fingerprint: ['dd'] }; const options = {} as ClientOptions; const processedEvent = await prepareEvent( @@ -277,15 +287,18 @@ describe('prepareEvent', () => { integrations: [], }, scope, + undefined, + isolationScope, ); expect(eventProcessor1).toHaveBeenCalledTimes(1); expect(eventProcessor2).toHaveBeenCalledTimes(1); + expect(eventProcessor3).toHaveBeenCalledTimes(1); // Test that attachments are correctly merged expect(eventProcessor1).toHaveBeenCalledWith(processedEvent, { integrations: [], - attachments: [attachment2, attachment1], + attachments: [attachment2, attachment3, attachment1], }); expect(processedEvent).toEqual({ @@ -298,9 +311,10 @@ describe('prepareEvent', () => { extra: { extra1: 'aa', extra2: 'aa' }, contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, fingerprint: ['dd', 'aa'], - breadcrumbs: [breadcrumb3, breadcrumb2, breadcrumb1], + breadcrumbs: [breadcrumb4, breadcrumb2, breadcrumb3, breadcrumb1], sdkProcessingMetadata: { aa: 'aa', + bb: 'bb', propagationContext: { spanId: '1', traceId: '1', @@ -312,33 +326,50 @@ describe('prepareEvent', () => { it('works without a scope', async () => { const breadcrumb1 = { message: '1', timestamp: 111 } as Breadcrumb; const breadcrumb2 = { message: '2', timestamp: 222 } as Breadcrumb; + const breadcrumb3 = { message: '3', timestamp: 333 } as Breadcrumb; const eventProcessor1 = jest.fn((a: unknown) => a) as EventProcessor; + const eventProcessor2 = jest.fn((a: unknown) => a) as EventProcessor; - const attachment1 = { filename: '1' } as Attachment; - const attachment2 = { filename: '2' } as Attachment; + const attachmentGlobal = { filename: 'global scope attachment' } as Attachment; + const attachmentIsolation = { filename: 'isolation scope attachment' } as Attachment; + const attachmentHint = { filename: 'hint attachment' } as Attachment; const globalScope = getGlobalScope(); + const isolationScope = getIsolationScope(); globalScope.addBreadcrumb(breadcrumb1); globalScope.addEventProcessor(eventProcessor1); globalScope.setSDKProcessingMetadata({ aa: 'aa' }); - globalScope.addAttachment(attachment1); + globalScope.addAttachment(attachmentGlobal); - const event = { message: 'foo', breadcrumbs: [breadcrumb2], fingerprint: ['dd'] }; + isolationScope.addBreadcrumb(breadcrumb2); + isolationScope.addEventProcessor(eventProcessor2); + isolationScope.setSDKProcessingMetadata({ bb: 'bb' }); + isolationScope.addAttachment(attachmentIsolation); + + const event = { message: 'foo', breadcrumbs: [breadcrumb3], fingerprint: ['dd'] }; const options = {} as ClientOptions; - const processedEvent = await prepareEvent(options, event, { - integrations: [], - attachments: [attachment2], - }); + const processedEvent = await prepareEvent( + options, + event, + { + integrations: [], + attachments: [attachmentHint], + }, + undefined, + undefined, + isolationScope, + ); expect(eventProcessor1).toHaveBeenCalledTimes(1); + expect(eventProcessor2).toHaveBeenCalledTimes(1); // Test that attachments are correctly merged expect(eventProcessor1).toHaveBeenCalledWith(processedEvent, { integrations: [], - attachments: [attachment2, attachment1], + attachments: [attachmentHint, attachmentGlobal, attachmentIsolation], }); expect(processedEvent).toEqual({ @@ -347,10 +378,11 @@ describe('prepareEvent', () => { environment: 'production', message: 'foo', fingerprint: ['dd'], - breadcrumbs: [breadcrumb2, breadcrumb1], + breadcrumbs: [breadcrumb3, breadcrumb1, breadcrumb2], sdkProcessingMetadata: { aa: 'aa', - propagationContext: globalScope.getPropagationContext(), + bb: 'bb', + propagationContext: isolationScope.getPropagationContext(), }, }); }); diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index bd2a7061019a..4a57bb6f2cfd 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -43,6 +43,8 @@ export { getCurrentHub, getClient, getCurrentScope, + getGlobalScope, + getIsolationScope, Hub, lastEventId, makeMain, diff --git a/packages/feedback/src/util/prepareFeedbackEvent.ts b/packages/feedback/src/util/prepareFeedbackEvent.ts index 9b1b0f9c6e8b..cb48efeaf89d 100644 --- a/packages/feedback/src/util/prepareFeedbackEvent.ts +++ b/packages/feedback/src/util/prepareFeedbackEvent.ts @@ -1,4 +1,5 @@ import type { Scope } from '@sentry/core'; +import { getIsolationScope } from '@sentry/core'; import { prepareEvent } from '@sentry/core'; import type { Client, FeedbackEvent } from '@sentry/types'; @@ -26,6 +27,7 @@ export async function prepareFeedbackEvent({ eventHint, scope, client, + getIsolationScope(), )) as FeedbackEvent | null; if (preparedEvent === null) { diff --git a/packages/hub/test/global.test.ts b/packages/hub/test/global.test.ts index dafe638a5a92..23bc51193a29 100644 --- a/packages/hub/test/global.test.ts +++ b/packages/hub/test/global.test.ts @@ -19,13 +19,13 @@ describe('global', () => { }); test('getGlobalHub', () => { - const newestHub = new Hub(undefined, undefined, 999999); + const newestHub = new Hub(undefined, undefined, undefined, 999999); GLOBAL_OBJ.__SENTRY__.hub = newestHub; expect(getCurrentHub()).toBe(newestHub); }); test('hub extension methods receive correct hub instance', () => { - const newestHub = new Hub(undefined, undefined, 999999); + const newestHub = new Hub(undefined, undefined, undefined, 999999); GLOBAL_OBJ.__SENTRY__.hub = newestHub; const fn = jest.fn().mockImplementation(function (...args: []) { // @ts-expect-error typescript complains that this can be `any` diff --git a/packages/node-experimental/src/sdk/client.ts b/packages/node-experimental/src/sdk/client.ts index 8a7626b4ff9c..6f1012bdb422 100644 --- a/packages/node-experimental/src/sdk/client.ts +++ b/packages/node-experimental/src/sdk/client.ts @@ -4,7 +4,7 @@ import type { Tracer } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { CaptureContext, Event, EventHint } from '@sentry/types'; -import { Scope } from './scope'; +import { Scope, getIsolationScope } from './scope'; /** A client for using Sentry with Node & OpenTelemetry. */ export class NodeExperimentalClient extends NodeClient { @@ -59,7 +59,12 @@ export class NodeExperimentalClient extends NodeClient { * Extends the base `_prepareEvent` so that we can properly handle `captureContext`. * This uses `new Scope()`, which we need to replace with our own Scope for this client. */ - protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + protected _prepareEvent( + event: Event, + hint: EventHint, + scope?: Scope, + _isolationScope?: Scope, + ): PromiseLike { let actualScope = scope; // Remove `captureContext` hint and instead clone already here @@ -68,7 +73,9 @@ export class NodeExperimentalClient extends NodeClient { delete hint.captureContext; } - return super._prepareEvent(event, hint, actualScope); + const isolationScope = _isolationScope || (scope && scope.isolationScope) || getIsolationScope(); + + return super._prepareEvent(event, hint, actualScope, isolationScope); } } diff --git a/packages/node-experimental/src/sdk/hub.ts b/packages/node-experimental/src/sdk/hub.ts index 21e1c83a34bb..b58548acb326 100644 --- a/packages/node-experimental/src/sdk/hub.ts +++ b/packages/node-experimental/src/sdk/hub.ts @@ -5,7 +5,6 @@ import type { Hub, Integration, IntegrationClass, - Session, Severity, SeverityLevel, TransactionContext, @@ -14,8 +13,6 @@ import type { import { addBreadcrumb, captureEvent, - captureException, - captureMessage, configureScope, endSession, getClient, @@ -32,6 +29,7 @@ import { } from './api'; import { callExtensionMethod, getGlobalCarrier } from './globals'; import type { Scope } from './scope'; +import { getIsolationScope } from './scope'; import type { SentryCarrier } from './types'; /** Ensure the global hub is our proxied hub. */ @@ -67,6 +65,7 @@ export function getCurrentHub(): Hub { withScope, getClient, getScope: getCurrentScope, + getIsolationScope, captureException: (exception: unknown, hint?: EventHint) => { return getCurrentScope().captureException(exception, hint); }, diff --git a/packages/node-experimental/src/sdk/scope.ts b/packages/node-experimental/src/sdk/scope.ts index f169f95250e2..86bad56e932e 100644 --- a/packages/node-experimental/src/sdk/scope.ts +++ b/packages/node-experimental/src/sdk/scope.ts @@ -189,32 +189,6 @@ export class Scope extends OpenTelemetryScope implements ScopeInterface { public getOwnScopeData(): ScopeData { return super.getScopeData(); } - - /** @inheritdoc */ - public getScopeData(): ScopeData { - const globalScope = getGlobalScope(); - const isolationScope = this._getIsolationScope(); - - // Special case: If this is the global/isolation scope, no need to merge other data in here - if (this === globalScope || this === isolationScope) { - return this.getOwnScopeData(); - } - - // Global scope is applied anyhow in prepareEvent, - // but we need to merge the isolation scope in here - const data = isolationScope.getOwnScopeData(); - const scopeData = this.getOwnScopeData(); - - // Merge data together, in order - mergeScopeData(data, scopeData); - - return data; - } - - /** Get the isolation scope for this scope. */ - protected _getIsolationScope(): Scope { - return this.isolationScope || getIsolationScope(); - } } function getScopes(): CurrentScopes { diff --git a/packages/node-experimental/test/sdk/scope.test.ts b/packages/node-experimental/test/sdk/scope.test.ts index c38437121959..6e1245c35aed 100644 --- a/packages/node-experimental/test/sdk/scope.test.ts +++ b/packages/node-experimental/test/sdk/scope.test.ts @@ -94,20 +94,6 @@ describe('Unit | Scope', () => { expect(scope.getClient()).toBe(client); }); - it('gets the correct isolationScope in _getIsolationScope', () => { - resetGlobals(); - - const scope = new Scope(); - const globalIsolationScope = getIsolationScope(); - - expect(scope['_getIsolationScope']()).toBe(globalIsolationScope); - - const customIsolationScope = new Scope(); - scope.isolationScope = customIsolationScope; - - expect(scope['_getIsolationScope']()).toBe(customIsolationScope); - }); - describe('prepareEvent', () => { it('works without any scope data', async () => { mockSdkInit(); @@ -205,6 +191,8 @@ describe('Unit | Scope', () => { integrations: [], }, scope, + undefined, + isolationScope, ); expect(eventProcessor1).toHaveBeenCalledTimes(1); diff --git a/packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts b/packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts new file mode 100644 index 000000000000..759206f761fc --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', +}); + +const globalScope = Sentry.getGlobalScope(); +const isolationScope = Sentry.getIsolationScope(); +const currentScope = Sentry.getCurrentScope(); + +globalScope.setExtra('aa', 'aa'); +isolationScope.setExtra('bb', 'bb'); +currentScope.setExtra('cc', 'cc'); + +Sentry.captureMessage('outer_before'); + +Sentry.withScope(scope => { + scope.setExtra('dd', 'dd'); + Sentry.captureMessage('inner'); +}); + +Sentry.captureMessage('outer_after'); diff --git a/packages/node-integration-tests/suites/public-api/scopes/initialScopes/test.ts b/packages/node-integration-tests/suites/public-api/scopes/initialScopes/test.ts new file mode 100644 index 000000000000..069285c452c7 --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/scopes/initialScopes/test.ts @@ -0,0 +1,31 @@ +import { TestEnv, assertSentryEvent } from '../../../../utils'; + +test('should apply scopes correctly', async () => { + const env = await TestEnv.init(__dirname); + const events = await env.getMultipleEnvelopeRequest({ count: 3 }); + + assertSentryEvent(events[0][2], { + message: 'outer_before', + extra: { + aa: 'aa', + bb: 'bb', + }, + }); + + assertSentryEvent(events[1][2], { + message: 'inner', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + }, + }); + + assertSentryEvent(events[2][2], { + message: 'outer_after', + extra: { + aa: 'aa', + bb: 'bb', + }, + }); +}); diff --git a/packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts b/packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts new file mode 100644 index 000000000000..7d78c276880b --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts @@ -0,0 +1,30 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', +}); + +const globalScope = Sentry.getGlobalScope(); +const isolationScope = Sentry.getIsolationScope(); +const currentScope = Sentry.getCurrentScope(); + +globalScope.setExtra('aa', 'aa'); +isolationScope.setExtra('bb', 'bb'); +currentScope.setExtra('cc', 'cc'); + +Sentry.captureMessage('outer_before'); + +Sentry.withScope(scope => { + Sentry.getIsolationScope().setExtra('dd', 'dd'); + scope.setExtra('ee', 'ee'); + Sentry.captureMessage('inner'); +}); + +Sentry.runWithAsyncContext(() => { + Sentry.getIsolationScope().setExtra('ff', 'ff'); + Sentry.getCurrentScope().setExtra('gg', 'gg'); + Sentry.captureMessage('inner_async_context'); +}); + +Sentry.captureMessage('outer_after'); diff --git a/packages/node-integration-tests/suites/public-api/scopes/isolationScope/test.ts b/packages/node-integration-tests/suites/public-api/scopes/isolationScope/test.ts new file mode 100644 index 000000000000..4288d59bb799 --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/scopes/isolationScope/test.ts @@ -0,0 +1,47 @@ +import { TestEnv, assertSentryEvent } from '../../../../utils'; + +test('should apply scopes correctly', async () => { + const env = await TestEnv.init(__dirname); + const events = await env.getMultipleEnvelopeRequest({ count: 4 }); + + assertSentryEvent(events[0][2], { + message: 'outer_before', + extra: { + aa: 'aa', + bb: 'bb', + }, + }); + + assertSentryEvent(events[1][2], { + message: 'inner', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + dd: 'dd', + ee: 'ee', + }, + }); + + assertSentryEvent(events[2][2], { + message: 'inner_async_context', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + dd: 'dd', + ff: 'ff', + gg: 'gg', + }, + }); + + assertSentryEvent(events[3][2], { + message: 'outer_after', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + dd: 'dd', + }, + }); +}); diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index af73f34df7bc..46b1d6d742d4 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -43,6 +43,8 @@ export { getCurrentHub, getClient, getCurrentScope, + getGlobalScope, + getIsolationScope, Hub, lastEventId, makeMain, diff --git a/packages/opentelemetry/src/custom/client.ts b/packages/opentelemetry/src/custom/client.ts index 44dd0cf80f4f..abb6ba30972d 100644 --- a/packages/opentelemetry/src/custom/client.ts +++ b/packages/opentelemetry/src/custom/client.ts @@ -67,7 +67,12 @@ export function wrapClientClass< * Extends the base `_prepareEvent` so that we can properly handle `captureContext`. * This uses `Scope.clone()`, which we need to replace with `OpenTelemetryScope.clone()` for this client. */ - protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + protected _prepareEvent( + event: Event, + hint: EventHint, + scope?: Scope, + isolationScope?: Scope, + ): PromiseLike { let actualScope = scope; // Remove `captureContext` hint and instead clone already here diff --git a/packages/replay/src/util/prepareReplayEvent.ts b/packages/replay/src/util/prepareReplayEvent.ts index 4505b5b86f41..6b104c87c573 100644 --- a/packages/replay/src/util/prepareReplayEvent.ts +++ b/packages/replay/src/util/prepareReplayEvent.ts @@ -1,4 +1,5 @@ import type { Scope } from '@sentry/core'; +import { getIsolationScope } from '@sentry/core'; import { prepareEvent } from '@sentry/core'; import type { IntegrationIndex } from '@sentry/core/build/types/integration'; import type { Client, EventHint, ReplayEvent } from '@sentry/types'; @@ -34,6 +35,7 @@ export async function prepareReplayEvent({ eventHint, scope, client, + getIsolationScope(), )) as ReplayEvent | null; // If e.g. a global event processor returned null diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index c8086fc5d69e..77f355e1e8af 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -31,6 +31,8 @@ export { getCurrentHub, getClient, getCurrentScope, + getGlobalScope, + getIsolationScope, getHubFromCarrier, makeMain, setContext, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 560f839c9fe3..b5d7d64a58a3 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -24,6 +24,8 @@ export { getCurrentHub, getClient, getCurrentScope, + getGlobalScope, + getIsolationScope, Hub, makeMain, Scope, diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index 8d4d47885d40..b911cca578d1 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -77,6 +77,12 @@ export interface Hub { /** Returns the scope of the top stack */ getScope(): Scope; + /** + * Get the currently active isolation scope. + * The isolation scope is used to isolate data between different hubs. + */ + getIsolationScope(): Scope; + /** * Captures an exception event and sends it to Sentry. * diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 76219f4faafa..cbc7f6a89d7c 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -43,6 +43,8 @@ export { getCurrentHub, getClient, getCurrentScope, + getGlobalScope, + getIsolationScope, Hub, lastEventId, makeMain,