diff --git a/packages/cloudflare/src/opentelemetry/tracer.ts b/packages/cloudflare/src/opentelemetry/tracer.ts index a180346f7cce..7a1cf89f3fef 100644 --- a/packages/cloudflare/src/opentelemetry/tracer.ts +++ b/packages/cloudflare/src/opentelemetry/tracer.ts @@ -1,4 +1,4 @@ -import type { Context, Span, SpanOptions, Tracer, TracerProvider } 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,16 +7,24 @@ 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 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 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,8 +33,10 @@ class SentryCloudflareTraceProvider implements TracerProvider { } class SentryCloudflareTracer implements Tracer { + public constructor(private readonly _tracer?: Tracer) {} public startSpan(name: string, options?: SpanOptions): Span { - return startInactiveSpan({ + const topSpan = this._tracer?.startSpan?.(name, options); + const sentrySpan = startInactiveSpan({ name, ...options, attributes: { @@ -34,6 +44,45 @@ class SentryCloudflareTracer implements Tracer { 'sentry.cloudflare_tracer': true, }, }); + if (!topSpan) { + return sentrySpan; + } + const _proxied = new WeakMap(); + return new Proxy(sentrySpan, { + set: (target, p, newValue, receiver) => { + try { + Reflect.set(topSpan, p, newValue); + } catch { + // + } + return Reflect.set(target, p, newValue, receiver); + }, + get: (target, p) => { + const propertyValue = Reflect.get(target, p); + if (typeof propertyValue !== 'function') { + return propertyValue; + } + const proxyTo = Reflect.get(topSpan, p); + if (typeof proxyTo !== 'function') { + return propertyValue; + } + if (_proxied.has(propertyValue)) { + return _proxied.get(propertyValue); + } + const proxy = new Proxy(propertyValue, { + apply: (target, thisArg, argArray) => { + try { + Reflect.apply(proxyTo, topSpan, argArray); + } catch { + // + } + return Reflect.apply(target, thisArg, argArray); + }, + }); + _proxied.set(propertyValue, proxy); + return proxy; + }, + }); } /** 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',