Skip to content

feat(node-experimental): Replace getCurrentHub with getCurrentScope #9419

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion packages/node-experimental/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 10 additions & 4 deletions packages/node-experimental/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -30,12 +39,10 @@ export {
captureEvent,
captureMessage,
close,
configureScope,
createTransport,
extractTraceparentData,
flush,
getActiveTransaction,
Hub,
lastEventId,
makeMain,
runWithAsyncContext,
Expand All @@ -49,7 +56,6 @@ export {
setUser,
spanStatusfromHttpCode,
trace,
withScope,
captureCheckIn,
withMonitor,
} from '@sentry/node';
Expand Down
11 changes: 10 additions & 1 deletion packages/node-experimental/src/sdk/client.ts
Original file line number Diff line number Diff line change
@@ -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<typeof NodeClient>[0]) {
Expand All @@ -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<NodeExperimentalClientInterface>();
}
32 changes: 32 additions & 0 deletions packages/node-experimental/test/integration/client.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
6 changes: 6 additions & 0 deletions packages/opentelemetry/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
31 changes: 28 additions & 3 deletions packages/opentelemetry/src/contextManager.ts
Original file line number Diff line number Diff line change
@@ -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 = {};
Expand Down Expand Up @@ -48,7 +55,25 @@ export function wrapContextManagerClass<ContextManagerInstance extends ContextMa
const existingHub = getCurrentHub();
const newHub = createNewHub(existingHub);

return super.with(setHubOnContext(context, newHub), fn, thisArg, ...args);
const hadActiveSpanBefore = !!getActiveSpan();
const hasActiveSpan = !!trace.getSpan(context);
const isRootSpan = hasActiveSpan && !hadActiveSpanBefore;

const forceRootScope = getForceRootScopeFromContext(context);

let ctx = setHubOnContext(context, newHub);

// If this is the root of the execution context, we store the root scope for later reference
if (isGlobalHub(existingHub) || forceRootScope || isRootSpan) {
const scope = newHub.getScope();
ctx = setRootScopeOnContext(ctx, scope);

if (forceRootScope) {
ctx = clearForceRootScopeOnContext(ctx);
}
}

return super.with(ctx, fn, thisArg, ...args);
}
}

Expand Down
12 changes: 11 additions & 1 deletion packages/opentelemetry/src/custom/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ export function getCurrentHub(): Hub {
return getGlobalHub(registry);
}

/**
* Check if a given hub is the global hub.
*/
export function isGlobalHub(hub: Hub): boolean {
return hub === getGlobalHub();
}

/**
* Ensure the global hub is an OpenTelemetryHub.
*/
Expand Down Expand Up @@ -112,7 +119,10 @@ export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub(
}
}

function getGlobalHub(registry: Carrier = getMainCarrier()): Hub {
/**
* Get the global hub.
*/
export 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)) {
setHubOnCarrier(registry, new OpenTelemetryHub());
Expand Down
2 changes: 2 additions & 0 deletions packages/opentelemetry/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export {
spanHasStatus,
} from './utils/spanTypes';

export { getCurrentScope, getCurrentRootScope, getGlobalScope, withScope, withRootScope } from './utils/scope';

export { isSentryRequestSpan } from './utils/isSentryRequest';

export { getActiveSpan, getRootSpan } from './utils/getActiveSpan';
Expand Down
53 changes: 49 additions & 4 deletions packages/opentelemetry/src/utils/contextData.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { Context } from '@opentelemetry/api';
import type { Hub, PropagationContext } from '@sentry/types';
import type { Hub, PropagationContext, Scope } from '@sentry/types';

import { SENTRY_HUB_CONTEXT_KEY, SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY } from '../constants';
import {
SENTRY_FORCE_ROOT_SCOPE_CONTEXT_KEY,
SENTRY_HUB_CONTEXT_KEY,
SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY,
SENTRY_ROOT_SCOPE_CONTEXT_KEY,
} from '../constants';

/**
* Try to get the Propagation Context from the given OTEL context.
Expand All @@ -20,17 +25,57 @@ export function setPropagationContextOnContext(context: Context, propagationCont
}

/**
* Try to get the Hub from the given OTEL context.
* Try to get the hub from the given OTEL context.
* This requires a Context Manager that was wrapped with getWrappedContextManager.
*/
export function getHubFromContext(context: Context): Hub | undefined {
return context.getValue(SENTRY_HUB_CONTEXT_KEY) as Hub | undefined;
}

/**
* Set a Hub on an OTEL context..
* Set a hub on an OTEL context.
* This will return a forked context with the Propagation Context set.
*/
export function setHubOnContext(context: Context, hub: Hub): Context {
return context.setValue(SENTRY_HUB_CONTEXT_KEY, hub);
}

/**
* Try to get the root scope from the given OTEL context.
* This requires a Context Manager that was wrapped with getWrappedContextManager.
*/
export function getRootScopeFromContext(context: Context): Scope | undefined {
return context.getValue(SENTRY_ROOT_SCOPE_CONTEXT_KEY) as Scope | undefined;
}

/**
* Set a root scope on an OTEL context.
* This will return a forked context with the Propagation Context set.
*/
export function setRootScopeOnContext(context: Context, scope: Scope): Context {
return context.setValue(SENTRY_ROOT_SCOPE_CONTEXT_KEY, scope);
}

/**
* If this context should be forced to generate a new root scope.
* This requires a Context Manager that was wrapped with getWrappedContextManager.
*/
export function getForceRootScopeFromContext(context: Context): boolean {
return !!context.getValue(SENTRY_FORCE_ROOT_SCOPE_CONTEXT_KEY);
}

/**
* Set a flag on the context to ensure we set the new scope as root scope.
* This will return a forked context with the Propagation Context set.
*/
export function setForceRootScopeOnContext(context: Context, force = true): Context {
return context.setValue(SENTRY_FORCE_ROOT_SCOPE_CONTEXT_KEY, force);
}

/**
* Clear the force root scope flag on the context.
* This will return a forked context with the Propagation Context set.
*/
export function clearForceRootScopeOnContext(context: Context): Context {
return context.deleteValue(SENTRY_FORCE_ROOT_SCOPE_CONTEXT_KEY);
}
53 changes: 53 additions & 0 deletions packages/opentelemetry/src/utils/scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { context } from '@opentelemetry/api';
import type { Scope } from '@sentry/types';

import { getCurrentHub, getGlobalHub } from '../custom/hub';
import { getRootScopeFromContext, setForceRootScopeOnContext } from './contextData';

/**
* Get the currently active scope.
*/
export function getCurrentScope(): Scope {
return getCurrentHub().getScope();
}

/**
* Get the currently active root scope,
* or fall back to the active scope if none is available.
*/
export function getCurrentRootScope(): Scope {
const rootScope = getRootScopeFromContext(context.active());

return rootScope || getCurrentScope();
}

/**
* Get the global scope.
*/
export function getGlobalScope(): Scope {
return getGlobalHub().getScope();
}

/**
* Creates a new scope with and executes the given operation within.
* The scope is automatically removed once the operation
* finishes or throws.
*/
export function withScope(callback: (scope: Scope) => 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);
});
}
Loading