diff --git a/packages/node-experimental/README.md b/packages/node-experimental/README.md index e23ee4f1817c..5058cace79ad 100644 --- a/packages/node-experimental/README.md +++ b/packages/node-experimental/README.md @@ -50,7 +50,39 @@ Currently, this SDK: * Will capture errors (same as @sentry/node) * Auto-instrument for performance - see below for which performance integrations are available. * Provide _some_ manual instrumentation APIs -* Sync OpenTelemetry Context with our Sentry Hub/Scope +* Sync OpenTelemetry Context with our Sentry Scope + +### Hub, Scope & Context + +node-experimental has no public concept of a Hub anymore. +Instead, you always interact with a Scope, which maps to an OpenTelemetry Context. +This means that the following common API is _not_ available: + +```js +const hub = Sentry.getCurrentHub(); +``` + +Instead, you can directly get the current scope: + +```js +const scope = Sentry.getCurrentScope(); +``` + +Additionally, there are some more utilities to work with: + +```js +// Get the currently active scope +const scope = Sentry.getCurrentScope(); +// Get the currently active root scope +// A root scope is either the global scope, OR the first forked scope, OR the scope of the root span +const rootScope = Sentry.getCurrentRootScope(); +// Create a new execution context - basically a wrapper for `context.with()` in OpenTelemetry +Sentry.withScope(scope => {}); +// Create a new execution context, which should be a root scope. This overwrites any previously set root scope +Sentry.withRootScope(rootScope => {}); +// Get the client of the SDK +const client = Sentry.getClient(); +``` ### Manual Instrumentation diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index 798a474702c1..70b4886edcc2 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -12,8 +12,17 @@ export { INTEGRATIONS as Integrations }; export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanceIntegrations'; export * as Handlers from './sdk/handlers'; export type { Span } from './types'; +export { getClient } from './sdk/client'; -export { startSpan, startInactiveSpan, getCurrentHub, getActiveSpan } from '@sentry/opentelemetry'; +export { + startSpan, + startInactiveSpan, + getActiveSpan, + getCurrentScope, + getCurrentRootScope, + withScope, + withRootScope, +} from '@sentry/opentelemetry'; export { makeNodeTransport, @@ -30,12 +39,10 @@ export { captureEvent, captureMessage, close, - configureScope, createTransport, extractTraceparentData, flush, getActiveTransaction, - Hub, lastEventId, makeMain, runWithAsyncContext, @@ -49,7 +56,6 @@ export { setUser, spanStatusfromHttpCode, trace, - withScope, captureCheckIn, withMonitor, } from '@sentry/node'; diff --git a/packages/node-experimental/src/sdk/client.ts b/packages/node-experimental/src/sdk/client.ts index 809d1fa49035..fd77fd417d10 100644 --- a/packages/node-experimental/src/sdk/client.ts +++ b/packages/node-experimental/src/sdk/client.ts @@ -1,5 +1,7 @@ import { NodeClient, SDK_VERSION } from '@sentry/node'; -import { wrapClientClass } from '@sentry/opentelemetry'; +import { getCurrentHub, wrapClientClass } from '@sentry/opentelemetry'; + +import type { NodeExperimentalClient as NodeExperimentalClientInterface } from '../types'; class NodeExperimentalBaseClient extends NodeClient { public constructor(options: ConstructorParameters[0]) { @@ -20,3 +22,10 @@ class NodeExperimentalBaseClient extends NodeClient { } export const NodeExperimentalClient = wrapClientClass(NodeExperimentalBaseClient); + +/** + * Get the currently active client (or undefined, if the SDK is not initialized). + */ +export function getClient(): NodeExperimentalClientInterface | undefined { + return getCurrentHub().getClient(); +} diff --git a/packages/node-experimental/test/integration/client.test.ts b/packages/node-experimental/test/integration/client.test.ts new file mode 100644 index 000000000000..ad1456827d64 --- /dev/null +++ b/packages/node-experimental/test/integration/client.test.ts @@ -0,0 +1,32 @@ +import { GLOBAL_OBJ } from '@sentry/utils'; + +import * as Sentry from '../../src'; +import { NodeExperimentalClient } from '../../src/sdk/client'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; + +describe('Integration | Client', () => { + describe('getClient', () => { + beforeEach(() => { + GLOBAL_OBJ.__SENTRY__ = { + extensions: {}, + hub: undefined, + globalEventProcessors: [], + logger: undefined, + }; + }); + + afterEach(() => { + cleanupOtel(); + }); + + test('it works with no client', () => { + expect(Sentry.getClient()).toBeUndefined(); + }); + + test('it works with a client', () => { + mockSdkInit(); + expect(Sentry.getClient()).toBeDefined(); + expect(Sentry.getClient()).toBeInstanceOf(NodeExperimentalClient); + }); + }); +}); diff --git a/packages/opentelemetry/src/constants.ts b/packages/opentelemetry/src/constants.ts index 36c8a36f886c..78a9cc70a01a 100644 --- a/packages/opentelemetry/src/constants.ts +++ b/packages/opentelemetry/src/constants.ts @@ -8,3 +8,9 @@ export const SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY = createContextKey('SENTRY_P /** Context Key to hold a Hub. */ export const SENTRY_HUB_CONTEXT_KEY = createContextKey('sentry_hub'); + +/** Context Key to hold a root scope. */ +export const SENTRY_ROOT_SCOPE_CONTEXT_KEY = createContextKey('sentry_root_scope'); + +/** Context Key to force setting of the root scope, even if one already exists. */ +export const SENTRY_FORCE_ROOT_SCOPE_CONTEXT_KEY = createContextKey('sentry_force_root_scope'); diff --git a/packages/opentelemetry/src/contextManager.ts b/packages/opentelemetry/src/contextManager.ts index ca9305dfea9b..48c0e1dc8f02 100644 --- a/packages/opentelemetry/src/contextManager.ts +++ b/packages/opentelemetry/src/contextManager.ts @@ -1,8 +1,15 @@ import type { Context, ContextManager } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; import type { Carrier, Hub } from '@sentry/core'; -import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './custom/hub'; -import { setHubOnContext } from './utils/contextData'; +import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier, isGlobalHub } from './custom/hub'; +import { + clearForceRootScopeOnContext, + getForceRootScopeFromContext, + setHubOnContext, + setRootScopeOnContext, +} from './utils/contextData'; +import { getActiveSpan } from './utils/getActiveSpan'; function createNewHub(parent: Hub | undefined): Hub { const carrier: Carrier = {}; @@ -48,7 +55,25 @@ export function wrapContextManagerClass void): void { + context.with(context.active(), () => { + const scope = getCurrentScope(); + callback(scope); + }); +} + +/** + * Creates a new root scope with and executes the given operation within. + * The scope is automatically removed once the operation + * finishes or throws. + */ +export function withRootScope(callback: (scope: Scope) => void): void { + context.with(setForceRootScopeOnContext(context.active()), () => { + const scope = getCurrentScope(); + callback(scope); + }); +} diff --git a/packages/opentelemetry/test/integration/scope.test.ts b/packages/opentelemetry/test/integration/scope.test.ts index c028e1893d7a..336109f83590 100644 --- a/packages/opentelemetry/test/integration/scope.test.ts +++ b/packages/opentelemetry/test/integration/scope.test.ts @@ -1,8 +1,9 @@ -import { captureException, setTag, withScope } from '@sentry/core'; +import { captureException, setTag } from '@sentry/core'; import { getCurrentHub, OpenTelemetryHub } from '../../src/custom/hub'; import { OpenTelemetryScope } from '../../src/custom/scope'; import { startSpan } from '../../src/trace'; +import { getCurrentRootScope, getCurrentScope, getGlobalScope, withRootScope, withScope } from '../../src/utils/scope'; import { getSpanScope } from '../../src/utils/spanData'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; import type { TestClientInterface } from '../helpers/TestClient'; @@ -12,6 +13,114 @@ describe('Integration | Scope', () => { cleanupOtel(); }); + test('withScope() & getCurrentScope() works', () => { + mockSdkInit({}); + + const globalScope = getCurrentScope() as OpenTelemetryScope; + expect(globalScope).toBeDefined(); + + globalScope.setTag('tag1', 'val1'); + + withScope(scope1 => { + expect(scope1).toBeDefined(); + expect(scope1).not.toBe(globalScope); + expect(getCurrentScope()).toBe(scope1); + + scope1.setTag('tag2', 'val2'); + + withScope(scope2 => { + expect(scope2).toBeDefined(); + + expect(scope2).not.toBe(scope1); + expect(getCurrentScope()).toBe(scope2); + + scope2.setTag('tag3', 'val3'); + + expect((scope2 as OpenTelemetryScope)['_tags']).toEqual({ tag1: 'val1', tag2: 'val2', tag3: 'val3' }); + }); + + expect((scope1 as OpenTelemetryScope)['_tags']).toEqual({ tag1: 'val1', tag2: 'val2' }); + }); + + globalScope.setTag('tag99', 'val99'); + + expect(getCurrentScope()).toBe(globalScope); + expect(globalScope['_tags']).toEqual({ tag1: 'val1', tag99: 'val99' }); + }); + + test('withRootScope() & getCurrentRootScope() works', async () => { + mockSdkInit({}); + + const globalScope = getCurrentScope(); + expect(globalScope).toBeDefined(); + + withScope(scope1 => { + expect(scope1).toBeDefined(); + expect(getCurrentRootScope()).toBe(scope1); + + withScope(scope2 => { + expect(scope2).toBeDefined(); + expect(getCurrentRootScope()).toBe(scope1); + + withRootScope(rootScope2 => { + expect(rootScope2).toBeDefined(); + expect(getCurrentRootScope()).toBe(rootScope2); + }); + }); + }); + + expect(getCurrentRootScope()).toBe(globalScope); + }); + + test('root scope is automatically set for root spans', async () => { + mockSdkInit({ enableTracing: true }); + + const globalScope = getCurrentScope(); + expect(globalScope).toBeDefined(); + + withScope(scope1 => { + expect(scope1).toBeDefined(); + expect(getCurrentRootScope()).toBe(scope1); + + startSpan({ name: 'root span' }, () => { + const scope2 = getCurrentScope(); + expect(scope2).not.toBe(scope1); + expect(getCurrentRootScope()).toBe(scope2); + + startSpan({ name: 'span' }, () => { + const scope3 = getCurrentScope(); + expect(scope3).not.toBe(scope2); + expect(getCurrentRootScope()).toBe(scope2); + }); + }); + }); + + expect(getCurrentRootScope()).toBe(globalScope); + }); + + test('getGlobalScope() works', () => { + mockSdkInit({}); + + const globalScope = getCurrentScope() as OpenTelemetryScope; + expect(globalScope).toBeDefined(); + + expect(getGlobalScope()).toBe(globalScope); + + withScope(() => { + expect(getGlobalScope()).toBe(globalScope); + + withScope(() => { + expect(getGlobalScope()).toBe(globalScope); + + withRootScope(() => { + expect(getGlobalScope()).toBe(globalScope); + }); + }); + }); + + expect(getGlobalScope()).toBe(globalScope); + }); + describe.each([ ['with tracing', true], ['without tracing', false],