From 235c683f23caad861d388ac02f4a5437eba0cae7 Mon Sep 17 00:00:00 2001 From: Sergiy Dybskiy Date: Mon, 9 Mar 2026 16:10:56 -0400 Subject: [PATCH 1/7] fix(deno): Clear pre-existing OTel global before registering TracerProvider Supabase Edge Runtime (and Deno's native OTel) pre-registers on the `@opentelemetry/api` global, causing `trace.setGlobalTracerProvider()` to silently fail. Call `trace.disable()` first so Sentry's provider always wins. Co-Authored-By: Claude Opus 4.6 --- packages/deno/src/opentelemetry/tracer.ts | 3 ++ packages/deno/test/opentelemetry.test.ts | 64 +++++++++++------------ 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/packages/deno/src/opentelemetry/tracer.ts b/packages/deno/src/opentelemetry/tracer.ts index 3176616bc04c..7bc704446d37 100644 --- a/packages/deno/src/opentelemetry/tracer.ts +++ b/packages/deno/src/opentelemetry/tracer.ts @@ -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(); trace.setGlobalTracerProvider(new SentryDenoTraceProvider()); } diff --git a/packages/deno/test/opentelemetry.test.ts b/packages/deno/test/opentelemetry.test.ts index 30723e033dd4..492dead3339c 100644 --- a/packages/deno/test/opentelemetry.test.ts +++ b/packages/deno/test/opentelemetry.test.ts @@ -144,38 +144,39 @@ Deno.test('opentelemetry spans should interop with Sentry spans', async () => { assertEquals(otelSpan?.data?.['sentry.origin'], 'manual'); }); -Deno.test('should be compatible with native Deno OpenTelemetry', async () => { +Deno.test('should override pre-existing OTel provider with Sentry provider', async () => { resetSdk(); - const providerBefore = trace.getTracerProvider(); + // Simulate a pre-existing OTel registration (e.g. from Supabase Edge Runtime) + const fakeProvider = { getTracer: () => ({}) }; + trace.setGlobalTracerProvider(fakeProvider as any); + + const transactionEvents: any[] = []; const client = init({ dsn: 'https://username@domain/123', tracesSampleRate: 1, - beforeSendTransaction: () => null, + beforeSendTransaction: event => { + transactionEvents.push(event); + return null; + }, }) as DenoClient; + // Sentry should have overridden the pre-existing provider via trace.disable() const providerAfter = trace.getTracerProvider(); - assertEquals(providerBefore, providerAfter); + assertNotEquals(providerAfter, fakeProvider); + // Verify Sentry's tracer actually captures spans const tracer = trace.getTracer('compat-test'); const span = tracer.startSpan('test-span'); span.setAttributes({ 'test.compatibility': true }); span.end(); - tracer.startActiveSpan('active-span', activeSpan => { - activeSpan.end(); - }); - - const otelSpan = tracer.startSpan('post-init-span'); - otelSpan.end(); - - startSpan({ name: 'sentry-span' }, () => { - const nestedOtelSpan = tracer.startSpan('nested-otel-span'); - nestedOtelSpan.end(); - }); - await client.flush(); + + assertEquals(transactionEvents.length, 1); + assertEquals(transactionEvents[0]?.transaction, 'test-span'); + assertEquals(transactionEvents[0]?.contexts?.trace?.data?.['sentry.deno_tracer'], true); }); // Test that name parameter takes precedence over options.name for both startSpan and startActiveSpan @@ -238,7 +239,7 @@ Deno.test('name parameter should take precedence over options.name in startActiv assertEquals(transactionEvent?.transaction, 'prisma:client:operation'); }); -Deno.test('should verify native Deno OpenTelemetry works when enabled', async () => { +Deno.test('should override native Deno OpenTelemetry when enabled', async () => { resetSdk(); // Set environment variable to enable native OTel @@ -246,34 +247,29 @@ Deno.test('should verify native Deno OpenTelemetry works when enabled', async () Deno.env.set('OTEL_DENO', 'true'); try { + const transactionEvents: any[] = []; + const client = init({ dsn: 'https://username@domain/123', tracesSampleRate: 1, - beforeSendTransaction: () => null, + beforeSendTransaction: event => { + transactionEvents.push(event); + return null; + }, }) as DenoClient; - const provider = trace.getTracerProvider(); + // Sentry's trace.disable() + setGlobalTracerProvider should have overridden + // any native Deno OTel provider, so spans go through Sentry's tracer. const tracer = trace.getTracer('native-verification'); const span = tracer.startSpan('verification-span'); - - if (provider.constructor.name === 'Function') { - // Native OTel is active - assertNotEquals(span.constructor.name, 'NonRecordingSpan'); - - let contextWorks = false; - tracer.startActiveSpan('parent-span', parentSpan => { - if (trace.getActiveSpan() === parentSpan) { - contextWorks = true; - } - parentSpan.end(); - }); - assertEquals(contextWorks, true); - } - span.setAttributes({ 'test.native_otel': true }); span.end(); await client.flush(); + + assertEquals(transactionEvents.length, 1); + assertEquals(transactionEvents[0]?.transaction, 'verification-span'); + assertEquals(transactionEvents[0]?.contexts?.trace?.data?.['sentry.deno_tracer'], true); } finally { // Restore original environment if (originalValue === undefined) { From e083e2a0c6f33076e6bccbd2e1009274919ca4e4 Mon Sep 17 00:00:00 2001 From: Sergiy Dybskiy Date: Tue, 10 Mar 2026 14:26:03 -0400 Subject: [PATCH 2/7] test(deno): Add e2e test app for OTel tracer and trace.disable() fix Co-Authored-By: Claude Opus 4.6 --- .../e2e-tests/test-applications/deno/.npmrc | 2 + .../test-applications/deno/deno.json | 7 ++ .../test-applications/deno/package.json | 19 ++++ .../deno/playwright.config.mjs | 8 ++ .../test-applications/deno/src/app.ts | 90 +++++++++++++++++++ .../deno/start-event-proxy.mjs | 6 ++ .../deno/tests/errors.test.ts | 15 ++++ .../deno/tests/transactions.test.ts | 90 +++++++++++++++++++ 8 files changed, 237 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/deno/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/deno/deno.json create mode 100644 dev-packages/e2e-tests/test-applications/deno/package.json create mode 100644 dev-packages/e2e-tests/test-applications/deno/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/deno/src/app.ts create mode 100644 dev-packages/e2e-tests/test-applications/deno/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/deno/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/deno/tests/transactions.test.ts diff --git a/dev-packages/e2e-tests/test-applications/deno/.npmrc b/dev-packages/e2e-tests/test-applications/deno/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/deno/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/deno/deno.json b/dev-packages/e2e-tests/test-applications/deno/deno.json new file mode 100644 index 000000000000..e68e7e00c78a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/deno/deno.json @@ -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" + } +} diff --git a/dev-packages/e2e-tests/test-applications/deno/package.json b/dev-packages/e2e-tests/test-applications/deno/package.json new file mode 100644 index 000000000000..92179ce5923b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/deno/package.json @@ -0,0 +1,19 @@ +{ + "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" + }, + "devDependencies": { + "@playwright/test": "~1.56.0", + "@sentry-internal/test-utils": "link:../../../test-utils" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/deno/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/deno/playwright.config.mjs new file mode 100644 index 000000000000..3d3ab7d8df02 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/deno/playwright.config.mjs @@ -0,0 +1,8 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, + port: 3030, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/deno/src/app.ts b/dev-packages/e2e-tests/test-applications/deno/src/app.ts new file mode 100644 index 000000000000..b92e7355e625 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/deno/src/app.ts @@ -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}`); diff --git a/dev-packages/e2e-tests/test-applications/deno/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/deno/start-event-proxy.mjs new file mode 100644 index 000000000000..a97ce6aa005c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/deno/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'deno', +}); diff --git a/dev-packages/e2e-tests/test-applications/deno/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/deno/tests/errors.test.ts new file mode 100644 index 000000000000..5b4018291a18 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/deno/tests/errors.test.ts @@ -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'); +}); diff --git a/dev-packages/e2e-tests/test-applications/deno/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/deno/tests/transactions.test.ts new file mode 100644 index 000000000000..3cd0892cebdc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/deno/tests/transactions.test.ts @@ -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; + }); + + 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); +}); From cb4f1f869d6588cbe473baab17d17707454703e3 Mon Sep 17 00:00:00 2001 From: Sergiy Dybskiy Date: Tue, 10 Mar 2026 14:33:06 -0400 Subject: [PATCH 3/7] fix(deno): Fix formatting in e2e test app (double -> single quotes) Co-Authored-By: Claude Opus 4.6 --- .../test-applications/deno/src/app.ts | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/deno/src/app.ts b/dev-packages/e2e-tests/test-applications/deno/src/app.ts index b92e7355e625..fb34053e29d7 100644 --- a/dev-packages/e2e-tests/test-applications/deno/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/deno/src/app.ts @@ -1,4 +1,4 @@ -import { trace } from "@opentelemetry/api"; +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 @@ -12,13 +12,13 @@ const fakeProvider = { trace.setGlobalTracerProvider(fakeProvider as any); // Sentry.init() must call trace.disable() to clear the fake provider above -import * as Sentry from "@sentry/deno"; +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/", + environment: 'qa', + dsn: Deno.env.get('E2E_TEST_DSN'), + debug: !!Deno.env.get('DEBUG'), + tunnel: 'http://localhost:3031/', tracesSampleRate: 1, }); @@ -27,64 +27,64 @@ 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-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")); + 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" }, + 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" }, () => { + 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" }, + 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"); + 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" }, + 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 }); + 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" }, + 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"); + 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(JSON.stringify({ status: 'ok' }), { + headers: { 'Content-Type': 'application/json' }, }); } - return new Response("Not found", { status: 404 }); + return new Response('Not found', { status: 404 }); }); console.log(`Deno test app listening on port ${port}`); From 0f123f808bf97ec6ca534be3be355e361952c79f Mon Sep 17 00:00:00 2001 From: Sergiy Dybskiy Date: Tue, 10 Mar 2026 15:00:41 -0400 Subject: [PATCH 4/7] fix(deno): Fix Deno e2e test CI integration - Add @sentry/deno to package.json so matrix builder triggers on SDK changes - Add Deno setup step in build.yml for CI runtime - Fix deno.json relative import paths in copyToTemp.ts for temp dir copies Co-Authored-By: Claude Opus 4.6 --- .github/workflows/build.yml | 5 +++ dev-packages/e2e-tests/lib/copyToTemp.ts | 34 ++++++++++++++++++- .../test-applications/deno/package.json | 3 ++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5b84a70ffbd6..39ca1e71cbb4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1017,6 +1017,11 @@ jobs: with: use-installer: true token: ${{ secrets.GITHUB_TOKEN }} + - name: Set up Deno + if: matrix.test-application == 'deno' + uses: denoland/setup-deno@v2.0.3 + with: + deno-version: v2.1.5 - name: Restore caches uses: ./.github/actions/restore-cache with: diff --git a/dev-packages/e2e-tests/lib/copyToTemp.ts b/dev-packages/e2e-tests/lib/copyToTemp.ts index 830ff76f6077..bd9a523fcacb 100644 --- a/dev-packages/e2e-tests/lib/copyToTemp.ts +++ b/dev-packages/e2e-tests/lib/copyToTemp.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import { readFileSync, writeFileSync } from 'fs'; +import { existsSync, readFileSync, writeFileSync } from 'fs'; import { cp } from 'fs/promises'; import { join } from 'path'; @@ -8,6 +8,7 @@ export async function copyToTemp(originalPath: string, tmpDirPath: string): Prom await cp(originalPath, tmpDirPath, { recursive: true }); fixPackageJson(tmpDirPath); + fixDenoJson(tmpDirPath); } function fixPackageJson(cwd: string): void { @@ -59,3 +60,34 @@ function fixFileLinkDependencies(dependencyObj: Record): void { } } } + +function fixDenoJson(cwd: string): void { + const denoJsonPath = join(cwd, 'deno.json'); + if (!existsSync(denoJsonPath)) { + return; + } + + const denoJson = JSON.parse(readFileSync(denoJsonPath, 'utf8')) as { + imports?: Record; + }; + + if (!denoJson.imports) { + return; + } + + let changed = false; + for (const [key, value] of Object.entries(denoJson.imports)) { + // Fix relative paths (not npm: or https: specifiers) + if (value.startsWith('.') || value.startsWith('/')) { + // Same virtual-dir trick as link: deps to get consistent relative depth + const newPath = join(__dirname, 'virtual-dir/', value); + denoJson.imports[key] = newPath; + console.log(`Fixed deno.json import ${key} to ${newPath}`); + changed = true; + } + } + + if (changed) { + writeFileSync(denoJsonPath, JSON.stringify(denoJson, null, 2)); + } +} diff --git a/dev-packages/e2e-tests/test-applications/deno/package.json b/dev-packages/e2e-tests/test-applications/deno/package.json index 92179ce5923b..7caa1cf51cb1 100644 --- a/dev-packages/e2e-tests/test-applications/deno/package.json +++ b/dev-packages/e2e-tests/test-applications/deno/package.json @@ -9,6 +9,9 @@ "test:build": "pnpm install", "test:assert": "pnpm test" }, + "dependencies": { + "@sentry/deno": "latest || *" + }, "devDependencies": { "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils" From a815bff54f8ac538cf4e158004a8622689d3bd03 Mon Sep 17 00:00:00 2001 From: Sergiy Dybskiy Date: Tue, 10 Mar 2026 15:13:59 -0400 Subject: [PATCH 5/7] fix(e2e): Fix TOCTOU race condition in copyToTemp.ts Replace `existsSync` check + `readFileSync` with a single `readFileSync` wrapped in try-catch to eliminate the CodeQL-flagged race condition. Co-Authored-By: Claude Opus 4.6 --- dev-packages/e2e-tests/lib/copyToTemp.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/dev-packages/e2e-tests/lib/copyToTemp.ts b/dev-packages/e2e-tests/lib/copyToTemp.ts index bd9a523fcacb..83b7ce352b85 100644 --- a/dev-packages/e2e-tests/lib/copyToTemp.ts +++ b/dev-packages/e2e-tests/lib/copyToTemp.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { readFileSync, writeFileSync } from 'fs'; import { cp } from 'fs/promises'; import { join } from 'path'; @@ -63,11 +63,15 @@ function fixFileLinkDependencies(dependencyObj: Record): void { function fixDenoJson(cwd: string): void { const denoJsonPath = join(cwd, 'deno.json'); - if (!existsSync(denoJsonPath)) { + + let raw: string; + try { + raw = readFileSync(denoJsonPath, 'utf8'); + } catch { return; } - const denoJson = JSON.parse(readFileSync(denoJsonPath, 'utf8')) as { + const denoJson = JSON.parse(raw) as { imports?: Record; }; From 59205fd9e8c90c534859b9f84a7af632e9aeba75 Mon Sep 17 00:00:00 2001 From: Sergiy Dybskiy Date: Tue, 10 Mar 2026 17:11:49 -0400 Subject: [PATCH 6/7] fix(deno): Add nodeModulesDir auto to fix CI npm resolution Deno couldn't find `@opentelemetry/api` in node_modules because it wasn't listed in package.json. Adding `"nodeModulesDir": "auto"` lets Deno auto-install npm: specifier deps on startup. Co-Authored-By: Claude Opus 4.6 --- dev-packages/e2e-tests/test-applications/deno/deno.json | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-packages/e2e-tests/test-applications/deno/deno.json b/dev-packages/e2e-tests/test-applications/deno/deno.json index e68e7e00c78a..cbe76753f4fa 100644 --- a/dev-packages/e2e-tests/test-applications/deno/deno.json +++ b/dev-packages/e2e-tests/test-applications/deno/deno.json @@ -1,4 +1,5 @@ { + "nodeModulesDir": "auto", "imports": { "@sentry/deno": "../../../../packages/deno/build/esm/index.js", "@sentry/core": "../../../../packages/core/build/esm/index.js", From 00531da5738fd9e8edb6b34fba71722d0f045c19 Mon Sep 17 00:00:00 2001 From: Sergiy Dybskiy Date: Tue, 10 Mar 2026 17:36:08 -0400 Subject: [PATCH 7/7] fix(deno): Fix Deno E2E test playwright version conflict Revert `nodeModulesDir: "auto"` and add `@opentelemetry/api` to package.json instead. This lets pnpm install OTel into node_modules so Deno resolves it in manual mode without creating a `.deno/` dir that duplicates playwright. Co-Authored-By: Claude Opus 4.6 --- dev-packages/e2e-tests/test-applications/deno/deno.json | 1 - dev-packages/e2e-tests/test-applications/deno/package.json | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/deno/deno.json b/dev-packages/e2e-tests/test-applications/deno/deno.json index cbe76753f4fa..e68e7e00c78a 100644 --- a/dev-packages/e2e-tests/test-applications/deno/deno.json +++ b/dev-packages/e2e-tests/test-applications/deno/deno.json @@ -1,5 +1,4 @@ { - "nodeModulesDir": "auto", "imports": { "@sentry/deno": "../../../../packages/deno/build/esm/index.js", "@sentry/core": "../../../../packages/core/build/esm/index.js", diff --git a/dev-packages/e2e-tests/test-applications/deno/package.json b/dev-packages/e2e-tests/test-applications/deno/package.json index 7caa1cf51cb1..8ec92fbd3985 100644 --- a/dev-packages/e2e-tests/test-applications/deno/package.json +++ b/dev-packages/e2e-tests/test-applications/deno/package.json @@ -10,7 +10,8 @@ "test:assert": "pnpm test" }, "dependencies": { - "@sentry/deno": "latest || *" + "@sentry/deno": "latest || *", + "@opentelemetry/api": "^1.9.0" }, "devDependencies": { "@playwright/test": "~1.56.0",