From 2e6eaf6a7a55e5b003a1ccd876c7a93cd6aa5f3d Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 20 Jul 2026 19:16:09 -0700 Subject: [PATCH 1/5] feat(deno)!: rename orchestrion integrations to match other SDKs Move the `setAsyncLocalStorageAsyncContextStrategy()` into the Deno SDK client init, since it's eventually called by any client that uses even the default set of integrations anyway. The previous renamed integrations are replaced by deprecated wrappers that do not change the integration name, maintained for backwards compatibility. Add tests for all imported integrations. This is the first step towards importing more (eventually, all) orchestrion integrations into the Deno SDK in a straightforward and well-tested way. Re: JS-2634 --- CHANGELOG.md | 8 +++ MIGRATION.md | 13 +++++ .../suites/direct-client-acs/scenario.mjs | 52 +++++++++++++++++++ .../suites/direct-client-acs/test.ts | 38 ++++++++++++++ .../suites/orchestrion-mysql/test.ts | 12 ++--- .../suites/orchestrion-postgres/test.ts | 12 ++--- packages/deno/src/client.ts | 11 ++++ packages/deno/src/index.ts | 22 +++++++- packages/deno/src/integrations/amqplib.ts | 34 ++---------- packages/deno/src/integrations/dataloader.ts | 34 ++---------- packages/deno/src/integrations/deno-serve.ts | 3 -- packages/deno/src/integrations/http.ts | 6 --- packages/deno/src/integrations/knex.ts | 34 ++---------- packages/deno/src/integrations/koa.ts | 33 +++--------- packages/deno/src/integrations/mongo.ts | 34 ++---------- packages/deno/src/integrations/mongoose.ts | 34 ++---------- packages/deno/src/integrations/mysql.ts | 34 ++---------- packages/deno/src/integrations/postgres.ts | 36 ++----------- packages/deno/src/integrations/redis.ts | 13 ++--- packages/deno/src/sdk.ts | 37 +++++++------ .../deno/test/__snapshots__/mod.test.ts.snap | 48 ++++++++--------- 21 files changed, 238 insertions(+), 310 deletions(-) create mode 100644 dev-packages/deno-integration-tests/suites/direct-client-acs/scenario.mjs create mode 100644 dev-packages/deno-integration-tests/suites/direct-client-acs/test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a8f1649628b..86436b9158be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ Work in this release was contributed by @psh4607, @trinitiwowka, @nehaprasad-dev, and @JealousGx. Thank you for your contributions! +- feat(deno)!: Rename several default integrations to match the other SDKs ([#22404](https://github.com/getsentry/sentry-javascript/pull/22404)). The `deno*Integration` exports are kept as deprecated aliases. If you were relying on the names (for example, to disable them), then note that these have changed: + - `DenoAmqplib` => `Amqplib` + - `DenoKoa` => `Koa` + - `DenoMongodb` => `Mongodb` + - `DenoMongoose` => `Mongoose` + - `DenoMysql` => `Mysql` + - `DenoPostgres` => `Postgres` + ## 10.67.0 ### Important Changes diff --git a/MIGRATION.md b/MIGRATION.md index e2d20c5f5118..7e8737512c10 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -527,6 +527,19 @@ The `childProcessIntegration` was split into a `childProcessIntegration` (for `c > **TODO(v11):** Document how the two integrations are configured and what users who customized > `childProcessIntegration` need to change. +### Deno default integrations renamed to match the other SDKs + +Affected SDKs: `@sentry/deno`. + +Several default integrations were renamed to match the names used by the other SDKs. The old `deno*Integration` exports are kept as deprecated aliases. If you relied on the old names (for example, to disable an integration), update them: + +- `DenoAmqplib` => `Amqplib` +- `DenoKoa` => `Koa` +- `DenoMongodb` => `Mongodb` +- `DenoMongoose` => `Mongoose` +- `DenoMysql` => `Mysql` +- `DenoPostgres` => `Postgres` + ## 6. Type Changes - Several public types that used `any` now use `unknown` — including `StackFrame`, `SamplingContext`, diff --git a/dev-packages/deno-integration-tests/suites/direct-client-acs/scenario.mjs b/dev-packages/deno-integration-tests/suites/direct-client-acs/scenario.mjs new file mode 100644 index 000000000000..beec4d932d13 --- /dev/null +++ b/dev-packages/deno-integration-tests/suites/direct-client-acs/scenario.mjs @@ -0,0 +1,52 @@ +// Spawned by test.ts via `deno run`, in a fresh process so nothing else has +// installed the AsyncLocalStorage context strategy. +// +// This builds a `DenoClient` DIRECTLY — `new DenoClient(...)` + `client.init()` +// instead of calling `Sentry.init()`, then drives the mysql orchestrion channel +// The mysql subscriber only binds once the ALS context strategy is installed +// (it waits for the tracing-channel binding), so a nested db span here proves +// `DenoClient.init()` installs that strategy on the direct-construction path. +// Without it, the subscriber never binds and no span is produced. +import { createStackParser, nodeStackLineParser } from '@sentry/core'; +import { DenoClient, getCurrentScope, getDefaultIntegrations, startSpan } from '@sentry/deno'; +import { tracingChannel } from 'node:diagnostics_channel'; + +let nested = false; + +const client = new DenoClient({ + dsn: 'https://username@domain/123', + tracesSampleRate: 1, + integrations: getDefaultIntegrations({}), + stackParser: createStackParser(nodeStackLineParser()), + beforeSendTransaction(event) { + const spans = event.spans ?? []; + if (spans.some(s => s.op === 'db' && s.data?.['sentry.origin'] === 'auto.db.orchestrion.mysql')) { + nested = true; + } + return null; + }, + transport: () => ({ send: () => Promise.resolve({}), flush: () => Promise.resolve(true) }), +}); + +client.init(); +getCurrentScope().setClient(client); + +const channel = tracingChannel('orchestrion:mysql:query'); +const ctx = { + arguments: ['SELECT 1 AS solution'], + self: { config: { host: '127.0.0.1', port: 3306, database: 'mydb', user: 'root' } }, +}; + +startSpan({ name: 'parent', op: 'test' }, () => { + channel.start.runStores(ctx, () => { + channel.end.publish(ctx); + }); + channel.asyncStart.runStores(ctx, () => { + channel.asyncEnd.publish(ctx); + }); +}); + +await client.flush(2000); + +// eslint-disable-next-line no-console +console.log(`SCENARIO nested=${nested}`); diff --git a/dev-packages/deno-integration-tests/suites/direct-client-acs/test.ts b/dev-packages/deno-integration-tests/suites/direct-client-acs/test.ts new file mode 100644 index 000000000000..018e2f8bd594 --- /dev/null +++ b/dev-packages/deno-integration-tests/suites/direct-client-acs/test.ts @@ -0,0 +1,38 @@ +// + +import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts'; +import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts'; + +// A directly-constructed `DenoClient` (no `Sentry.init()`) is a supported path. +// The SDK's own tests use it. It must still install the AsyncLocalStorage +// context strategy, which the channel integrations depend on. We run it in a +// fresh process so no prior `init()` has installed the strategy already, then +// assert a nested mysql span appears (see scenario.mjs for why that proves the +// strategy was installed by `client.init()`). +Deno.test('DenoClient.init installs the AsyncLocalStorage strategy on the direct-construction path', async () => { + const scenario = new URL('./scenario.mjs', import.meta.url); + + // The package root — where `node_modules` (and thus `@sentry/deno`) resolves + // for the spawned `deno run`. + const cwd = new URL('../../', import.meta.url); + + const command = new Deno.Command('deno', { + args: ['run', '--allow-all', scenario.pathname], + cwd: cwd.pathname, + stdout: 'piped', + stderr: 'piped', + }); + + const { code, stdout, stderr } = await command.output(); + const out = new TextDecoder().decode(stdout); + const err = new TextDecoder().decode(stderr); + + assertEquals(code, 0, `scenario exited ${code}\nstdout:\n${out}\nstderr:\n${err}`); + + const line = out.split('\n').find(l => l.startsWith('SCENARIO')) ?? ''; + assert(line, `no SCENARIO line in output:\n${out}\nstderr:\n${err}`); + assert( + line.includes('nested=true'), + `expected a nested mysql span via the direct client path (ACS must be installed by client.init), got: ${line}`, + ); +}); diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-mysql/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-mysql/test.ts index 6e68395b1681..a7c92d675f44 100644 --- a/dev-packages/deno-integration-tests/suites/orchestrion-mysql/test.ts +++ b/dev-packages/deno-integration-tests/suites/orchestrion-mysql/test.ts @@ -54,11 +54,11 @@ function withTimeout(p: Promise, ms: number, what: string): Promise { }); } -Deno.test('denoMysqlIntegration: included in default integrations (Deno 2.8.0+)', () => { +Deno.test('mysql instrumentation: included in default integrations (Deno 2.8.0+)', () => { resetGlobals(); const client = init({ dsn: 'https://username@domain/123' }) as DenoClient; const names = client.getOptions().integrations.map(i => i.name); - assert(names.includes('DenoMysql'), `DenoMysql should be in defaults, got ${names.join(', ')}`); + assert(names.includes('Mysql'), `Mysql should be in defaults, got ${names.join(', ')}`); }); // The orchestrion runtime hook (`@sentry/deno/import`) only works as a FIRST @@ -95,13 +95,7 @@ Deno.test('@sentry/deno/import: transforms mysql so it publishes the orchestrion assert(line.includes('"runtime":["mysql"]'), `expected runtime marker, got: ${line}`); }); -// Exercises the SDK path end-to-end: `init()` wires `denoMysqlIntegration` -// (which installs the AsyncLocalStorage context strategy and subscribes to the -// channel), and we drive the `orchestrion:mysql:query` channel manually — the -// same events the orchestrion transform publishes around `connection.query()` — -// so no live database is needed. Asserting a nested `db` span proves the -// subscriber, the emitted attributes, AND the context-strategy wiring all work. -Deno.test('denoMysqlIntegration: orchestrion:mysql:query channel produces a nested db span', async () => { +Deno.test('mysql instrumentation: orchestrion:mysql:query channel produces a nested db span', async () => { resetGlobals(); const sink = transactionSink(); init({ diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-postgres/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-postgres/test.ts index 8584b222c7d7..4d08d5e2505b 100644 --- a/dev-packages/deno-integration-tests/suites/orchestrion-postgres/test.ts +++ b/dev-packages/deno-integration-tests/suites/orchestrion-postgres/test.ts @@ -54,11 +54,11 @@ function withTimeout(p: Promise, ms: number, what: string): Promise { }); } -Deno.test('denoPostgresIntegration: included in default integrations (Deno 2.8.0+)', () => { +Deno.test('pg instrumentation: included in default integrations (Deno 2.8.0+)', () => { resetGlobals(); const client = init({ dsn: 'https://username@domain/123' }) as DenoClient; const names = client.getOptions().integrations.map(i => i.name); - assert(names.includes('DenoPostgres'), `DenoPostgres should be in defaults, got ${names.join(', ')}`); + assert(names.includes('Postgres'), `Postgres should be in defaults, got ${names.join(', ')}`); }); // The orchestrion runtime hook (`@sentry/deno/import`) only works as a FIRST @@ -95,13 +95,7 @@ Deno.test('@sentry/deno/import: transforms pg so it publishes the orchestrion ch assert(line.includes('"runtime":["pg","pg-pool"]'), `expected runtime marker, got: ${line}`); }); -// Exercises the SDK path end-to-end: `init()` wires `denoPostgresIntegration` -// (which installs the AsyncLocalStorage context strategy and subscribes to the -// channel), and we drive the `orchestrion:pg:query` channel manually — the -// same events the orchestrion transform publishes around `client.query()` — -// so no live database is needed. Asserting a nested `db` span proves the -// subscriber, the emitted attributes, AND the context-strategy wiring all work. -Deno.test('denoPostgresIntegration: orchestrion:pg:query channel produces a nested db span', async () => { +Deno.test('pg instrumentation: orchestrion:pg:query channel produces a nested db span', async () => { resetGlobals(); const sink = transactionSink(); init({ diff --git a/packages/deno/src/client.ts b/packages/deno/src/client.ts index 36886c8d5a5e..f403b9ba6f0d 100644 --- a/packages/deno/src/client.ts +++ b/packages/deno/src/client.ts @@ -1,5 +1,6 @@ import type { ServerRuntimeClientOptions } from '@sentry/core'; import { _INTERNAL_flushLogsBuffer, SDK_VERSION, ServerRuntimeClient } from '@sentry/core'; +import { setAsyncLocalStorageAsyncContextStrategy } from './async'; import type { DenoClientOptions } from './types'; function getHostName(): string | undefined { @@ -67,6 +68,16 @@ export class DenoClient extends ServerRuntimeClient { } } + /** @inheritDoc */ + public init(): void { + // The channel-based default integrations propagate scope across async + // boundaries via Deno's AsyncLocalStorage context strategy. Install it here, + // the setup path both `Sentry.init()` and a directly-constructed client run + // through, so it is in place before the integrations subscribe. + setAsyncLocalStorageAsyncContextStrategy(); + super.init(); + } + /** @inheritDoc */ // @ts-expect-error - PromiseLike is a subset of Promise public async close(timeout?: number | undefined): PromiseLike { diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 58611921021d..ab41f4179bad 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -111,14 +111,32 @@ export { denoHttpIntegration } from './integrations/http'; export type { DenoHttpIntegrationOptions } from './integrations/http'; export { denoRedisIntegration } from './integrations/redis'; export type { DenoRedisIntegrationOptions } from './integrations/redis'; +// The orchestrion channel integrations, re-exported from `@sentry/server-utils`. +// The first six are in the default set; `dataloader` and `knex` are opt-in (add +// them to `integrations` to enable), matching Node. +export { + amqplibChannelIntegration, + dataloaderChannelIntegration, + knexChannelIntegration, + koaChannelIntegration, + mongodbChannelIntegration, + mongooseChannelIntegration, + mysqlChannelIntegration, + postgresChannelIntegration, +} from '@sentry/server-utils/orchestrion'; +// Deprecated aliases kept for back-compat. Each forwards to the shared +// integration above, so its name is the shared name (e.g. `Mysql`), not the old +// `Deno*` name. See each alias's `@deprecated` note. +/* eslint-disable typescript/no-deprecated */ export { denoMysqlIntegration } from './integrations/mysql'; export { denoPostgresIntegration } from './integrations/postgres'; export { denoAmqplibIntegration } from './integrations/amqplib'; -export { denoDataloaderIntegration } from './integrations/dataloader'; -export { denoKnexIntegration } from './integrations/knex'; export { denoKoaIntegration } from './integrations/koa'; export { denoMongoIntegration } from './integrations/mongo'; export { denoMongooseIntegration } from './integrations/mongoose'; +export { denoDataloaderIntegration } from './integrations/dataloader'; +export { denoKnexIntegration } from './integrations/knex'; +/* eslint-enable typescript/no-deprecated */ export { denoContextIntegration } from './integrations/context'; export { globalHandlersIntegration } from './integrations/globalhandlers'; export { normalizePathsIntegration } from './integrations/normalizepaths'; diff --git a/packages/deno/src/integrations/amqplib.ts b/packages/deno/src/integrations/amqplib.ts index 2ff88fd7e939..42a04828f9a3 100644 --- a/packages/deno/src/integrations/amqplib.ts +++ b/packages/deno/src/integrations/amqplib.ts @@ -1,34 +1,10 @@ import { amqplibChannelIntegration } from '@sentry/server-utils/orchestrion'; -import type { Integration, IntegrationFn } from '@sentry/core'; -import { defineIntegration, extendIntegration } from '@sentry/core'; -import { setAsyncLocalStorageAsyncContextStrategy } from '../async'; - -const INTEGRATION_NAME = 'DenoAmqplib' as const; /** - * Create spans for `amqplib` publish/consume operations under Deno. - * - * `amqplib` channels are injected by the orchestrion runtime hook at load time. - * The `@sentry/deno/import` loader must be active for this integration to - * record anything. + * Create spans for `amqplib` publish/consume operations under Deno. Included in + * the default integrations. * - * The channel-subscription logic is shared with the other server runtimes in - * `@sentry/server-utils`. This just installs Deno's `AsyncLocalStorage` context - * strategy (so spans nest under the active span and survive amqplib's internal - * callback dispatch) before delegating. + * @deprecated Use `amqplibChannelIntegration` instead. This alias will be + * removed in a future major. */ -const _denoAmqplibIntegration = (() => { - const inner = amqplibChannelIntegration(); - - return extendIntegration(inner, { - name: INTEGRATION_NAME, - setupOnce() { - setAsyncLocalStorageAsyncContextStrategy(); - }, - }); -}) satisfies IntegrationFn; - -export const denoAmqplibIntegration = defineIntegration(_denoAmqplibIntegration) as () => Integration & { - name: 'DenoAmqplib'; - setupOnce: () => void; -}; +export const denoAmqplibIntegration = amqplibChannelIntegration; diff --git a/packages/deno/src/integrations/dataloader.ts b/packages/deno/src/integrations/dataloader.ts index 37aedf926417..192b38e0cc43 100644 --- a/packages/deno/src/integrations/dataloader.ts +++ b/packages/deno/src/integrations/dataloader.ts @@ -1,34 +1,10 @@ import { dataloaderChannelIntegration } from '@sentry/server-utils/orchestrion'; -import type { Integration, IntegrationFn } from '@sentry/core'; -import { defineIntegration, extendIntegration } from '@sentry/core'; -import { setAsyncLocalStorageAsyncContextStrategy } from '../async'; - -const INTEGRATION_NAME = 'DenoDataloader' as const; /** - * Create spans for `dataloader` load/batch operations under Deno. - * - * `dataloader` channels are injected by the orchestrion runtime hook at load time. - * The `@sentry/deno/import` loader must be active for this integration to - * record anything. + * Create spans for `dataloader` load/batch operations under Deno. Not a default; + * add it to `integrations` to enable. * - * The channel-subscription logic is shared with the other server runtimes in - * `@sentry/server-utils`. This just installs Deno's `AsyncLocalStorage` context - * strategy (so spans nest under the active span and survive dataloader's deferred - * batch dispatch) before delegating. + * @deprecated Use `dataloaderChannelIntegration` instead. This alias will be + * removed in a future major. */ -const _denoDataloaderIntegration = (() => { - const inner = dataloaderChannelIntegration(); - - return extendIntegration(inner, { - name: INTEGRATION_NAME, - setupOnce() { - setAsyncLocalStorageAsyncContextStrategy(); - }, - }); -}) satisfies IntegrationFn; - -export const denoDataloaderIntegration = defineIntegration(_denoDataloaderIntegration) as () => Integration & { - name: 'DenoDataloader'; - setupOnce: () => void; -}; +export const denoDataloaderIntegration = dataloaderChannelIntegration; diff --git a/packages/deno/src/integrations/deno-serve.ts b/packages/deno/src/integrations/deno-serve.ts index 1bde0dad15d8..18af58b8d60d 100644 --- a/packages/deno/src/integrations/deno-serve.ts +++ b/packages/deno/src/integrations/deno-serve.ts @@ -1,6 +1,5 @@ import type { IntegrationFn } from '@sentry/core'; import { debug, defineIntegration } from '@sentry/core'; -import { setAsyncLocalStorageAsyncContextStrategy } from '../async'; import type { RequestHandlerWrapperOptions } from '../wrap-deno-request-handler'; import { wrapDenoRequestHandler } from '../wrap-deno-request-handler'; @@ -63,8 +62,6 @@ const _denoServeIntegration = (() => { return { name: INTEGRATION_NAME, setupOnce() { - setAsyncLocalStorageAsyncContextStrategy(); - const originalServe = Deno.serve; const wrappedServe = instrumentedDenoServe(originalServe); diff --git a/packages/deno/src/integrations/http.ts b/packages/deno/src/integrations/http.ts index a256a8f25bf6..6e11f7abaa99 100644 --- a/packages/deno/src/integrations/http.ts +++ b/packages/deno/src/integrations/http.ts @@ -11,7 +11,6 @@ import { HTTP_ON_CLIENT_REQUEST, HTTP_ON_SERVER_REQUEST, } from '@sentry/core'; -import { setAsyncLocalStorageAsyncContextStrategy } from '../async'; import { DENO_VERSION, HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED, @@ -111,11 +110,6 @@ const _denoHttpIntegration = ((options: DenoHttpIntegrationOptions = {}) => { return; } - // Wire up Deno's AsyncLocalStorage-backed ACS so the server subscription's - // `withIsolationScope(clone, ...)` actually activates the cloned scope. - // Without this, request isolation and span creation degrade silently. - setAsyncLocalStorageAsyncContextStrategy(); - if (HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED) { const { [HTTP_ON_SERVER_REQUEST]: onHttpServerRequest } = getHttpServerSubscriptions({ // `spans` falls through to the client's tracing config when unset. diff --git a/packages/deno/src/integrations/knex.ts b/packages/deno/src/integrations/knex.ts index 84924ed1c2dc..4467a8b8b1c2 100644 --- a/packages/deno/src/integrations/knex.ts +++ b/packages/deno/src/integrations/knex.ts @@ -1,34 +1,10 @@ import { knexChannelIntegration } from '@sentry/server-utils/orchestrion'; -import type { Integration, IntegrationFn } from '@sentry/core'; -import { defineIntegration, extendIntegration } from '@sentry/core'; -import { setAsyncLocalStorageAsyncContextStrategy } from '../async'; - -const INTEGRATION_NAME = 'DenoKnex' as const; /** - * Create spans for `knex` queries under Deno. - * - * `knex` channels are injected by the orchestrion runtime hook at load time. - * The `@sentry/deno/import` loader must be active for this integration to - * record anything. + * Create spans for `knex` queries under Deno. Not a default; add it to + * `integrations` to enable. * - * The channel-subscription logic is shared with the other server runtimes in - * `@sentry/server-utils`. This just installs Deno's - * `AsyncLocalStorage` context strategy (so spans nest under the active - * span and survive knex's internal callback dispatch) before delegating. + * @deprecated Use `knexChannelIntegration` instead. This alias will be removed + * in a future major. */ -const _denoKnexIntegration = (() => { - const inner = knexChannelIntegration(); - - return extendIntegration(inner, { - name: INTEGRATION_NAME, - setupOnce() { - setAsyncLocalStorageAsyncContextStrategy(); - }, - }); -}) satisfies IntegrationFn; - -export const denoKnexIntegration = defineIntegration(_denoKnexIntegration) as () => Integration & { - name: 'DenoKnex'; - setupOnce: () => void; -}; +export const denoKnexIntegration = knexChannelIntegration; diff --git a/packages/deno/src/integrations/koa.ts b/packages/deno/src/integrations/koa.ts index 7f41fd166c43..0ad40fe71909 100644 --- a/packages/deno/src/integrations/koa.ts +++ b/packages/deno/src/integrations/koa.ts @@ -1,31 +1,10 @@ import { koaChannelIntegration } from '@sentry/server-utils/orchestrion'; -import type { KoaChannelIntegrationOptions } from '@sentry/server-utils/orchestrion'; -import type { Integration, IntegrationFn } from '@sentry/core'; -import { defineIntegration, extendIntegration } from '@sentry/core'; -import { setAsyncLocalStorageAsyncContextStrategy } from '../async'; - -const INTEGRATION_NAME = 'DenoKoa' as const; /** - * Create spans for `koa` middleware/router layers under Deno. Requires the - * `@sentry/deno/import` loader. Delegates to the shared subscriber in - * `@sentry/server-utils`, adding Deno's `AsyncLocalStorage` context strategy so - * spans nest under the active HTTP server span. + * Create spans for `koa` middleware/router layers under Deno. Included in the + * default integrations. + * + * @deprecated Use `koaChannelIntegration` instead. This alias will be removed + * in a future major. */ -const _denoKoaIntegration = ((options: KoaChannelIntegrationOptions = {}) => { - const inner = koaChannelIntegration(options); - - return extendIntegration(inner, { - name: INTEGRATION_NAME, - setupOnce() { - setAsyncLocalStorageAsyncContextStrategy(); - }, - }); -}) satisfies IntegrationFn; - -export const denoKoaIntegration = defineIntegration(_denoKoaIntegration) as ( - options?: KoaChannelIntegrationOptions, -) => Integration & { - name: 'DenoKoa'; - setupOnce: () => void; -}; +export const denoKoaIntegration = koaChannelIntegration; diff --git a/packages/deno/src/integrations/mongo.ts b/packages/deno/src/integrations/mongo.ts index 15d6d1e9e028..28146a4d70c2 100644 --- a/packages/deno/src/integrations/mongo.ts +++ b/packages/deno/src/integrations/mongo.ts @@ -1,34 +1,10 @@ import { mongodbChannelIntegration } from '@sentry/server-utils/orchestrion'; -import type { Integration, IntegrationFn } from '@sentry/core'; -import { defineIntegration, extendIntegration } from '@sentry/core'; -import { setAsyncLocalStorageAsyncContextStrategy } from '../async'; - -const INTEGRATION_NAME = 'DenoMongo' as const; /** - * Create spans for `mongodb` queries under Deno. - * - * `mongodb` channels are injected by the orchestrion runtime hook at load time. - * The `@sentry/deno/import` loader must be active for this integration to - * record anything. + * Create spans for `mongodb` queries under Deno. Included in the default + * integrations. * - * The channel-subscription logic is shared with the other server runtimes in - * `@sentry/server-utils`. This just installs Deno's - * `AsyncLocalStorage` context strategy (so spans nest under the active - * span and survive mongodb's internal callback dispatch) before delegating. + * @deprecated Use `mongodbChannelIntegration` instead. This alias will be + * removed in a future major. */ -const _denoMongoIntegration = (() => { - const inner = mongodbChannelIntegration(); - - return extendIntegration(inner, { - name: INTEGRATION_NAME, - setupOnce() { - setAsyncLocalStorageAsyncContextStrategy(); - }, - }); -}) satisfies IntegrationFn; - -export const denoMongoIntegration = defineIntegration(_denoMongoIntegration) as () => Integration & { - name: 'DenoMongo'; - setupOnce: () => void; -}; +export const denoMongoIntegration = mongodbChannelIntegration; diff --git a/packages/deno/src/integrations/mongoose.ts b/packages/deno/src/integrations/mongoose.ts index 86e079cd37e6..67df26fe70ed 100644 --- a/packages/deno/src/integrations/mongoose.ts +++ b/packages/deno/src/integrations/mongoose.ts @@ -1,34 +1,10 @@ import { mongooseChannelIntegration } from '@sentry/server-utils/orchestrion'; -import type { Integration, IntegrationFn } from '@sentry/core'; -import { defineIntegration, extendIntegration } from '@sentry/core'; -import { setAsyncLocalStorageAsyncContextStrategy } from '../async'; - -const INTEGRATION_NAME = 'DenoMongoose' as const; /** - * Create spans for `mongoose` queries under Deno. - * - * `mongoose` channels are injected by the orchestrion runtime hook at load - * time. The `@sentry/deno/import` loader must be active for this integration - * to record anything. + * Create spans for `mongoose` queries under Deno. Included in the default + * integrations. * - * The channel-subscription logic is shared with the other server runtimes in - * `@sentry/server-utils`. This just installs Deno's `AsyncLocalStorage` - * context strategy (so spans nest under the active span and survive mongoose's - * internal callback dispatch) before delegating. + * @deprecated Use `mongooseChannelIntegration` instead. This alias will be + * removed in a future major. */ -const _denoMongooseIntegration = (() => { - const inner = mongooseChannelIntegration(); - - return extendIntegration(inner, { - name: INTEGRATION_NAME, - setupOnce() { - setAsyncLocalStorageAsyncContextStrategy(); - }, - }); -}) satisfies IntegrationFn; - -export const denoMongooseIntegration = defineIntegration(_denoMongooseIntegration) as () => Integration & { - name: 'DenoMongoose'; - setupOnce: () => void; -}; +export const denoMongooseIntegration = mongooseChannelIntegration; diff --git a/packages/deno/src/integrations/mysql.ts b/packages/deno/src/integrations/mysql.ts index 717f26b1369f..4b98805d868a 100644 --- a/packages/deno/src/integrations/mysql.ts +++ b/packages/deno/src/integrations/mysql.ts @@ -1,34 +1,10 @@ import { mysqlChannelIntegration } from '@sentry/server-utils/orchestrion'; -import type { Integration, IntegrationFn } from '@sentry/core'; -import { defineIntegration, extendIntegration } from '@sentry/core'; -import { setAsyncLocalStorageAsyncContextStrategy } from '../async'; - -const INTEGRATION_NAME = 'DenoMysql' as const; /** - * Create spans for `mysql` queries under Deno. - * - * `mysql` channels are injected by the orchestrion runtime hook at load time. - * The `@sentry/deno/import` loader must be active for this integration to - * record anything. + * Create spans for `mysql` queries under Deno. Included in the default + * integrations. * - * The channel-subscription logic is shared with the other server runtimes in - * `@sentry/server-utils`. This just installs Deno's - * `AsyncLocalStorage` context strategy (so spans nest under the active - * span and survive mysql's internal callback dispatch) before delegating. + * @deprecated Use `mysqlChannelIntegration` instead. This alias will be removed + * in a future major. */ -const _denoMysqlIntegration = (() => { - const inner = mysqlChannelIntegration(); - - return extendIntegration(inner, { - name: INTEGRATION_NAME, - setupOnce() { - setAsyncLocalStorageAsyncContextStrategy(); - }, - }); -}) satisfies IntegrationFn; - -export const denoMysqlIntegration = defineIntegration(_denoMysqlIntegration) as () => Integration & { - name: 'DenoMysql'; - setupOnce: () => void; -}; +export const denoMysqlIntegration = mysqlChannelIntegration; diff --git a/packages/deno/src/integrations/postgres.ts b/packages/deno/src/integrations/postgres.ts index b0ccc9b30c95..e1233a441359 100644 --- a/packages/deno/src/integrations/postgres.ts +++ b/packages/deno/src/integrations/postgres.ts @@ -1,36 +1,10 @@ import { postgresChannelIntegration } from '@sentry/server-utils/orchestrion'; -import type { IntegrationFn } from '@sentry/core'; -import { defineIntegration, extendIntegration } from '@sentry/core'; -import { setAsyncLocalStorageAsyncContextStrategy } from '../async'; - -const INTEGRATION_NAME = 'DenoPostgres' as const; - -interface DenoPostgresIntegrationOptions { - /** Whether to skip creating spans for `pg`/`pg-pool` connections. Defaults to `false`. */ - ignoreConnectSpans?: boolean; -} /** - * Create spans for `pg` (node-postgres) queries under Deno. - * - * `pg` channels are injected by the orchestrion runtime hook at load time. - * The `@sentry/deno/import` loader must be active for this integration to - * record anything. + * Create spans for `pg` (node-postgres) queries under Deno. Included in the + * default integrations. * - * The channel-subscription logic is shared with the other server runtimes in - * `@sentry/server-utils`. This just installs Deno's - * `AsyncLocalStorage` context strategy (so spans nest under the active - * span and survive pg's internal callback dispatch) before delegating. + * @deprecated Use `postgresChannelIntegration` instead. This alias will be + * removed in a future major. */ -const _denoPostgresIntegration = ((options?: DenoPostgresIntegrationOptions) => { - const inner = postgresChannelIntegration(options); - - return extendIntegration(inner, { - name: INTEGRATION_NAME, - setupOnce() { - setAsyncLocalStorageAsyncContextStrategy(); - }, - }); -}) satisfies IntegrationFn; - -export const denoPostgresIntegration = defineIntegration(_denoPostgresIntegration); +export const denoPostgresIntegration = postgresChannelIntegration; diff --git a/packages/deno/src/integrations/redis.ts b/packages/deno/src/integrations/redis.ts index 98479eb857cc..713be849562f 100644 --- a/packages/deno/src/integrations/redis.ts +++ b/packages/deno/src/integrations/redis.ts @@ -2,7 +2,6 @@ import type { RedisDiagnosticChannelResponseHook } from '@sentry/server-utils'; import { redisIntegration as redisChannelIntegration } from '@sentry/server-utils'; import type { Integration, IntegrationFn } from '@sentry/core'; import { defineIntegration, extendIntegration } from '@sentry/core'; -import { setAsyncLocalStorageAsyncContextStrategy } from '../async'; const INTEGRATION_NAME = 'DenoRedis' as const; @@ -15,16 +14,12 @@ export interface DenoRedisIntegrationOptions { } const _denoRedisIntegration = ((options: DenoRedisIntegrationOptions = {}) => { - // The diagnostics_channel subscription lives in server-utils so it is shared across runtimes; we - // extend it here to install Deno's AsyncLocalStorage async-context strategy, which the channel - // binding reads via `getTracingChannelBinding`. `extendIntegration` runs the base `setupOnce` - // first, but its subscribe is deferred a tick when no binding exists yet, so the strategy set - // synchronously below is in place by the time the deferred subscribe runs. + // The diagnostics_channel subscription lives in server-utils so it is shared + // across runtimes. The AsyncLocalStorage async-context strategy the channel + // binding depends on is installed once in `init()`, so this wrapper only + // renames the shared integration. return extendIntegration(redisChannelIntegration({ responseHook: options.responseHook }), { name: INTEGRATION_NAME, - setupOnce() { - setAsyncLocalStorageAsyncContextStrategy(); - }, }); }) satisfies IntegrationFn; diff --git a/packages/deno/src/sdk.ts b/packages/deno/src/sdk.ts index d9a75982b667..342f45797393 100644 --- a/packages/deno/src/sdk.ts +++ b/packages/deno/src/sdk.ts @@ -11,6 +11,14 @@ import { requestDataIntegration, stackParserFromStackParserOptions, } from '@sentry/core'; +import { + amqplibChannelIntegration, + koaChannelIntegration, + mongodbChannelIntegration, + mongooseChannelIntegration, + mysqlChannelIntegration, + postgresChannelIntegration, +} from '@sentry/server-utils/orchestrion'; import { DenoClient } from './client'; import { breadcrumbsIntegration } from './integrations/breadcrumbs'; import { denoContextIntegration } from './integrations/context'; @@ -23,12 +31,6 @@ import { } from './denoVersion'; import { denoServeIntegration } from './integrations/deno-serve'; import { denoHttpIntegration } from './integrations/http'; -import { denoAmqplibIntegration } from './integrations/amqplib'; -import { denoKoaIntegration } from './integrations/koa'; -import { denoMongoIntegration } from './integrations/mongo'; -import { denoMongooseIntegration } from './integrations/mongoose'; -import { denoMysqlIntegration } from './integrations/mysql'; -import { denoPostgresIntegration } from './integrations/postgres'; import { denoRedisIntegration } from './integrations/redis'; import { globalHandlersIntegration } from './integrations/globalhandlers'; import { normalizePathsIntegration } from './integrations/normalizepaths'; @@ -60,18 +62,21 @@ export function getDefaultIntegrations(_options: Options): Integration[] { : []), // node:diagnostics_channel.tracingChannel exists on Deno 1.44.3+. ...(TRACING_CHANNEL_SUPPORTED ? [denoRedisIntegration()] : []), - // orchestrion-based instrumentations. - // It's possible that the orchestrion channels will be injected AFTER - // (or in parallel to) loading the SDK, so we only gate on whether the - // feature is possible. If they're never loaded, it'll just be a no-op. + // 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 ? [ - denoAmqplibIntegration(), - denoKoaIntegration(), - denoMongoIntegration(), - denoMongooseIntegration(), - denoMysqlIntegration(), - denoPostgresIntegration(), + amqplibChannelIntegration(), + koaChannelIntegration(), + mongodbChannelIntegration(), + mongooseChannelIntegration(), + mysqlChannelIntegration(), + postgresChannelIntegration(), ] : []), contextLinesIntegration(), diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap index 7da66392eb30..c7e4b9616035 100644 --- a/packages/deno/test/__snapshots__/mod.test.ts.snap +++ b/packages/deno/test/__snapshots__/mod.test.ts.snap @@ -115,12 +115,12 @@ snapshot[`captureException 1`] = ` "DenoServe", "DenoHttp", "DenoRedis", - "DenoAmqplib", - "DenoKoa", - "DenoMongo", - "DenoMongoose", - "DenoMysql", - "DenoPostgres", + "Amqplib", + "Koa", + "Mongo", + "Mongoose", + "Mysql", + "Postgres", "ContextLines", "NormalizePaths", "GlobalHandlers", @@ -196,12 +196,12 @@ snapshot[`captureMessage 1`] = ` "DenoServe", "DenoHttp", "DenoRedis", - "DenoAmqplib", - "DenoKoa", - "DenoMongo", - "DenoMongoose", - "DenoMysql", - "DenoPostgres", + "Amqplib", + "Koa", + "Mongo", + "Mongoose", + "Mysql", + "Postgres", "ContextLines", "NormalizePaths", "GlobalHandlers", @@ -284,12 +284,12 @@ snapshot[`captureMessage twice 1`] = ` "DenoServe", "DenoHttp", "DenoRedis", - "DenoAmqplib", - "DenoKoa", - "DenoMongo", - "DenoMongoose", - "DenoMysql", - "DenoPostgres", + "Amqplib", + "Koa", + "Mongo", + "Mongoose", + "Mysql", + "Postgres", "ContextLines", "NormalizePaths", "GlobalHandlers", @@ -379,12 +379,12 @@ snapshot[`captureMessage twice 2`] = ` "DenoServe", "DenoHttp", "DenoRedis", - "DenoAmqplib", - "DenoKoa", - "DenoMongo", - "DenoMongoose", - "DenoMysql", - "DenoPostgres", + "Amqplib", + "Koa", + "Mongo", + "Mongoose", + "Mysql", + "Postgres", "ContextLines", "NormalizePaths", "GlobalHandlers", From 778952fb4e0b03a2d7e4ca882917083708647dfb Mon Sep 17 00:00:00 2001 From: isaacs Date: Tue, 21 Jul 2026 09:35:02 -0700 Subject: [PATCH 2/5] test(deno): integration test for amqplib --- .../suites/orchestrion-amqplib/test.ts | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 dev-packages/deno-integration-tests/suites/orchestrion-amqplib/test.ts diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-amqplib/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-amqplib/test.ts new file mode 100644 index 000000000000..a713c83e6327 --- /dev/null +++ b/dev-packages/deno-integration-tests/suites/orchestrion-amqplib/test.ts @@ -0,0 +1,110 @@ +// + +import { tracingChannel } from 'node:diagnostics_channel'; +import type { TransactionEvent } from '@sentry/core'; +import type { DenoClient } from '@sentry/deno'; +import { getCurrentScope, getGlobalScope, getIsolationScope, init, startSpan } from '@sentry/deno'; +import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts'; +import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts'; +import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts'; + +function resetGlobals(): void { + getCurrentScope().clear(); + getCurrentScope().setClient(undefined); + getIsolationScope().clear(); + getGlobalScope().clear(); +} + +/** See deno-redis.test.ts — same sink shape, deduped for clarity. */ +function transactionSink(): { + beforeSendTransaction: (event: TransactionEvent) => null; + waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise; +} { + const transactions: TransactionEvent[] = []; + const waiters: { predicate: (e: TransactionEvent) => boolean; resolve: (e: TransactionEvent) => void }[] = []; + return { + beforeSendTransaction(event) { + transactions.push(event); + for (let i = waiters.length - 1; i >= 0; i--) { + const w = waiters[i]!; + if (w.predicate(event)) { + waiters.splice(i, 1); + w.resolve(event); + } + } + return null; + }, + waitFor(predicate) { + const already = transactions.find(predicate); + if (already) return Promise.resolve(already); + return new Promise(resolve => { + waiters.push({ predicate, resolve }); + }); + }, + }; +} + +function withTimeout(p: Promise, ms: number, what: string): Promise { + let timer: ReturnType | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`Timed out waiting for ${what} after ${ms}ms`)), ms); + }); + return Promise.race([p, timeout]).finally(() => { + if (timer !== undefined) clearTimeout(timer); + }); +} + +Deno.test('amqplib instrumentation: included in default integrations (Deno 2.8.0+)', () => { + resetGlobals(); + const client = init({ dsn: 'https://username@domain/123' }) as DenoClient; + const names = client.getOptions().integrations.map(i => i.name); + assert(names.includes('Amqplib'), `Amqplib should be in defaults, got ${names.join(', ')}`); +}); + +// Exercises the SDK path end-to-end: `init()` installs the AsyncLocalStorage +// context strategy and wires the default `amqplibChannelIntegration` (which +// subscribes to the channel), and we drive the `orchestrion:amqplib:publish` +// channel manually — the same events the orchestrion transform publishes around +// `Channel.prototype.publish` — so no live broker is needed. Asserting a nested +// producer `message` span proves the subscriber, the emitted attributes, AND the +// context-strategy wiring all work. +Deno.test('amqplib instrumentation: orchestrion:amqplib:publish channel produces a nested message span', async () => { + resetGlobals(); + const sink = transactionSink(); + init({ + dsn: 'https://username@domain/123', + tracesSampleRate: 1, + beforeSendTransaction: sink.beforeSendTransaction, + }); + + const channel = tracingChannel('orchestrion:amqplib:publish'); + + // `publish(exchange, routingKey, content, options)`; `self.connection` carries + // the server product used for `messaging.system`. + const ctx = { + self: { connection: { serverProperties: { product: 'RabbitMQ' } } }, + arguments: ['my-exchange', 'my.routing.key', new Uint8Array(), { messageId: 'msg-1' }], + }; + + startSpan({ name: 'parent', op: 'test' }, () => { + channel.start.runStores(ctx, () => { + channel.end.publish(ctx); + }); + channel.asyncStart.runStores(ctx, () => { + channel.asyncEnd.publish(ctx); + }); + }); + + const parent = await withTimeout( + sink.waitFor(t => t.transaction === 'parent'), + 5000, + "'parent' transaction", + ); + + const publishSpan = parent.spans?.find(s => s.op === 'message'); + assertExists(publishSpan, `expected a message child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`); + assertEquals(publishSpan!.description, 'publish my-exchange'); + assertEquals(publishSpan!.data?.['messaging.destination.name'], 'my-exchange'); + assertEquals(publishSpan!.data?.['messaging.system'], 'rabbitmq'); + assertEquals(publishSpan!.data?.['sentry.origin'], 'auto.amqplib.orchestrion.publisher'); +}); From 2d80e3e0e112ec184891319c3af59163b48111f1 Mon Sep 17 00:00:00 2001 From: isaacs Date: Tue, 21 Jul 2026 09:35:17 -0700 Subject: [PATCH 3/5] test(deno): integration test for koa --- .../suites/orchestrion-koa/test.ts | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 dev-packages/deno-integration-tests/suites/orchestrion-koa/test.ts diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-koa/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-koa/test.ts new file mode 100644 index 000000000000..8c1564b84853 --- /dev/null +++ b/dev-packages/deno-integration-tests/suites/orchestrion-koa/test.ts @@ -0,0 +1,103 @@ +// + +import { tracingChannel } from 'node:diagnostics_channel'; +import type { TransactionEvent } from '@sentry/core'; +import type { DenoClient } from '@sentry/deno'; +import { getCurrentScope, getGlobalScope, getIsolationScope, init, startSpan } from '@sentry/deno'; +import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts'; +import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts'; +import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts'; + +function resetGlobals(): void { + getCurrentScope().clear(); + getCurrentScope().setClient(undefined); + getIsolationScope().clear(); + getGlobalScope().clear(); +} + +/** See deno-redis.test.ts — same sink shape, deduped for clarity. */ +function transactionSink(): { + beforeSendTransaction: (event: TransactionEvent) => null; + waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise; +} { + const transactions: TransactionEvent[] = []; + const waiters: { predicate: (e: TransactionEvent) => boolean; resolve: (e: TransactionEvent) => void }[] = []; + return { + beforeSendTransaction(event) { + transactions.push(event); + for (let i = waiters.length - 1; i >= 0; i--) { + const w = waiters[i]!; + if (w.predicate(event)) { + waiters.splice(i, 1); + w.resolve(event); + } + } + return null; + }, + waitFor(predicate) { + const already = transactions.find(predicate); + if (already) return Promise.resolve(already); + return new Promise(resolve => { + waiters.push({ predicate, resolve }); + }); + }, + }; +} + +function withTimeout(p: Promise, ms: number, what: string): Promise { + let timer: ReturnType | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`Timed out waiting for ${what} after ${ms}ms`)), ms); + }); + return Promise.race([p, timeout]).finally(() => { + if (timer !== undefined) clearTimeout(timer); + }); +} + +Deno.test('koa instrumentation: included in default integrations (Deno 2.8.0+)', () => { + resetGlobals(); + const client = init({ dsn: 'https://username@domain/123' }) as DenoClient; + const names = client.getOptions().integrations.map(i => i.name); + assert(names.includes('Koa'), `Koa should be in defaults, got ${names.join(', ')}`); +}); + +// Exercises the SDK path end-to-end. Unlike the db integrations, koa's channel +// doesn't build a span directly: its `start` handler wraps the registered +// middleware (arg 0) in a span-creating proxy, and the span opens when that +// middleware later runs under an active span. So we publish `orchestrion:koa:use` +// with a middleware, then invoke the wrapped middleware inside a parent span — +// the same shape `app.use(fn)` then a request produces. Asserting a nested +// `middleware.koa` span proves the subscriber and context wiring work. +Deno.test('koa instrumentation: orchestrion:koa:use channel wraps middleware into a span', async () => { + resetGlobals(); + const sink = transactionSink(); + init({ + dsn: 'https://username@domain/123', + tracesSampleRate: 1, + beforeSendTransaction: sink.beforeSendTransaction, + }); + + function myMiddleware(_context: unknown, next: () => Promise): Promise { + return next(); + } + + // Publishing `start` runs the subscriber, which patches `arguments[0]` in place. + const ctx = { arguments: [myMiddleware] as unknown[] }; + tracingChannel('orchestrion:koa:use').start.publish(ctx); + const wrappedMiddleware = ctx.arguments[0] as typeof myMiddleware; + + await startSpan({ name: 'parent', op: 'test' }, async () => { + await wrappedMiddleware({}, () => Promise.resolve()); + }); + + const parent = await withTimeout( + sink.waitFor(t => t.transaction === 'parent'), + 5000, + "'parent' transaction", + ); + + const koaSpan = parent.spans?.find(s => s.op === 'middleware.koa'); + assertExists(koaSpan, `expected a middleware.koa child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`); + assertEquals(koaSpan!.description, 'myMiddleware'); + assertEquals(koaSpan!.data?.['sentry.origin'], 'auto.http.orchestrion.koa'); +}); From c9d4af024e868529be09d49a5c96e779752828ca Mon Sep 17 00:00:00 2001 From: isaacs Date: Tue, 21 Jul 2026 09:35:38 -0700 Subject: [PATCH 4/5] test(deno): integration test for mongodb --- .../suites/orchestrion-mongo/test.ts | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 dev-packages/deno-integration-tests/suites/orchestrion-mongo/test.ts diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-mongo/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-mongo/test.ts new file mode 100644 index 000000000000..112c8e2ed74c --- /dev/null +++ b/dev-packages/deno-integration-tests/suites/orchestrion-mongo/test.ts @@ -0,0 +1,117 @@ +// + +import { tracingChannel } from 'node:diagnostics_channel'; +import type { TransactionEvent } from '@sentry/core'; +import type { DenoClient } from '@sentry/deno'; +import { getCurrentScope, getGlobalScope, getIsolationScope, init, startSpan } from '@sentry/deno'; +import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts'; +import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts'; +import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts'; + +function resetGlobals(): void { + getCurrentScope().clear(); + getCurrentScope().setClient(undefined); + getIsolationScope().clear(); + getGlobalScope().clear(); +} + +/** See deno-redis.test.ts — same sink shape, deduped for clarity. */ +function transactionSink(): { + beforeSendTransaction: (event: TransactionEvent) => null; + waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise; +} { + const transactions: TransactionEvent[] = []; + const waiters: { predicate: (e: TransactionEvent) => boolean; resolve: (e: TransactionEvent) => void }[] = []; + return { + beforeSendTransaction(event) { + transactions.push(event); + for (let i = waiters.length - 1; i >= 0; i--) { + const w = waiters[i]!; + if (w.predicate(event)) { + waiters.splice(i, 1); + w.resolve(event); + } + } + return null; + }, + waitFor(predicate) { + const already = transactions.find(predicate); + if (already) return Promise.resolve(already); + return new Promise(resolve => { + waiters.push({ predicate, resolve }); + }); + }, + }; +} + +function withTimeout(p: Promise, ms: number, what: string): Promise { + let timer: ReturnType | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`Timed out waiting for ${what} after ${ms}ms`)), ms); + }); + return Promise.race([p, timeout]).finally(() => { + if (timer !== undefined) clearTimeout(timer); + }); +} + +Deno.test('mongodb instrumentation: included in default integrations (Deno 2.8.0+)', () => { + resetGlobals(); + const client = init({ dsn: 'https://username@domain/123' }) as DenoClient; + const names = client.getOptions().integrations.map(i => i.name); + assert(names.includes('Mongo'), `Mongo should be in defaults, got ${names.join(', ')}`); +}); + +// Exercises the SDK path end-to-end: `init()` installs the AsyncLocalStorage +// context strategy and wires the default `mongodbChannelIntegration` (which +// subscribes to the channel), and we drive the `orchestrion:mongodb:command` +// channel manually — the same events the orchestrion transform publishes around +// `Connection.prototype.command` — so no live database is needed. Asserting a +// nested `db` span proves the subscriber, the emitted attributes, AND the +// context-strategy wiring all work. +Deno.test('mongodb instrumentation: orchestrion:mongodb:command channel produces a nested db span', async () => { + resetGlobals(); + const sink = transactionSink(); + init({ + dsn: 'https://username@domain/123', + tracesSampleRate: 1, + beforeSendTransaction: sink.beforeSendTransaction, + }); + + const channel = tracingChannel('orchestrion:mongodb:command'); + + // `arguments[0]` is the namespace, `arguments[1]` the command doc (its first + // key is the operation); `self.address` is the connection's host:port. + const ctx = { + self: { address: '127.0.0.1:27017' }, + arguments: [ + { db: 'mydb', collection: 'users' }, + { find: 'users', filter: { name: 'test' } }, + ], + }; + + startSpan({ name: 'parent', op: 'test' }, () => { + channel.start.runStores(ctx, () => { + channel.end.publish(ctx); + }); + channel.asyncStart.runStores(ctx, () => { + channel.asyncEnd.publish(ctx); + }); + }); + + const parent = await withTimeout( + sink.waitFor(t => t.transaction === 'parent'), + 5000, + "'parent' transaction", + ); + + const mongoSpan = parent.spans?.find(s => s.op === 'db'); + assertExists(mongoSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`); + assertEquals(mongoSpan!.description, 'mongodb.find'); + assertEquals(mongoSpan!.data?.['db.system'], 'mongodb'); + assertEquals(mongoSpan!.data?.['db.name'], 'mydb'); + assertEquals(mongoSpan!.data?.['db.mongodb.collection'], 'users'); + assertEquals(mongoSpan!.data?.['db.operation'], 'find'); + assertEquals(mongoSpan!.data?.['net.peer.name'], '127.0.0.1'); + assertEquals(mongoSpan!.data?.['net.peer.port'], 27017); + assertEquals(mongoSpan!.data?.['sentry.origin'], 'auto.db.orchestrion.mongo'); +}); From f75324fcb5a79fc82c9317a5bfa254415f028807 Mon Sep 17 00:00:00 2001 From: isaacs Date: Tue, 21 Jul 2026 09:35:47 -0700 Subject: [PATCH 5/5] test(deno): integration test for mongoose --- .../suites/orchestrion-mongoose/test.ts | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 dev-packages/deno-integration-tests/suites/orchestrion-mongoose/test.ts diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-mongoose/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-mongoose/test.ts new file mode 100644 index 000000000000..2ca17958006a --- /dev/null +++ b/dev-packages/deno-integration-tests/suites/orchestrion-mongoose/test.ts @@ -0,0 +1,119 @@ +// + +import { tracingChannel } from 'node:diagnostics_channel'; +import type { TransactionEvent } from '@sentry/core'; +import type { DenoClient } from '@sentry/deno'; +import { getCurrentScope, getGlobalScope, getIsolationScope, init, startSpan } from '@sentry/deno'; +import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts'; +import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts'; +import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts'; + +function resetGlobals(): void { + getCurrentScope().clear(); + getCurrentScope().setClient(undefined); + getIsolationScope().clear(); + getGlobalScope().clear(); +} + +/** See deno-redis.test.ts — same sink shape, deduped for clarity. */ +function transactionSink(): { + beforeSendTransaction: (event: TransactionEvent) => null; + waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise; +} { + const transactions: TransactionEvent[] = []; + const waiters: { predicate: (e: TransactionEvent) => boolean; resolve: (e: TransactionEvent) => void }[] = []; + return { + beforeSendTransaction(event) { + transactions.push(event); + for (let i = waiters.length - 1; i >= 0; i--) { + const w = waiters[i]!; + if (w.predicate(event)) { + waiters.splice(i, 1); + w.resolve(event); + } + } + return null; + }, + waitFor(predicate) { + const already = transactions.find(predicate); + if (already) return Promise.resolve(already); + return new Promise(resolve => { + waiters.push({ predicate, resolve }); + }); + }, + }; +} + +function withTimeout(p: Promise, ms: number, what: string): Promise { + let timer: ReturnType | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`Timed out waiting for ${what} after ${ms}ms`)), ms); + }); + return Promise.race([p, timeout]).finally(() => { + if (timer !== undefined) clearTimeout(timer); + }); +} + +Deno.test('mongoose instrumentation: included in default integrations (Deno 2.8.0+)', () => { + resetGlobals(); + const client = init({ dsn: 'https://username@domain/123' }) as DenoClient; + const names = client.getOptions().integrations.map(i => i.name); + assert(names.includes('Mongoose'), `Mongoose should be in defaults, got ${names.join(', ')}`); +}); + +// Exercises the SDK path end-to-end: `init()` installs the AsyncLocalStorage +// context strategy and wires the default `mongooseChannelIntegration` (which +// subscribes to the channel), and we drive the `orchestrion:mongoose:model_save` +// channel manually — the same events the orchestrion transform publishes around +// `Model.prototype.save` — so no live database is needed. Asserting a nested +// `db` span proves the subscriber, the emitted attributes, AND the +// context-strategy wiring all work. +Deno.test('mongoose instrumentation: orchestrion:mongoose:model_save channel produces a nested db span', async () => { + resetGlobals(); + const sink = transactionSink(); + init({ + dsn: 'https://username@domain/123', + tracesSampleRate: 1, + beforeSendTransaction: sink.beforeSendTransaction, + }); + + const channel = tracingChannel('orchestrion:mongoose:model_save'); + + // `self` is the mongoose document; its `constructor` carries the collection + // (name + connection info) and the model name. + const ctx = { + self: { + constructor: { + collection: { name: 'blogposts', conn: { name: 'mydb', user: 'root', host: '127.0.0.1', port: 27017 } }, + modelName: 'BlogPost', + }, + }, + }; + + startSpan({ name: 'parent', op: 'test' }, () => { + channel.start.runStores(ctx, () => { + channel.end.publish(ctx); + }); + channel.asyncStart.runStores(ctx, () => { + channel.asyncEnd.publish(ctx); + }); + }); + + const parent = await withTimeout( + sink.waitFor(t => t.transaction === 'parent'), + 5000, + "'parent' transaction", + ); + + const mongooseSpan = parent.spans?.find(s => s.op === 'db'); + assertExists(mongooseSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`); + assertEquals(mongooseSpan!.description, 'mongoose.BlogPost.save'); + assertEquals(mongooseSpan!.data?.['db.system'], 'mongoose'); + assertEquals(mongooseSpan!.data?.['db.name'], 'mydb'); + assertEquals(mongooseSpan!.data?.['db.mongodb.collection'], 'blogposts'); + assertEquals(mongooseSpan!.data?.['db.operation'], 'save'); + assertEquals(mongooseSpan!.data?.['db.user'], 'root'); + assertEquals(mongooseSpan!.data?.['net.peer.name'], '127.0.0.1'); + assertEquals(mongooseSpan!.data?.['net.peer.port'], 27017); + assertEquals(mongooseSpan!.data?.['sentry.origin'], 'auto.db.orchestrion.mongoose'); +});