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-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'); +}); 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'); +}); 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'); +}); 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'); +}); 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",