-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix(deno): Clear pre-existing OTel global before registering TracerProvider #19723
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
235c683
1c1a149
e083e2a
cb4f1f8
0f123f8
a815bff
59205fd
00531da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| @sentry:registry=http://127.0.0.1:4873 | ||
| @sentry-internal:registry=http://127.0.0.1:4873 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "imports": { | ||
| "@sentry/deno": "../../../../packages/deno/build/esm/index.js", | ||
| "@sentry/core": "../../../../packages/core/build/esm/index.js", | ||
| "@opentelemetry/api": "npm:@opentelemetry/api@^1.9.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "name": "deno-app", | ||
| "version": "1.0.0", | ||
| "private": true, | ||
| "scripts": { | ||
| "start": "deno run --allow-net --allow-env --allow-read src/app.ts", | ||
| "test": "playwright test", | ||
| "clean": "npx rimraf node_modules pnpm-lock.yaml", | ||
| "test:build": "pnpm install", | ||
| "test:assert": "pnpm test" | ||
| }, | ||
| "dependencies": { | ||
| "@sentry/deno": "latest || *", | ||
| "@opentelemetry/api": "^1.9.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@playwright/test": "~1.56.0", | ||
| "@sentry-internal/test-utils": "link:../../../test-utils" | ||
| }, | ||
| "volta": { | ||
| "extends": "../../package.json" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { getPlaywrightConfig } from '@sentry-internal/test-utils'; | ||
|
|
||
| const config = getPlaywrightConfig({ | ||
| startCommand: `pnpm start`, | ||
| port: 3030, | ||
| }); | ||
|
|
||
| export default config; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { trace } from '@opentelemetry/api'; | ||
|
|
||
| // Simulate a pre-existing OTel provider (like Supabase Edge Runtime registers | ||
| // before user code runs). Without trace.disable() in Sentry's setup, this would | ||
| // cause setGlobalTracerProvider to be a no-op, silently dropping all OTel spans. | ||
| const fakeProvider = { | ||
| getTracer: () => ({ | ||
| startSpan: () => ({ end: () => {}, setAttributes: () => {} }), | ||
| startActiveSpan: (_name: string, fn: Function) => fn({ end: () => {}, setAttributes: () => {} }), | ||
| }), | ||
| }; | ||
| trace.setGlobalTracerProvider(fakeProvider as any); | ||
|
|
||
| // Sentry.init() must call trace.disable() to clear the fake provider above | ||
| import * as Sentry from '@sentry/deno'; | ||
|
|
||
| Sentry.init({ | ||
| environment: 'qa', | ||
| dsn: Deno.env.get('E2E_TEST_DSN'), | ||
| debug: !!Deno.env.get('DEBUG'), | ||
| tunnel: 'http://localhost:3031/', | ||
| tracesSampleRate: 1, | ||
| }); | ||
|
|
||
| const port = 3030; | ||
|
|
||
| Deno.serve({ port }, (req: Request) => { | ||
| const url = new URL(req.url); | ||
|
|
||
| if (url.pathname === '/test-success') { | ||
| return new Response(JSON.stringify({ version: 'v1' }), { | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }); | ||
| } | ||
|
|
||
| if (url.pathname === '/test-error') { | ||
| const exceptionId = Sentry.captureException(new Error('This is an error')); | ||
| return new Response(JSON.stringify({ exceptionId }), { | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }); | ||
| } | ||
|
|
||
| // Test Sentry.startSpan — uses Sentry's internal pipeline | ||
| if (url.pathname === '/test-sentry-span') { | ||
| Sentry.startSpan({ name: 'test-sentry-span' }, () => { | ||
| // noop | ||
| }); | ||
| return new Response(JSON.stringify({ status: 'ok' }), { | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }); | ||
| } | ||
|
|
||
| // Test OTel tracer.startSpan — goes through the global TracerProvider | ||
| if (url.pathname === '/test-otel-span') { | ||
| const tracer = trace.getTracer('test-tracer'); | ||
| const span = tracer.startSpan('test-otel-span'); | ||
| span.end(); | ||
| return new Response(JSON.stringify({ status: 'ok' }), { | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }); | ||
| } | ||
|
|
||
| // Test OTel tracer.startActiveSpan — what AI SDK and most instrumentations use | ||
| if (url.pathname === '/test-otel-active-span') { | ||
| const tracer = trace.getTracer('test-tracer'); | ||
| tracer.startActiveSpan('test-otel-active-span', span => { | ||
| span.setAttributes({ 'test.active': true }); | ||
| span.end(); | ||
| }); | ||
| return new Response(JSON.stringify({ status: 'ok' }), { | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }); | ||
| } | ||
|
|
||
| // Test interop: OTel span inside a Sentry span | ||
| if (url.pathname === '/test-interop') { | ||
| Sentry.startSpan({ name: 'sentry-parent' }, () => { | ||
| const tracer = trace.getTracer('test-tracer'); | ||
| const span = tracer.startSpan('otel-child'); | ||
| span.end(); | ||
| }); | ||
| return new Response(JSON.stringify({ status: 'ok' }), { | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }); | ||
| } | ||
|
|
||
| return new Response('Not found', { status: 404 }); | ||
| }); | ||
|
|
||
| console.log(`Deno test app listening on port ${port}`); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { startEventProxyServer } from '@sentry-internal/test-utils'; | ||
|
|
||
| startEventProxyServer({ | ||
| port: 3031, | ||
| proxyServerName: 'deno', | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { waitForError } from '@sentry-internal/test-utils'; | ||
|
|
||
| test('Sends error event', async ({ baseURL }) => { | ||
| const errorEventPromise = waitForError('deno', event => { | ||
| return !event.type && event.exception?.values?.[0]?.value === 'This is an error'; | ||
| }); | ||
|
|
||
| await fetch(`${baseURL}/test-error`); | ||
|
|
||
| const errorEvent = await errorEventPromise; | ||
|
|
||
| expect(errorEvent.exception?.values).toHaveLength(1); | ||
| expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an error'); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { waitForTransaction } from '@sentry-internal/test-utils'; | ||
|
|
||
| test('Sends transaction with Sentry.startSpan', async ({ baseURL }) => { | ||
| const transactionPromise = waitForTransaction('deno', event => { | ||
| return event?.spans?.some(span => span.description === 'test-sentry-span') ?? false; | ||
sergical marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| await fetch(`${baseURL}/test-sentry-span`); | ||
|
|
||
| const transaction = await transactionPromise; | ||
|
|
||
| expect(transaction.spans).toEqual( | ||
| expect.arrayContaining([ | ||
| expect.objectContaining({ | ||
| description: 'test-sentry-span', | ||
| origin: 'manual', | ||
| }), | ||
| ]), | ||
| ); | ||
| }); | ||
|
|
||
| test('Sends transaction with OTel tracer.startSpan despite pre-existing provider', async ({ baseURL }) => { | ||
| const transactionPromise = waitForTransaction('deno', event => { | ||
| return event?.spans?.some(span => span.description === 'test-otel-span') ?? false; | ||
| }); | ||
|
|
||
| await fetch(`${baseURL}/test-otel-span`); | ||
|
|
||
| const transaction = await transactionPromise; | ||
|
|
||
| expect(transaction.spans).toEqual( | ||
| expect.arrayContaining([ | ||
| expect.objectContaining({ | ||
| description: 'test-otel-span', | ||
| op: 'otel.span', | ||
| origin: 'manual', | ||
| }), | ||
| ]), | ||
| ); | ||
| }); | ||
|
|
||
| test('Sends transaction with OTel tracer.startActiveSpan', async ({ baseURL }) => { | ||
| const transactionPromise = waitForTransaction('deno', event => { | ||
| return event?.spans?.some(span => span.description === 'test-otel-active-span') ?? false; | ||
| }); | ||
|
|
||
| await fetch(`${baseURL}/test-otel-active-span`); | ||
|
|
||
| const transaction = await transactionPromise; | ||
|
|
||
| expect(transaction.spans).toEqual( | ||
| expect.arrayContaining([ | ||
| expect.objectContaining({ | ||
| description: 'test-otel-active-span', | ||
| op: 'otel.span', | ||
| origin: 'manual', | ||
| }), | ||
| ]), | ||
| ); | ||
| }); | ||
|
|
||
| test('OTel span appears as child of Sentry span (interop)', async ({ baseURL }) => { | ||
| const transactionPromise = waitForTransaction('deno', event => { | ||
| return event?.spans?.some(span => span.description === 'sentry-parent') ?? false; | ||
| }); | ||
|
|
||
| await fetch(`${baseURL}/test-interop`); | ||
|
|
||
| const transaction = await transactionPromise; | ||
|
|
||
| expect(transaction.spans).toEqual( | ||
| expect.arrayContaining([ | ||
| expect.objectContaining({ | ||
| description: 'sentry-parent', | ||
| origin: 'manual', | ||
| }), | ||
| expect.objectContaining({ | ||
| description: 'otel-child', | ||
| op: 'otel.span', | ||
| origin: 'manual', | ||
| }), | ||
| ]), | ||
| ); | ||
|
|
||
| // Verify the OTel span is a child of the Sentry span | ||
| const sentrySpan = transaction.spans!.find((s: any) => s.description === 'sentry-parent'); | ||
| const otelSpan = transaction.spans!.find((s: any) => s.description === 'otel-child'); | ||
| expect(otelSpan!.parent_span_id).toBe(sentrySpan!.span_id); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,9 @@ import { | |
| * This is not perfect but handles easy/common use cases. | ||
| */ | ||
| export function setupOpenTelemetryTracer(): void { | ||
| // Clear any pre-existing OTel global registration (e.g. from Supabase Edge Runtime | ||
| // or Deno's built-in OTel) so Sentry's TracerProvider gets registered successfully. | ||
| trace.disable(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Update: I just saw that we gate this function call with |
||
| trace.setGlobalTracerProvider(new SentryDenoTraceProvider()); | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.