Skip to content

Commit 264b4f7

Browse files
andreiborzaclaude
andauthored
fix(node): Register tracer provider when OTel API global pre-exists with a different version (#22374)
## What When the OTel API global registry (`Symbol.for('opentelemetry.js.api.1')`) pre-exists with a different `@opentelemetry/api` version, the SDK now replaces it and registers its tracer provider instead of silently disabling tracing. - Retry registration after replacing the registry, only when its `trace` slot is empty - Debug warning when the registry is replaced, pointing to `skipOpenTelemetrySetup` - Applies to both the default `SentryTracerProvider` and the `BasicTracerProvider` path ## Why Host runtimes like Neon Functions pre-create the registry with their own api version. OTel's `registerGlobal` requires an exact version match, so every registration is rejected and all spans (including `Sentry.startSpan()`) are non-recording, with no signal outside debug builds. Node flavor of the Deno issue fixed in #19723. The empty-`trace`-slot condition keeps existing behavior when a real provider is already registered (second `Sentry.init()`, user-managed OTel): those still back off as before, so no working setup gets clobbered. Closes: #22338 --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 237956e commit 264b4f7

2 files changed

Lines changed: 127 additions & 2 deletions

File tree

packages/node/src/sdk/initOtel.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { TracerProvider } from '@opentelemetry/api';
12
import { context, propagation, trace } from '@opentelemetry/api';
23
import type { SpanProcessor } from '@opentelemetry/sdk-trace-base';
34
import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
@@ -26,6 +27,61 @@ import { getOpenTelemetryInstrumentationToPreload } from '../integrations/tracin
2627
// About 277h - this must fit into new Array(len)!
2728
const MAX_MAX_SPAN_WAIT_DURATION = 1_000_000;
2829

30+
// The global registry of @opentelemetry/api 1.x, shared across all copies of the package
31+
const OTEL_API_GLOBAL_KEY = Symbol.for('opentelemetry.js.api.1');
32+
33+
/**
34+
* Registers the given tracer provider as the global tracer provider, recreating the OpenTelemetry
35+
* API registry when it pre-exists with a different `@opentelemetry/api` version.
36+
*
37+
* Some host runtimes (e.g. Neon Functions) pre-create the registry with their own API version.
38+
* `registerGlobal` requires an exact version match, so every registration through the SDK's copy
39+
* of `@opentelemetry/api` is rejected and tracing is silently disabled
40+
* (https://github.com/getsentry/sentry-javascript/issues/22338). This case is identified by a
41+
* failed registration with an empty `trace` slot: `registerGlobal` checks the slot before the
42+
* version, so an empty slot means the version gate rejected us and recreating the registry
43+
* clobbers no other tracer provider. If the slot is occupied (another provider registered first,
44+
* e.g. a second `Sentry.init()` call), the registry is left untouched and registration fails.
45+
*
46+
* Slots Sentry does not claim itself (e.g. `diag`, `metrics`) are carried over into the recreated
47+
* registry: reads resolve via a semver-compatibility check rather than the exact-match write gate,
48+
* so they keep working for the copy that registered them. `propagation` and `context` are not
49+
* carried over because Sentry registers its own right after this.
50+
*/
51+
function registerGlobalTracerProvider(provider: TracerProvider): boolean {
52+
if (trace.setGlobalTracerProvider(provider)) {
53+
return true;
54+
}
55+
56+
// @opentelemetry/api stores the registry under a `Symbol.for` key that no public type
57+
// describes, so `typeof globalThis` can only be narrowed to it by casting through `unknown`.
58+
const otelGlobal = globalThis as unknown as Record<symbol, Record<string, unknown> | undefined>;
59+
const registry = otelGlobal[OTEL_API_GLOBAL_KEY];
60+
if (registry && !registry.trace) {
61+
DEBUG_BUILD &&
62+
coreDebug.warn(
63+
'Replaced a pre-existing OpenTelemetry API registry that was created by a different @opentelemetry/api version and would have blocked tracing. If you want to manage OpenTelemetry yourself, set `skipOpenTelemetrySetup: true` in `Sentry.init()`.',
64+
);
65+
otelGlobal[OTEL_API_GLOBAL_KEY] = undefined;
66+
67+
if (!trace.setGlobalTracerProvider(provider)) {
68+
return false;
69+
}
70+
71+
// The cast is needed because TS still has the slot narrowed to `undefined` from the reset
72+
// above and cannot know the registration call just recreated the registry.
73+
const recreatedRegistry = otelGlobal[OTEL_API_GLOBAL_KEY] as Record<string, unknown> | undefined;
74+
if (recreatedRegistry) {
75+
const { propagation: _propagation, context: _context, ...carriedOverSlots } = registry;
76+
otelGlobal[OTEL_API_GLOBAL_KEY] = { ...carriedOverSlots, ...recreatedRegistry };
77+
}
78+
79+
return true;
80+
}
81+
82+
return false;
83+
}
84+
2985
interface AdditionalOpenTelemetryOptions {
3086
/** Additional SpanProcessor instances that should be used. */
3187
spanProcessors?: SpanProcessor[];
@@ -118,7 +174,7 @@ export function setupOtel(
118174
});
119175

120176
// Register as globals
121-
trace.setGlobalTracerProvider(provider);
177+
registerGlobalTracerProvider(provider);
122178
propagation.setGlobalPropagator(new SentryPropagator());
123179

124180
const ctxManager = new SentryContextManager();
@@ -132,7 +188,7 @@ function setupSentryTracerProvider(
132188
): [SentryTracerProvider | undefined, AsyncLocalStorageLookup | undefined] {
133189
const provider = new SentryTracerProvider({ resource: getSentryResource('node') });
134190

135-
if (!trace.setGlobalTracerProvider(provider)) {
191+
if (!registerGlobalTracerProvider(provider)) {
136192
DEBUG_BUILD &&
137193
coreDebug.warn(
138194
'Could not register SentryTracerProvider because another OpenTelemetry tracer provider is already registered.',

packages/node/test/sdk/init.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ declare var global: any;
1414

1515
const PUBLIC_DSN = 'https://username@domain/123';
1616

17+
const OTEL_API_GLOBAL_KEY = Symbol.for('opentelemetry.js.api.1');
18+
1719
class MockIntegration implements Integration {
1820
public name: string;
1921
public setupOnce: Mock = vi.fn();
@@ -231,6 +233,73 @@ describe('init()', () => {
231233
expect(client?.traceProvider).toBeInstanceOf(BasicTracerProvider);
232234
});
233235

236+
it('recreates the OTel API registry when it pre-exists with a different @opentelemetry/api version', () => {
237+
// Simulate a host runtime (e.g. Neon Functions) pre-creating the registry with its own api version
238+
global[OTEL_API_GLOBAL_KEY] = { version: '0.0.1' };
239+
240+
init({ dsn: PUBLIC_DSN });
241+
242+
const client = getClient<NodeClient>();
243+
const registry = global[OTEL_API_GLOBAL_KEY];
244+
245+
expect(client?.traceProvider).toBeInstanceOf(SentryOpentelemetry.SentryTracerProvider);
246+
expect(registry?.version).not.toBe('0.0.1');
247+
expect(registry?.trace).toBeDefined();
248+
});
249+
250+
it('recreates a version-mismatched OTel API registry also for the OpenTelemetry SDK tracer provider', () => {
251+
global[OTEL_API_GLOBAL_KEY] = { version: '0.0.1' };
252+
253+
init({ dsn: PUBLIC_DSN, openTelemetryBasicTracerProvider: true });
254+
255+
const client = getClient<NodeClient>();
256+
const registry = global[OTEL_API_GLOBAL_KEY];
257+
258+
expect(client?.traceProvider).toBeInstanceOf(BasicTracerProvider);
259+
expect(registry?.version).not.toBe('0.0.1');
260+
expect(registry?.trace).toBeDefined();
261+
});
262+
263+
it('carries non-Sentry slots of a version-mismatched OTel API registry over into the recreated one', () => {
264+
// Must be a complete DiagLogger: once carried over, the SDK's api copy resolves it and
265+
// calls it for its own diag output.
266+
const diagLogger = { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn(), verbose: vi.fn() };
267+
const meterProvider = { getMeter: vi.fn() };
268+
const propagator = { inject: vi.fn() };
269+
global[OTEL_API_GLOBAL_KEY] = {
270+
version: '0.0.1',
271+
diag: diagLogger,
272+
metrics: meterProvider,
273+
propagation: propagator,
274+
};
275+
276+
init({ dsn: PUBLIC_DSN });
277+
278+
const registry = global[OTEL_API_GLOBAL_KEY];
279+
280+
expect(registry?.trace).toBeDefined();
281+
expect(registry?.diag).toBe(diagLogger);
282+
expect(registry?.metrics).toBe(meterProvider);
283+
// propagation is claimed by Sentry's own propagator, not carried over
284+
expect(registry?.propagation).not.toBe(propagator);
285+
});
286+
287+
it('does not recreate the OTel API registry when another tracer provider is already registered', () => {
288+
const existingProvider = { getTracer: vi.fn() };
289+
const existingRegistry = { version: '0.0.1', trace: existingProvider };
290+
global[OTEL_API_GLOBAL_KEY] = existingRegistry;
291+
292+
init({ dsn: PUBLIC_DSN });
293+
294+
const client = getClient<NodeClient>();
295+
296+
expect(client?.traceProvider).not.toBeDefined();
297+
expect(global[OTEL_API_GLOBAL_KEY]).toBe(existingRegistry);
298+
expect(existingRegistry.trace).toBe(existingProvider);
299+
300+
global[OTEL_API_GLOBAL_KEY] = undefined;
301+
});
302+
234303
it('does not mark SentryTracerProvider as set up when global registration fails', () => {
235304
// Simulate another OpenTelemetry tracer provider already being registered.
236305
const setGlobalSpy = vi.spyOn(trace, 'setGlobalTracerProvider').mockReturnValue(false);

0 commit comments

Comments
 (0)