Skip to content

Commit 29f9e8e

Browse files
committed
add compatibility & tests
1 parent 729f388 commit 29f9e8e

File tree

6 files changed

+208
-4
lines changed

6 files changed

+208
-4
lines changed

packages/cloudflare/src/client.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ interface BaseCloudflareOptions {
3636
* @default true
3737
*/
3838
enableDedupe?: boolean;
39+
40+
/**
41+
* The Cloudflare SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility
42+
* via a custom trace provider.
43+
* This ensures that any spans emitted via `@opentelemetry/api` will be captured by Sentry.
44+
* HOWEVER, big caveat: This does not handle custom context handling, it will always work off the current scope.
45+
* This should be good enough for many, but not all integrations.
46+
*
47+
* If you want to opt-out of setting up the OpenTelemetry compatibility tracer, set this to `true`.
48+
*
49+
* @default false
50+
*/
51+
skipOpenTelemetrySetup?: boolean;
3952
}
4053

4154
/**

packages/cloudflare/src/opentelemetry/tracer.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ class SentryCloudflareTraceProvider implements TracerProvider {
2626

2727
class SentryCloudflareTracer implements Tracer {
2828
public startSpan(name: string, options?: SpanOptions): Span {
29-
return startInactiveSpan({ name, ...options });
29+
return startInactiveSpan({
30+
name,
31+
...options,
32+
attributes: {
33+
...options?.attributes,
34+
'sentry.cloudflare_tracer': true,
35+
},
36+
});
3037
}
3138

3239
/**
@@ -46,11 +53,15 @@ class SentryCloudflareTracer implements Tracer {
4653
context?: unknown,
4754
fn?: F,
4855
): ReturnType<F> {
49-
const opts = typeof options === 'object' && options !== null ? options : {};
56+
const opts = (typeof options === 'object' && options !== null ? options : {}) as SpanOptions;
5057

5158
const spanOpts = {
5259
name,
5360
...opts,
61+
attributes: {
62+
...opts.attributes,
63+
'sentry.cloudflare_tracer': true,
64+
},
5465
};
5566

5667
const callback = (

packages/cloudflare/src/sdk.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,16 @@ export function init(options: CloudflareOptions): CloudflareClient | undefined {
5151
transport: options.transport || makeCloudflareTransport,
5252
};
5353

54-
setupOpenTelemetryTracer();
54+
/**
55+
* The Cloudflare SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility
56+
* via a custom trace provider.
57+
* This ensures that any spans emitted via `@opentelemetry/api` will be captured by Sentry.
58+
* HOWEVER, big caveat: This does not handle custom context handling, it will always work off the current scope.
59+
* This should be good enough for many, but not all integrations.
60+
*/
61+
if (!options.skipOpenTelemetrySetup) {
62+
setupOpenTelemetryTracer();
63+
}
5564

5665
return initAndBind(CloudflareClient, clientOptions) as CloudflareClient;
5766
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { trace } from '@opentelemetry/api';
2+
import type { TransactionEvent } from '@sentry/core';
3+
import { startSpan } from '@sentry/core';
4+
import { beforeEach, describe, expect, test } from 'vitest';
5+
import { init } from '../src/sdk';
6+
import { resetSdk } from './testUtils';
7+
8+
describe('opentelemetry compatibility', () => {
9+
beforeEach(() => {
10+
resetSdk();
11+
});
12+
13+
test('should not capture spans emitted via @opentelemetry/api when skipOpenTelemetrySetup is true', async () => {
14+
const transactionEvents: TransactionEvent[] = [];
15+
16+
const client = init({
17+
dsn: 'https://username@domain/123',
18+
tracesSampleRate: 1,
19+
skipOpenTelemetrySetup: true,
20+
beforeSendTransaction: event => {
21+
transactionEvents.push(event);
22+
return null;
23+
},
24+
});
25+
26+
const tracer = trace.getTracer('test');
27+
const span = tracer.startSpan('test');
28+
span.end();
29+
30+
await client!.flush();
31+
32+
tracer.startActiveSpan('test 2', { attributes: { 'test.attribute': 'test' } }, span2 => {
33+
const span = tracer.startSpan('test 3', { attributes: { 'test.attribute': 'test2' } });
34+
span.end();
35+
span2.end();
36+
});
37+
38+
await client!.flush();
39+
40+
expect(transactionEvents).toHaveLength(0);
41+
});
42+
43+
test('should capture spans emitted via @opentelemetry/api', async () => {
44+
const transactionEvents: TransactionEvent[] = [];
45+
46+
const client = init({
47+
dsn: 'https://username@domain/123',
48+
tracesSampleRate: 1,
49+
beforeSendTransaction: event => {
50+
transactionEvents.push(event);
51+
return null;
52+
},
53+
});
54+
55+
const tracer = trace.getTracer('test');
56+
const span = tracer.startSpan('test');
57+
span.end();
58+
59+
await client!.flush();
60+
61+
tracer.startActiveSpan('test 2', { attributes: { 'test.attribute': 'test' } }, span2 => {
62+
const span = tracer.startSpan('test 3', { attributes: { 'test.attribute': 'test2' } });
63+
span.end();
64+
span2.end();
65+
});
66+
67+
await client!.flush();
68+
69+
expect(transactionEvents).toHaveLength(2);
70+
const [transactionEvent, transactionEvent2] = transactionEvents;
71+
72+
expect(transactionEvent?.spans?.length).toBe(0);
73+
expect(transactionEvent?.transaction).toBe('test');
74+
expect(transactionEvent?.contexts?.trace?.data).toEqual({
75+
'sentry.cloudflare_tracer': true,
76+
'sentry.origin': 'manual',
77+
'sentry.sample_rate': 1,
78+
'sentry.source': 'custom',
79+
});
80+
81+
expect(transactionEvent2?.spans?.length).toBe(1);
82+
expect(transactionEvent2?.transaction).toBe('test 2');
83+
expect(transactionEvent2?.contexts?.trace?.data).toEqual({
84+
'sentry.cloudflare_tracer': true,
85+
'sentry.origin': 'manual',
86+
'sentry.sample_rate': 1,
87+
'sentry.source': 'custom',
88+
'test.attribute': 'test',
89+
});
90+
91+
expect(transactionEvent2?.spans).toEqual([
92+
expect.objectContaining({
93+
description: 'test 3',
94+
data: {
95+
'sentry.cloudflare_tracer': true,
96+
'sentry.origin': 'manual',
97+
'test.attribute': 'test2',
98+
},
99+
}),
100+
]);
101+
});
102+
103+
test('opentelemetry spans should interop with Sentry spans', async () => {
104+
const transactionEvents: TransactionEvent[] = [];
105+
106+
const client = init({
107+
dsn: 'https://username@domain/123',
108+
tracesSampleRate: 1,
109+
beforeSendTransaction: event => {
110+
transactionEvents.push(event);
111+
return null;
112+
},
113+
});
114+
115+
const tracer = trace.getTracer('test');
116+
117+
startSpan({ name: 'sentry span' }, () => {
118+
const span = tracer.startSpan('otel span');
119+
span.end();
120+
});
121+
122+
await client!.flush();
123+
124+
expect(transactionEvents).toHaveLength(1);
125+
const [transactionEvent] = transactionEvents;
126+
127+
expect(transactionEvent?.spans?.length).toBe(1);
128+
expect(transactionEvent?.transaction).toBe('sentry span');
129+
expect(transactionEvent?.contexts?.trace?.data).toEqual({
130+
'sentry.origin': 'manual',
131+
'sentry.sample_rate': 1,
132+
'sentry.source': 'custom',
133+
});
134+
135+
expect(transactionEvent?.spans).toEqual([
136+
expect.objectContaining({
137+
description: 'otel span',
138+
data: {
139+
'sentry.cloudflare_tracer': true,
140+
'sentry.origin': 'manual',
141+
},
142+
}),
143+
]);
144+
});
145+
});

packages/cloudflare/test/sdk.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import * as SentryCore from '@sentry/core';
2-
import { describe, expect, test, vi } from 'vitest';
2+
import { beforeEach, describe, expect, test, vi } from 'vitest';
33
import { CloudflareClient } from '../src/client';
44
import { init } from '../src/sdk';
5+
import { resetSdk } from './testUtils';
56

67
describe('init', () => {
8+
beforeEach(() => {
9+
resetSdk();
10+
});
11+
712
test('should call initAndBind with the correct options', () => {
813
const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind');
914
const client = init({});

packages/cloudflare/test/testUtils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { context, propagation, trace } from '@opentelemetry/api';
2+
import { getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core';
3+
4+
function resetGlobals(): void {
5+
getCurrentScope().clear();
6+
getCurrentScope().setClient(undefined);
7+
getIsolationScope().clear();
8+
getGlobalScope().clear();
9+
}
10+
11+
function cleanupOtel(): void {
12+
// Disable all globally registered APIs
13+
trace.disable();
14+
context.disable();
15+
propagation.disable();
16+
}
17+
18+
export function resetSdk(): void {
19+
resetGlobals();
20+
cleanupOtel();
21+
}

0 commit comments

Comments
 (0)