Skip to content

Commit 57ef2f2

Browse files
andreiborzaclaude
andcommitted
fix(node): Register tracer provider when OTel API global pre-exists with a different version
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 1c9dabf commit 57ef2f2

2 files changed

Lines changed: 83 additions & 2 deletions

File tree

packages/node/src/sdk/initOtel.ts

Lines changed: 38 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,41 @@ 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+
function registerGlobalTracerProvider(provider: TracerProvider): boolean {
47+
if (trace.setGlobalTracerProvider(provider)) {
48+
return true;
49+
}
50+
51+
const otelGlobal = globalThis as unknown as Record<symbol, { trace?: unknown } | undefined>;
52+
const registry = otelGlobal[OTEL_API_GLOBAL_KEY];
53+
if (registry && !registry.trace) {
54+
DEBUG_BUILD &&
55+
coreDebug.warn(
56+
'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()`.',
57+
);
58+
otelGlobal[OTEL_API_GLOBAL_KEY] = undefined;
59+
return trace.setGlobalTracerProvider(provider);
60+
}
61+
62+
return false;
63+
}
64+
2965
interface AdditionalOpenTelemetryOptions {
3066
/** Additional SpanProcessor instances that should be used. */
3167
spanProcessors?: SpanProcessor[];
@@ -118,7 +154,7 @@ export function setupOtel(
118154
});
119155

120156
// Register as globals
121-
trace.setGlobalTracerProvider(provider);
157+
registerGlobalTracerProvider(provider);
122158
propagation.setGlobalPropagator(new SentryPropagator());
123159

124160
const ctxManager = new SentryContextManager();
@@ -132,7 +168,7 @@ function setupSentryTracerProvider(
132168
): [SentryTracerProvider | undefined, AsyncLocalStorageLookup | undefined] {
133169
const provider = new SentryTracerProvider({ resource: getSentryResource('node') });
134170

135-
if (!trace.setGlobalTracerProvider(provider)) {
171+
if (!registerGlobalTracerProvider(provider)) {
136172
DEBUG_BUILD &&
137173
coreDebug.warn(
138174
'Could not register SentryTracerProvider because another OpenTelemetry tracer provider is already registered.',

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

Lines changed: 45 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,49 @@ 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('does not recreate the OTel API registry when another tracer provider is already registered', () => {
264+
const existingProvider = { getTracer: vi.fn() };
265+
const existingRegistry = { version: '0.0.1', trace: existingProvider };
266+
global[OTEL_API_GLOBAL_KEY] = existingRegistry;
267+
268+
init({ dsn: PUBLIC_DSN });
269+
270+
const client = getClient<NodeClient>();
271+
272+
expect(client?.traceProvider).not.toBeDefined();
273+
expect(global[OTEL_API_GLOBAL_KEY]).toBe(existingRegistry);
274+
expect(existingRegistry.trace).toBe(existingProvider);
275+
276+
global[OTEL_API_GLOBAL_KEY] = undefined;
277+
});
278+
234279
it('does not mark SentryTracerProvider as set up when global registration fails', () => {
235280
// Simulate another OpenTelemetry tracer provider already being registered.
236281
const setGlobalSpy = vi.spyOn(trace, 'setGlobalTracerProvider').mockReturnValue(false);

0 commit comments

Comments
 (0)