From ddab6e02fa77203bd4410c5d21f7328964062918 Mon Sep 17 00:00:00 2001 From: cod1k Date: Wed, 13 Aug 2025 09:57:31 +0300 Subject: [PATCH 1/5] Enhance `setupOpenTelemetryTracer` to chain existing TracerProvider and improve SentryCloudflareTracer integration. --- .../cloudflare/src/opentelemetry/tracer.ts | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/cloudflare/src/opentelemetry/tracer.ts b/packages/cloudflare/src/opentelemetry/tracer.ts index a180346f7cce..82e28f08a583 100644 --- a/packages/cloudflare/src/opentelemetry/tracer.ts +++ b/packages/cloudflare/src/opentelemetry/tracer.ts @@ -7,16 +7,20 @@ import { startInactiveSpan, startSpanManual } from '@sentry/core'; * This is not perfect but handles easy/common use cases. */ export function setupOpenTelemetryTracer(): void { - trace.setGlobalTracerProvider(new SentryCloudflareTraceProvider()); + const current = trace.getTracerProvider(); + trace.setGlobalTracerProvider(new SentryCloudflareTraceProvider(current)); } class SentryCloudflareTraceProvider implements TracerProvider { private readonly _tracers: Map = new Map(); + public constructor(private readonly _provider: TracerProvider) {} + public getTracer(name: string, version?: string, options?: { schemaUrl?: string }): Tracer { const key = `${name}@${version || ''}:${options?.schemaUrl || ''}`; if (!this._tracers.has(key)) { - this._tracers.set(key, new SentryCloudflareTracer()); + const tracer = this._provider.getTracer(key, version, options); + this._tracers.set(key, new SentryCloudflareTracer(tracer)); } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -25,15 +29,20 @@ class SentryCloudflareTraceProvider implements TracerProvider { } class SentryCloudflareTracer implements Tracer { + public constructor(private readonly _tracer: Tracer) {} public startSpan(name: string, options?: SpanOptions): Span { - return startInactiveSpan({ - name, - ...options, - attributes: { - ...options?.attributes, - 'sentry.cloudflare_tracer': true, - }, - }); + try { + return startInactiveSpan({ + name, + ...options, + attributes: { + ...options?.attributes, + 'sentry.cloudflare_tracer': true, + }, + }); + } finally { + this._tracer.startSpan(name, options); + } } /** From 53f3af161d7f2c247ec55343af6a98f0c7ed9caa Mon Sep 17 00:00:00 2001 From: cod1k Date: Wed, 13 Aug 2025 11:22:28 +0300 Subject: [PATCH 2/5] Refactor `SentryCloudflareTracer` to enhance span proxying and ensure seamless integration with underlying tracer. --- .../cloudflare/src/opentelemetry/tracer.ts | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/cloudflare/src/opentelemetry/tracer.ts b/packages/cloudflare/src/opentelemetry/tracer.ts index 82e28f08a583..1b063cb75b21 100644 --- a/packages/cloudflare/src/opentelemetry/tracer.ts +++ b/packages/cloudflare/src/opentelemetry/tracer.ts @@ -31,18 +31,36 @@ class SentryCloudflareTraceProvider implements TracerProvider { class SentryCloudflareTracer implements Tracer { public constructor(private readonly _tracer: Tracer) {} public startSpan(name: string, options?: SpanOptions): Span { - try { - return startInactiveSpan({ - name, - ...options, - attributes: { - ...options?.attributes, - 'sentry.cloudflare_tracer': true, - }, - }); - } finally { - this._tracer.startSpan(name, options); - } + const topSpan = this._tracer.startSpan(name, options); + const sentrySpan = startInactiveSpan({ + name, + ...options, + attributes: { + ...options?.attributes, + 'sentry.cloudflare_tracer': true, + }, + }); + return new Proxy(sentrySpan, { + get: (target, p) => { + const propertyValue = Reflect.get(target, p); + if (typeof propertyValue === 'function') { + const proxyTo = Reflect.get(topSpan, p); + if (typeof proxyTo !== 'function') { + return propertyValue; + } + return new Proxy(propertyValue, { + apply: (target, thisArg, argArray) => { + try { + Reflect.apply(proxyTo, topSpan, argArray); + } catch (e) { + // + } + return Reflect.apply(target, thisArg, argArray); + }, + }); + } + }, + }); } /** From 7d7d3b9d3c5d7741527e2cd700736041c9a78553 Mon Sep 17 00:00:00 2001 From: cod1k Date: Wed, 13 Aug 2025 12:34:48 +0300 Subject: [PATCH 3/5] Improve `setupOpenTelemetryTracer` to support `ProxyTracerProvider` and refactor span proxy logic in `SentryCloudflareTraceProvider`. --- .../cloudflare/src/opentelemetry/tracer.ts | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/cloudflare/src/opentelemetry/tracer.ts b/packages/cloudflare/src/opentelemetry/tracer.ts index 1b063cb75b21..f2a2d122bac3 100644 --- a/packages/cloudflare/src/opentelemetry/tracer.ts +++ b/packages/cloudflare/src/opentelemetry/tracer.ts @@ -1,5 +1,5 @@ import type { Context, Span, SpanOptions, Tracer, TracerProvider } from '@opentelemetry/api'; -import { trace } from '@opentelemetry/api'; +import { ProxyTracerProvider, trace } from '@opentelemetry/api'; import { startInactiveSpan, startSpanManual } from '@sentry/core'; /** @@ -8,7 +8,8 @@ import { startInactiveSpan, startSpanManual } from '@sentry/core'; */ export function setupOpenTelemetryTracer(): void { const current = trace.getTracerProvider(); - trace.setGlobalTracerProvider(new SentryCloudflareTraceProvider(current)); + const delegate = current instanceof ProxyTracerProvider ? current.getDelegate() : current; + trace.setGlobalTracerProvider(new SentryCloudflareTraceProvider(delegate)); } class SentryCloudflareTraceProvider implements TracerProvider { @@ -43,22 +44,23 @@ class SentryCloudflareTracer implements Tracer { return new Proxy(sentrySpan, { get: (target, p) => { const propertyValue = Reflect.get(target, p); - if (typeof propertyValue === 'function') { - const proxyTo = Reflect.get(topSpan, p); - if (typeof proxyTo !== 'function') { - return propertyValue; - } - return new Proxy(propertyValue, { - apply: (target, thisArg, argArray) => { - try { - Reflect.apply(proxyTo, topSpan, argArray); - } catch (e) { - // - } - return Reflect.apply(target, thisArg, argArray); - }, - }); + if (typeof propertyValue !== 'function') { + return propertyValue; } + const proxyTo = Reflect.get(topSpan, p); + if (typeof proxyTo !== 'function') { + return propertyValue; + } + return new Proxy(propertyValue, { + apply: (target, thisArg, argArray) => { + try { + Reflect.apply(proxyTo, topSpan, argArray); + } catch (e) { + // + } + return Reflect.apply(target, thisArg, argArray); + }, + }); }, }); } From 0d67dbc72fa0620a251128206fea3bd5e4a65d29 Mon Sep 17 00:00:00 2001 From: cod1k Date: Wed, 13 Aug 2025 15:47:25 +0300 Subject: [PATCH 4/5] Expand `setupOpenTelemetryTracer` tests and enhance `SentryCloudflareTracer` error handling for optional tracer usage. --- .../cloudflare/src/opentelemetry/tracer.ts | 34 +++++++---- .../cloudflare/test/opentelemetry.test.ts | 56 ++++++++++++++++++- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/packages/cloudflare/src/opentelemetry/tracer.ts b/packages/cloudflare/src/opentelemetry/tracer.ts index f2a2d122bac3..ff0996ca6c3b 100644 --- a/packages/cloudflare/src/opentelemetry/tracer.ts +++ b/packages/cloudflare/src/opentelemetry/tracer.ts @@ -1,5 +1,5 @@ -import type { Context, Span, SpanOptions, Tracer, TracerProvider } from '@opentelemetry/api'; -import { ProxyTracerProvider, trace } from '@opentelemetry/api'; +import type { Context, ProxyTracerProvider, Span, SpanOptions, Tracer, TracerProvider } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; import { startInactiveSpan, startSpanManual } from '@sentry/core'; /** @@ -7,20 +7,23 @@ import { startInactiveSpan, startSpanManual } from '@sentry/core'; * This is not perfect but handles easy/common use cases. */ export function setupOpenTelemetryTracer(): void { - const current = trace.getTracerProvider(); - const delegate = current instanceof ProxyTracerProvider ? current.getDelegate() : current; - trace.setGlobalTracerProvider(new SentryCloudflareTraceProvider(delegate)); + const result = trace.setGlobalTracerProvider(new SentryCloudflareTraceProvider()); + if (result) { + return; + } + const current = trace.getTracerProvider() as ProxyTracerProvider; + current.setDelegate(new SentryCloudflareTraceProvider(current.getDelegate())); } class SentryCloudflareTraceProvider implements TracerProvider { private readonly _tracers: Map = new Map(); - public constructor(private readonly _provider: TracerProvider) {} + public constructor(private readonly _provider?: TracerProvider) {} public getTracer(name: string, version?: string, options?: { schemaUrl?: string }): Tracer { const key = `${name}@${version || ''}:${options?.schemaUrl || ''}`; if (!this._tracers.has(key)) { - const tracer = this._provider.getTracer(key, version, options); + const tracer = this._provider?.getTracer?.(key, version, options); this._tracers.set(key, new SentryCloudflareTracer(tracer)); } @@ -30,9 +33,9 @@ class SentryCloudflareTraceProvider implements TracerProvider { } class SentryCloudflareTracer implements Tracer { - public constructor(private readonly _tracer: Tracer) {} + public constructor(private readonly _tracer?: Tracer) {} public startSpan(name: string, options?: SpanOptions): Span { - const topSpan = this._tracer.startSpan(name, options); + const topSpan = this._tracer?.startSpan?.(name, options); const sentrySpan = startInactiveSpan({ name, ...options, @@ -41,7 +44,18 @@ class SentryCloudflareTracer implements Tracer { 'sentry.cloudflare_tracer': true, }, }); + if (!topSpan) { + return sentrySpan; + } return new Proxy(sentrySpan, { + set: (target, p, newValue, receiver) => { + try { + Reflect.set(topSpan, p, newValue, receiver); + } catch { + // + } + return Reflect.set(target, p, newValue); + }, get: (target, p) => { const propertyValue = Reflect.get(target, p); if (typeof propertyValue !== 'function') { @@ -55,7 +69,7 @@ class SentryCloudflareTracer implements Tracer { apply: (target, thisArg, argArray) => { try { Reflect.apply(proxyTo, topSpan, argArray); - } catch (e) { + } catch { // } return Reflect.apply(target, thisArg, argArray); diff --git a/packages/cloudflare/test/opentelemetry.test.ts b/packages/cloudflare/test/opentelemetry.test.ts index f918afff90cc..5961b94baa70 100644 --- a/packages/cloudflare/test/opentelemetry.test.ts +++ b/packages/cloudflare/test/opentelemetry.test.ts @@ -1,7 +1,7 @@ import { trace } from '@opentelemetry/api'; import type { TransactionEvent } from '@sentry/core'; import { startSpan } from '@sentry/core'; -import { beforeEach, describe, expect, test } from 'vitest'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import { init } from '../src/sdk'; import { resetSdk } from './testUtils'; @@ -132,6 +132,60 @@ describe('opentelemetry compatibility', () => { 'sentry.source': 'custom', }); + expect(transactionEvent?.spans).toEqual([ + expect.objectContaining({ + description: 'otel span', + data: { + 'sentry.cloudflare_tracer': true, + 'sentry.origin': 'manual', + }, + }), + ]); + }); + test('Ensure that sentry spans works over other opentelemetry implementations', async () => { + const transactionEvents: TransactionEvent[] = []; + const end = vi.fn(); + const _startSpan = vi.fn().mockImplementation(() => ({ end })); + + const getTracer = vi.fn().mockImplementation(() => ({ + startSpan: _startSpan, + })); + trace.setGlobalTracerProvider({ + getTracer, + }); + + const client = init({ + dsn: 'https://username@domain/123', + tracesSampleRate: 1, + beforeSendTransaction: event => { + transactionEvents.push(event); + return null; + }, + }); + + const tracer = trace.getTracer('test'); + + expect(getTracer).toBeCalledWith('test@:', undefined, undefined); + startSpan({ name: 'sentry span' }, () => { + const span = tracer.startSpan('otel span'); + span.end(); + }); + expect(_startSpan).toBeCalledWith('otel span', undefined); + expect(end).toBeCalled(); + + await client!.flush(); + + expect(transactionEvents).toHaveLength(1); + const [transactionEvent] = transactionEvents; + + expect(transactionEvent?.spans?.length).toBe(1); + expect(transactionEvent?.transaction).toBe('sentry span'); + expect(transactionEvent?.contexts?.trace?.data).toEqual({ + 'sentry.origin': 'manual', + 'sentry.sample_rate': 1, + 'sentry.source': 'custom', + }); + expect(transactionEvent?.spans).toEqual([ expect.objectContaining({ description: 'otel span', From b2ac2266ea11c986bc2dbc08df0f5f9af9385173 Mon Sep 17 00:00:00 2001 From: cod1k Date: Wed, 13 Aug 2025 16:11:13 +0300 Subject: [PATCH 5/5] Refactor `SentryCloudflareTracer` span proxying to improve performance and prevent redundant proxy creation. --- packages/cloudflare/src/opentelemetry/tracer.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/cloudflare/src/opentelemetry/tracer.ts b/packages/cloudflare/src/opentelemetry/tracer.ts index ff0996ca6c3b..7a1cf89f3fef 100644 --- a/packages/cloudflare/src/opentelemetry/tracer.ts +++ b/packages/cloudflare/src/opentelemetry/tracer.ts @@ -47,14 +47,15 @@ class SentryCloudflareTracer implements Tracer { if (!topSpan) { return sentrySpan; } + const _proxied = new WeakMap(); return new Proxy(sentrySpan, { set: (target, p, newValue, receiver) => { try { - Reflect.set(topSpan, p, newValue, receiver); + Reflect.set(topSpan, p, newValue); } catch { // } - return Reflect.set(target, p, newValue); + return Reflect.set(target, p, newValue, receiver); }, get: (target, p) => { const propertyValue = Reflect.get(target, p); @@ -65,7 +66,10 @@ class SentryCloudflareTracer implements Tracer { if (typeof proxyTo !== 'function') { return propertyValue; } - return new Proxy(propertyValue, { + if (_proxied.has(propertyValue)) { + return _proxied.get(propertyValue); + } + const proxy = new Proxy(propertyValue, { apply: (target, thisArg, argArray) => { try { Reflect.apply(proxyTo, topSpan, argArray); @@ -75,6 +79,8 @@ class SentryCloudflareTracer implements Tracer { return Reflect.apply(target, thisArg, argArray); }, }); + _proxied.set(propertyValue, proxy); + return proxy; }, }); }