From c787622d9771700f9e3ba557bc6974e886cc0be4 Mon Sep 17 00:00:00 2001 From: isaacs Date: Tue, 28 Jul 2026 12:50:04 -0700 Subject: [PATCH] fix(deno)!: require deno >=2.8.3 This avoids some tedious workarounds and caveats regarding a loader hook deadlock bug that was fixed in version 2.8.3. Since all versions support module loaders fully, we can now unconditionally include the orchestrion instrumentations. --- .github/workflows/build.yml | 8 +- MIGRATION.md | 2 +- packages/deno/README.md | 22 +---- packages/deno/scripts/download-deno-types.mjs | 2 +- packages/deno/src/denoVersion.ts | 32 -------- packages/deno/src/import.mjs | 20 +---- packages/deno/src/integrations/http.ts | 78 +++++++----------- packages/deno/src/sdk.ts | 80 +++++++------------ packages/deno/test/deno-http.test.ts | 36 +-------- .../src/orchestrion/runtime/register.ts | 12 +-- 10 files changed, 75 insertions(+), 217 deletions(-) delete mode 100644 packages/deno/src/denoVersion.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ee0c41858e98..1ad4d82fd9d6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -546,7 +546,7 @@ jobs: - name: Set up Deno uses: denoland/setup-deno@v2.0.4 with: - deno-version: ${{ matrix.deno-version || 'v2.8.0' }} + deno-version: ${{ matrix.deno-version || 'v2.8.3' }} - name: Restore caches uses: ./.github/actions/restore-cache with: @@ -936,7 +936,7 @@ jobs: - name: Set up Deno uses: denoland/setup-deno@v2.0.4 with: - deno-version: 'v2.8.0' + deno-version: 'v2.8.3' - name: Restore caches uses: ./.github/actions/restore-cache with: @@ -1046,7 +1046,7 @@ jobs: matrix.test-application == 'deno-pg' uses: denoland/setup-deno@v2.0.4 with: - deno-version: ${{ matrix.deno-version || 'v2.8.0' }} + deno-version: ${{ matrix.deno-version || 'v2.8.3' }} - name: Restore caches uses: ./.github/actions/restore-cache with: @@ -1169,7 +1169,7 @@ jobs: if: matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed' uses: denoland/setup-deno@v2.0.4 with: - deno-version: ${{ matrix.deno-version || 'v2.8.0' }} + deno-version: ${{ matrix.deno-version || 'v2.8.3' }} - name: Restore caches uses: ./.github/actions/restore-cache with: diff --git a/MIGRATION.md b/MIGRATION.md index 3ee29f749e7a..d98adb0507b7 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -34,7 +34,7 @@ Version 11 of the Sentry SDK has new compatibility ranges for runtimes and frame **Node.js:** The minimum supported Node.js version is now **20.19.0**. Node.js 18 is no longer supported. -**Deno:** The minimum supported Deno version is now **2.8.2**. +**Deno:** The minimum supported Deno version is now **2.8.3**. **Browsers:** Support for **Safari 14** was dropped. Sentry now requires Safari 15 or higher. For the rest of the browser support matrix, refer to the [Sentry docs](https://docs.sentry.io/platforms/javascript/#browser-support). diff --git a/packages/deno/README.md b/packages/deno/README.md index 073a29ae373e..3e868bb6bd62 100644 --- a/packages/deno/README.md +++ b/packages/deno/README.md @@ -65,30 +65,14 @@ own. To instrument them, Sentry uses transform them at load time so they publish to `node:diagnostics_channel`. -In Deno versions prior to 2.8.0, this is not available, as it -relies on `Module.registerHooks`, which was added in that -version. - -As of Deno 2.8.3, you can use the `--import` or `--preload` -argument to `deno run` in order to enable these instrumentations. +Use the `--import` or `--preload` argument to `deno run` to enable +these instrumentations. ```bash $ deno run --import=@sentry/deno/import app.ts ``` -> [!NOTE] -> In Deno versions **2.8.0** through **2.8.2**, a bug causes Deno -> to deadlock when a module hook is added in this way. As a -> workaround, you can import the loader explicitly, and then -> dynamically import your app to take advantage of the added -> module loading hooks. -> -> ```ts -> import 'npm:@sentry/deno/import'; -> await import('./app.ts'); -> ``` - -In both cases, your `app.ts` should simply load Sentry as usual: +Your `app.ts` should simply load Sentry as usual: ```ts // app.ts diff --git a/packages/deno/scripts/download-deno-types.mjs b/packages/deno/scripts/download-deno-types.mjs index 96cd724fbf99..c73f349ce212 100644 --- a/packages/deno/scripts/download-deno-types.mjs +++ b/packages/deno/scripts/download-deno-types.mjs @@ -2,6 +2,6 @@ import { existsSync, writeFileSync } from 'fs'; import { download } from './download.mjs'; if (!existsSync('lib.deno.d.ts')) { - const code = await download('https://github.com/denoland/deno/releases/download/v2.8.0/lib.deno.d.ts'); + const code = await download('https://github.com/denoland/deno/releases/download/v2.8.3/lib.deno.d.ts'); writeFileSync('lib.deno.d.ts', code); } diff --git a/packages/deno/src/denoVersion.ts b/packages/deno/src/denoVersion.ts deleted file mode 100644 index da87eec5b595..000000000000 --- a/packages/deno/src/denoVersion.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { parseSemver } from '@sentry/core'; - -export const DENO_VERSION = parseSemver(typeof Deno !== 'undefined' ? (Deno.version?.deno ?? '') : '') as { - major: number | undefined; - minor: number | undefined; - patch: number | undefined; -}; - -/** Exported for testing */ -function gte(major: number, minor: number, patch: number): boolean { - const { major: M, minor: m, patch: p } = DENO_VERSION; - if (M === undefined || m === undefined || p === undefined) return false; - if (M !== major) return M > major; - if (m !== minor) return m > minor; - return p >= patch; -} - -/** Whether `http.client.request.created` fires (Deno 2.7.13+). */ -export const HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED = gte(2, 7, 13); - -/** Whether `http.server.request.start` fires (Deno 2.8.0+). */ -export const HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED = gte(2, 8, 0); - -/** Whether `node:diagnostics_channel.tracingChannel` exists (Deno 1.44.3+). */ -export const TRACING_CHANNEL_SUPPORTED = gte(1, 44, 3); - -/** - * Whether `Module.registerHooks` is available (Deno 2.8.0+), which the - * orchestrion runtime hook (`@sentry/deno/import`) needs to transform libraries - * like `mysql` so they publish to their tracing channels. - */ -export const MODULE_REGISTER_HOOKS_SUPPORTED = gte(2, 8, 0); diff --git a/packages/deno/src/import.mjs b/packages/deno/src/import.mjs index f45680705555..dd501c169a8f 100644 --- a/packages/deno/src/import.mjs +++ b/packages/deno/src/import.mjs @@ -1,13 +1,8 @@ /** * EXPERIMENTAL: orchestrion runtime hook for Deno. * - * In Deno versions prior to 2.8.0, this is a no-op: it relies on - * `Module.registerHooks` (added in 2.8.0), so without it the channels are - * simply not injected (channel-based instrumentation is disabled, with a - * warning in debug builds). It does not crash. - * - * As of Deno 2.8.3, this can be loaded via `--import` or `--preload` - * argument to `deno run` in order to enable these instrumentations. + * Load this via the `--import` or `--preload` argument to `deno run` to + * enable the channel-based instrumentations. * * For example: * @@ -15,17 +10,6 @@ * $ deno run --import=@sentry/deno/import app.ts * ``` * - * In Deno 2.8.0 through 2.8.2, it can be loaded directly in an - * `init.ts` file that then loads the app via dynamic import. - * - * For example: - * - * ```ts - * // init.ts - * import '@sentry/deno/import'; - * await import('./app.ts'); - * ``` - * * @module */ import '@sentry/server-utils/orchestrion/import-hook'; diff --git a/packages/deno/src/integrations/http.ts b/packages/deno/src/integrations/http.ts index 7d2b90f3f8f5..ee977ae3a74e 100644 --- a/packages/deno/src/integrations/http.ts +++ b/packages/deno/src/integrations/http.ts @@ -3,7 +3,6 @@ import { errorMonitor } from 'node:events'; import type { RequestOptions } from 'node:http'; import type { HttpIncomingMessage, Integration, IntegrationFn, Span } from '@sentry/core'; import { - debug, defineIntegration, getHttpClientSubscriptions, getHttpServerSubscriptions, @@ -11,11 +10,6 @@ import { HTTP_ON_CLIENT_REQUEST, HTTP_ON_SERVER_REQUEST, } from '@sentry/core'; -import { - DENO_VERSION, - HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED, - HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED, -} from '../denoVersion'; const INTEGRATION_NAME = 'DenoHttp' as const; @@ -100,51 +94,33 @@ const _denoHttpIntegration = ((options: DenoHttpIntegrationOptions = {}) => { return { name: INTEGRATION_NAME, setupOnce() { - const denoVersion = DENO_VERSION.major !== undefined ? `${Deno.version.deno}` : 'unknown'; - - // Below 2.7.13 neither channel fires. Warn and bail without touching the ACS. - if (!HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED && !HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED) { - debug.warn( - `denoHttpIntegration requires Deno 2.7.13+ (client) or 2.8.0+ (server) for node:http diagnostics channels; running on Deno ${denoVersion}. The integration is a no-op on this version.`, - ); - return; - } - - if (HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED) { - const { [HTTP_ON_SERVER_REQUEST]: onHttpServerRequest } = getHttpServerSubscriptions({ - // `spans` falls through to the client's tracing config when unset. - spans: options.spans, - ignoreStaticAssets: options.ignoreStaticAssets, - ignoreIncomingRequests: options.ignoreIncomingRequests, - maxRequestBodySize: options.maxRequestBodySize ?? 'medium', - ignoreRequestBody: options.ignoreRequestBody, - onSpanCreated: options.onIncomingSpanCreated, - onSpanEnd: options.onIncomingSpanEnd, - errorMonitor, - sessions: false, - }); - subscribe(HTTP_ON_SERVER_REQUEST, onHttpServerRequest); - } else { - debug.log( - `denoHttpIntegration: server-side instrumentation requires Deno 2.8.0+; running on Deno ${denoVersion}. Client-side instrumentation is still active.`, - ); - } - - if (HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED) { - const { [HTTP_ON_CLIENT_REQUEST]: onHttpClientRequest } = getHttpClientSubscriptions({ - spans: options.spans, - breadcrumbs, - propagateTrace: tracePropagation, - ignoreOutgoingRequests: options.ignoreOutgoingRequests - ? (url, request) => options.ignoreOutgoingRequests!(url, getRequestOptions(request)) - : undefined, - // Deno doesn't run OTel's http instrumentation, so there's no - // double-wrap to detect; skip the warning to avoid loading the module. - suppressOtelWarning: true, - errorMonitor, - }); - subscribe(HTTP_ON_CLIENT_REQUEST, onHttpClientRequest); - } + const { [HTTP_ON_SERVER_REQUEST]: onHttpServerRequest } = getHttpServerSubscriptions({ + // `spans` falls through to the client's tracing config when unset. + spans: options.spans, + ignoreStaticAssets: options.ignoreStaticAssets, + ignoreIncomingRequests: options.ignoreIncomingRequests, + maxRequestBodySize: options.maxRequestBodySize ?? 'medium', + ignoreRequestBody: options.ignoreRequestBody, + onSpanCreated: options.onIncomingSpanCreated, + onSpanEnd: options.onIncomingSpanEnd, + errorMonitor, + sessions: false, + }); + subscribe(HTTP_ON_SERVER_REQUEST, onHttpServerRequest); + + const { [HTTP_ON_CLIENT_REQUEST]: onHttpClientRequest } = getHttpClientSubscriptions({ + spans: options.spans, + breadcrumbs, + propagateTrace: tracePropagation, + ignoreOutgoingRequests: options.ignoreOutgoingRequests + ? (url, request) => options.ignoreOutgoingRequests!(url, getRequestOptions(request)) + : undefined, + // Deno doesn't run OTel's http instrumentation, so there's no + // double-wrap to detect; skip the warning to avoid loading the module. + suppressOtelWarning: true, + errorMonitor, + }); + subscribe(HTTP_ON_CLIENT_REQUEST, onHttpClientRequest); }, }; }) satisfies IntegrationFn; diff --git a/packages/deno/src/sdk.ts b/packages/deno/src/sdk.ts index cf3ca19ce4ed..cda4a95a6e62 100644 --- a/packages/deno/src/sdk.ts +++ b/packages/deno/src/sdk.ts @@ -40,12 +40,6 @@ import { DenoClient } from './client'; import { breadcrumbsIntegration } from './integrations/breadcrumbs'; import { denoContextIntegration } from './integrations/context'; import { contextLinesIntegration } from './integrations/contextlines'; -import { - HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED, - HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED, - MODULE_REGISTER_HOOKS_SUPPORTED, - TRACING_CHANNEL_SUPPORTED, -} from './denoVersion'; import { denoServeIntegration } from './integrations/deno-serve'; import { denoHttpIntegration } from './integrations/http'; import { denoRedisIntegration } from './integrations/redis'; @@ -71,57 +65,37 @@ export function getDefaultIntegrations(_options: Options): Integration[] { breadcrumbsIntegration(), denoContextIntegration(), denoServeIntegration(), - // node:http client diagnostics channels fire on Deno 2.7.13+ - // server channels arrive at 2.8.0+ - // Include in defaults if at least one is available - ...(HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED || HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED - ? [denoHttpIntegration()] - : []), - // node:diagnostics_channel.tracingChannel exists on Deno 1.44.3+. - ...(TRACING_CHANNEL_SUPPORTED ? [denoRedisIntegration()] : []), - // graphql is gated on tracingChannel rather than the module hook: the - // composed integration also subscribes to graphql v17's native diagnostics - // channels, which need only tracingChannel. The orchestrion implementation - // (graphql v14–16) stays inert until the runtime hook injects those channels. - ...(TRACING_CHANNEL_SUPPORTED ? [graphqlDiagnosticsIntegration()] : []), - // vercel-ai is gated on tracingChannel rather than the module hook, like - // graphql: the composed integration also subscribes to the `ai` SDK v7's - // native `ai:telemetry` channel, which needs only tracingChannel. The - // orchestrion implementation (ai v4–6) stays inert until the runtime hook - // injects those channels. - ...(TRACING_CHANNEL_SUPPORTED ? [vercelAiIntegration()] : []), + denoHttpIntegration(), + denoRedisIntegration(), + graphqlDiagnosticsIntegration(), + vercelAiIntegration(), // orchestrion-based instrumentations. We add a deliberate list here rather // than every channel integration: each one needs a Deno test proving it // records spans. // - // The orchestrion channels may be injected after (or while) the SDK loads, - // so we gate only on whether the feature is possible. If they never load, - // this is a no-op. - ...(MODULE_REGISTER_HOOKS_SUPPORTED - ? [ - amqplibIntegration(), - anthropicIntegration(), - awsIntegration(), - expressIntegration(), - firebaseIntegration(), - genericPoolIntegration(), - googleGenAIIntegration(), - hapiIntegration(), - kafkajsIntegration(), - koaIntegration(), - langChainIntegration(), - langGraphIntegration(), - lruMemoizerIntegration(), - mongodbIntegration(), - mongooseIntegration(), - mysqlIntegration(), - mysql2Integration(), - openaiIntegration(), - postgresIntegration(), - postgresJsIntegration(), - tediousIntegration(), - ] - : []), + // The orchestrion channels may be injected after (or while) the SDK loads. + // If they never load, these are no-ops. + amqplibIntegration(), + anthropicIntegration(), + awsIntegration(), + expressIntegration(), + firebaseIntegration(), + genericPoolIntegration(), + googleGenAIIntegration(), + hapiIntegration(), + kafkajsIntegration(), + koaIntegration(), + langChainIntegration(), + langGraphIntegration(), + lruMemoizerIntegration(), + mongodbIntegration(), + mongooseIntegration(), + mysqlIntegration(), + mysql2Integration(), + openaiIntegration(), + postgresIntegration(), + postgresJsIntegration(), + tediousIntegration(), contextLinesIntegration(), normalizePathsIntegration(), globalHandlersIntegration(), diff --git a/packages/deno/test/deno-http.test.ts b/packages/deno/test/deno-http.test.ts index 3632ea0cb86b..836d219253a5 100644 --- a/packages/deno/test/deno-http.test.ts +++ b/packages/deno/test/deno-http.test.ts @@ -7,11 +7,6 @@ import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts'; import type { DenoClient } from '../build/esm/index.js'; import { getCurrentScope, getGlobalScope, getIsolationScope, init, startSpan } from '../build/esm/index.js'; -import { - DENO_VERSION, - HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED, - HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED, -} from '../build/esm/denoVersion.js'; function resetGlobals(): void { getCurrentScope().clear(); @@ -66,40 +61,18 @@ function withTimeout(p: Promise, ms: number, what: string): Promise { }); } -// Activation gate — split into two skip-mirrored tests so each run exercises -// exactly one assertion. CI on a supported Deno verifies inclusion; CI on an -// unsupported Deno verifies exclusion. Deno.test({ - name: 'denoHttpIntegration: included in default integrations on Deno >= 2.7.13', - ignore: !HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED, + name: 'denoHttpIntegration: included in default integrations', fn() { resetGlobals(); const client = init({ dsn: 'https://username@domain/123' }) as DenoClient; const names = client.getOptions().integrations.map(i => i.name); - assert( - names.includes('DenoHttp'), - `DenoHttp should be a default integration on Deno ${DENO_VERSION.major}.${DENO_VERSION.minor}.${DENO_VERSION.patch}, got ${names.join(', ')}`, - ); - }, -}); - -Deno.test({ - name: 'denoHttpIntegration: NOT in default integrations on Deno < 2.7.13', - ignore: HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED, - fn() { - resetGlobals(); - const client = init({ dsn: 'https://username@domain/123' }) as DenoClient; - const names = client.getOptions().integrations.map(i => i.name); - assert( - !names.includes('DenoHttp'), - `DenoHttp should NOT be in defaults on Deno ${DENO_VERSION.major}.${DENO_VERSION.minor}.${DENO_VERSION.patch} (< 2.7.13), got ${names.join(', ')}`, - ); + assert(names.includes('DenoHttp'), `DenoHttp should be a default integration, got ${names.join(', ')}`); }, }); Deno.test({ name: 'denoHttpIntegration: node:http incoming request creates an http.server transaction', - ignore: !HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED, async fn() { resetGlobals(); const sink = transactionSink(); @@ -141,7 +114,6 @@ Deno.test({ Deno.test({ name: 'denoHttpIntegration: node:http outgoing request creates a child http.client span', - ignore: !HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED, async fn() { resetGlobals(); const sink = transactionSink(); @@ -152,8 +124,8 @@ Deno.test({ traceLifecycle: 'static', }); - // Use Deno.serve for the target so the test does not depend on the - // node:http server side (which only works on Deno 2.8.0+). + // Use Deno.serve for the target so this client test does not depend on + // the node:http server-side instrumentation. const abortController = new AbortController(); let onListen: ((_: unknown) => void) | undefined; const listening = new Promise(resolve => (onListen = resolve)); diff --git a/packages/server-utils/src/orchestrion/runtime/register.ts b/packages/server-utils/src/orchestrion/runtime/register.ts index 0cdf73ac9b5a..51402c21dbc4 100644 --- a/packages/server-utils/src/orchestrion/runtime/register.ts +++ b/packages/server-utils/src/orchestrion/runtime/register.ts @@ -12,11 +12,11 @@ type NodeModule = { register?: typeof register; }; -/** `Module.registerHooks` only became stable in Node 24.13 / 25.1 and Deno 2.8. */ -function hasStableSyncModuleHooks(denoVersionString: string | undefined): boolean { - if (denoVersionString) { - const { major = 0, minor = 0 } = parseSemver(denoVersionString); - return major > 2 || (major === 2 && minor >= 8); +/** `Module.registerHooks` only became stable in Node 24.13 / 25.1. */ +function hasStableSyncModuleHooks(isDeno: boolean): boolean { + // The minimum supported Deno (2.8.3) always has stable sync module hooks. + if (isDeno) { + return true; } const { major = 0, minor = 0 } = parseSemver(process.versions.node ?? '0.0.0'); @@ -41,7 +41,7 @@ export function registerDiagnosticsChannelInjection(): void { } const globalAny = globalThis as { Bun?: unknown; Deno?: { version?: { deno?: string } } }; - const stableSyncHooks = hasStableSyncModuleHooks(globalAny.Deno?.version?.deno); + const stableSyncHooks = hasStableSyncModuleHooks(Boolean(globalAny.Deno)); // `Module.registerHooks` / `Module.register` are newer than the @types/node // we build against, hence the cast.