From e72237042a1df1410e902708a7132fb3b3c42614 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 13 Jul 2026 15:53:28 -0400 Subject: [PATCH 1/2] feat(node): Rewrite knex instrumentation to orchestrion tracing channels Migrate the knex integration off the vendored `InstrumentationBase` monkey-patch onto a `node:diagnostics_channel` subscriber whose channels are injected by the orchestrion code transform. The OTel path stays as the fallback when orchestrion isn't injected. knex is opt-in (not a default performance integration), so the `@sentry/node` factory picks the path itself in `setupOnce` via `isOrchestrionInjected()` rather than the central `channelIntegrations` swap. `Runner.query` produces the span; `Client.queryBuilder`/`schemaBuilder`/`raw` are wrapped for bookkeeping so the span active at builder-creation time is reused as the parent when the query runs later. Fixes #20757 --- .../suites/tracing/knex/mysql2/test.ts | 14 +- .../suites/tracing/knex/pg/test.ts | 18 +- .../src/integrations/tracing/knex/index.ts | 10 +- .../src/integrations/tracing-channel/knex.ts | 314 ++++++++++++++++++ .../src/orchestrion/config/knex.ts | 53 ++- .../server-utils/src/orchestrion/index.ts | 2 + 6 files changed, 393 insertions(+), 18 deletions(-) create mode 100644 packages/server-utils/src/integrations/tracing-channel/knex.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts index 2d8c798e5e6f..f4d4b5d430cb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts @@ -1,9 +1,11 @@ import { describe, expect } from 'vitest'; +import { isOrchestrionEnabled } from '../../../../utils'; import { createEsmAndCjsTests, describeWithDockerCompose } from '../../../../utils/runner'; describeWithDockerCompose('knex auto instrumentation', { workingDirectory: [__dirname] }, () => { // Update this if another knex version is installed const KNEX_VERSION = '2.5.1'; + const ORIGIN = isOrchestrionEnabled() ? 'auto.db.orchestrion.knex' : 'auto.db.otel.knex'; describe('with `mysql2` client', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { @@ -17,7 +19,7 @@ describeWithDockerCompose('knex auto instrumentation', { workingDirectory: [__di 'db.system': 'mysql2', 'db.name': 'tests', 'db.user': 'root', - 'sentry.origin': 'auto.db.otel.knex', + 'sentry.origin': ORIGIN, 'sentry.op': 'db', 'net.peer.name': 'localhost', 'net.peer.port': 3307, @@ -25,7 +27,7 @@ describeWithDockerCompose('knex auto instrumentation', { workingDirectory: [__di status: 'ok', description: 'create table `User` (`id` int unsigned not null auto_increment primary key, `createdAt` timestamp(3) not null default CURRENT_TIMESTAMP(3), `email` text not null, `name` text not null)', - origin: 'auto.db.otel.knex', + origin: ORIGIN, }), expect.objectContaining({ data: expect.objectContaining({ @@ -33,14 +35,14 @@ describeWithDockerCompose('knex auto instrumentation', { workingDirectory: [__di 'db.system': 'mysql2', 'db.name': 'tests', 'db.user': 'root', - 'sentry.origin': 'auto.db.otel.knex', + 'sentry.origin': ORIGIN, 'sentry.op': 'db', 'net.peer.name': 'localhost', 'net.peer.port': 3307, }), status: 'ok', description: 'insert into `User` (`email`, `name`) values (?, ?)', - origin: 'auto.db.otel.knex', + origin: ORIGIN, }), expect.objectContaining({ @@ -52,12 +54,12 @@ describeWithDockerCompose('knex auto instrumentation', { workingDirectory: [__di 'db.name': 'tests', 'db.statement': 'select * from `User`', 'db.user': 'root', - 'sentry.origin': 'auto.db.otel.knex', + 'sentry.origin': ORIGIN, 'sentry.op': 'db', }), status: 'ok', description: 'select * from `User`', - origin: 'auto.db.otel.knex', + origin: ORIGIN, }), ]), }; diff --git a/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts b/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts index 2a00286f8152..bc63c52637ec 100644 --- a/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts @@ -1,9 +1,11 @@ import { describe, expect } from 'vitest'; +import { isOrchestrionEnabled } from '../../../../utils'; import { createEsmAndCjsTests, describeWithDockerCompose } from '../../../../utils/runner'; describe('knex auto instrumentation', () => { // Update this if another knex version is installed const KNEX_VERSION = '2.5.1'; + const ORIGIN = isOrchestrionEnabled() ? 'auto.db.orchestrion.knex' : 'auto.db.otel.knex'; describeWithDockerCompose('with `pg` client', { workingDirectory: [__dirname] }, () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { @@ -16,7 +18,7 @@ describe('knex auto instrumentation', () => { 'knex.version': KNEX_VERSION, 'db.system': 'postgresql', 'db.name': 'tests', - 'sentry.origin': 'auto.db.otel.knex', + 'sentry.origin': ORIGIN, 'sentry.op': 'db', 'net.peer.name': 'localhost', 'net.peer.port': 5445, @@ -24,14 +26,14 @@ describe('knex auto instrumentation', () => { status: 'ok', description: 'create table "User" ("id" serial primary key, "createdAt" timestamptz(3) not null default CURRENT_TIMESTAMP(3), "email" text not null, "name" text not null)', - origin: 'auto.db.otel.knex', + origin: ORIGIN, }), expect.objectContaining({ data: expect.objectContaining({ 'knex.version': KNEX_VERSION, 'db.system': 'postgresql', 'db.name': 'tests', - 'sentry.origin': 'auto.db.otel.knex', + 'sentry.origin': ORIGIN, 'sentry.op': 'db', 'net.peer.name': 'localhost', 'net.peer.port': 5445, @@ -39,7 +41,7 @@ describe('knex auto instrumentation', () => { status: 'ok', // In the knex-otel spans, the placeholders (e.g., `$1`) are replaced by a `?`. description: 'insert into "User" ("email", "name") values (?, ?)', - origin: 'auto.db.otel.knex', + origin: ORIGIN, }), expect.objectContaining({ @@ -50,12 +52,12 @@ describe('knex auto instrumentation', () => { 'db.system': 'postgresql', 'db.name': 'tests', 'db.statement': 'select * from "User"', - 'sentry.origin': 'auto.db.otel.knex', + 'sentry.origin': ORIGIN, 'sentry.op': 'db', }), status: 'ok', description: 'select * from "User"', - origin: 'auto.db.otel.knex', + origin: ORIGIN, }), expect.objectContaining({ @@ -66,12 +68,12 @@ describe('knex auto instrumentation', () => { 'db.system': 'postgresql', 'db.name': 'tests', 'db.statement': 'select * from "DoesNotExist"', - 'sentry.origin': 'auto.db.otel.knex', + 'sentry.origin': ORIGIN, 'sentry.op': 'db', }), status: 'internal_error', description: 'select * from "DoesNotExist"', - origin: 'auto.db.otel.knex', + origin: ORIGIN, }), ]), }; diff --git a/packages/node/src/integrations/tracing/knex/index.ts b/packages/node/src/integrations/tracing/knex/index.ts index f0c5fe89cbf2..e914bba0a841 100644 --- a/packages/node/src/integrations/tracing/knex/index.ts +++ b/packages/node/src/integrations/tracing/knex/index.ts @@ -2,6 +2,7 @@ import { KnexInstrumentation } from './vendored/instrumentation'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import { generateInstrumentOnce } from '@sentry/node-core'; +import { isOrchestrionInjected, knexChannelIntegration } from '@sentry/server-utils/orchestrion'; const INTEGRATION_NAME = 'Knex' as const; @@ -11,7 +12,14 @@ const _knexIntegration = (() => { return { name: INTEGRATION_NAME, setupOnce() { - instrumentKnex(); + // Prefer the diagnostics-channel subscriber when orchestrion injected its channels; otherwise + // fall back to the vendored OTel instrumentation. `isOrchestrionInjected()` is only reliable by + // `setupOnce` (the runtime injection runs during `Sentry.init()`, after integrations are built). + if (isOrchestrionInjected()) { + knexChannelIntegration().setupOnce?.(); + } else { + instrumentKnex(); + } }, }; }) satisfies IntegrationFn; diff --git a/packages/server-utils/src/integrations/tracing-channel/knex.ts b/packages/server-utils/src/integrations/tracing-channel/knex.ts new file mode 100644 index 000000000000..68f08edbf6f1 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/knex.ts @@ -0,0 +1,314 @@ +// The `@sentry/conventions` db/net attribute keys are deprecated (superseded by newer semconv), but we +// emit them deliberately to preserve parity with what `@opentelemetry/instrumentation-knex` produced. +/* oxlint-disable typescript/no-deprecated */ + +import * as diagnosticsChannel from 'node:diagnostics_channel'; +import type { IntegrationFn, Span, SpanAttributes } from '@sentry/core'; +import { + debug, + defineIntegration, + getActiveSpan, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SPAN_KIND, + SPAN_STATUS_ERROR, + startInactiveSpan, + truncate, + waitForTracingChannelBinding, +} from '@sentry/core'; +import { + DB_NAME, + DB_OPERATION, + DB_STATEMENT, + DB_SYSTEM, + DB_USER, + NET_PEER_NAME, + NET_PEER_PORT, + NET_TRANSPORT, +} from '@sentry/conventions/attributes'; +import { DEBUG_BUILD } from '../../debug-build'; +import { CHANNELS } from '../../orchestrion/channels'; +import { bindTracingChannelToSpan } from '../../tracing-channel'; + +// NOTE: this uses the same name as the OTel integration by design. `@sentry/node`'s `knexIntegration` +// picks this subscriber over the vendored OTel path when orchestrion injection is active. +const INTEGRATION_NAME = 'Knex' as const; +const ORIGIN = 'auto.db.orchestrion.knex'; + +// Max length of the query text captured in `db.statement`; "..." is appended when truncated, so the +// truncated statement caps at 1024 chars (1 KiB), matching `@opentelemetry/instrumentation-knex`. +const MAX_QUERY_LENGTH = 1021; + +// `db.sql.table` is deprecated and not exported by `@sentry/conventions`, so keep it local. Matches +// what `@opentelemetry/instrumentation-knex` emitted. +const ATTR_DB_SQL_TABLE = 'db.sql.table'; + +const DB_SYSTEM_SQLITE = 'sqlite'; +const DB_SYSTEM_POSTGRESQL = 'postgresql'; + +// The span active when a builder is created, reused as the parent when the query runs later (the query +// often executes in a different async context than the builder was created in). +const parentSpanSymbol = Symbol('sentry.orchestrion.knex.parent-span'); + +interface KnexQuery { + sql?: string; + method?: string; + bindings?: unknown[]; +} + +interface KnexConnectionConfig { + filename?: string; + database?: string; + user?: string; + host?: string; + port?: number; + connectionString?: string; +} + +interface KnexBuilder { + _single?: { table?: unknown }; + toString?: () => string; + [parentSpanSymbol]?: Span; +} + +interface KnexClient { + driverName?: string; + config?: { connection?: KnexConnectionConfig }; + _formatQuery?: (sql: string, bindings: unknown[]) => string; + SqlString?: { format: (sql: string, bindings: unknown[]) => string }; +} + +interface KnexRunner { + client?: KnexClient; + builder?: KnexBuilder; +} + +/** Context orchestrion attaches to the `orchestrion:knex:query` channel (wrapping `Runner.query`). */ +interface KnexQueryChannelContext { + // `arguments[0]` is the query object (`{ sql, method, bindings }`). + arguments: [KnexQuery?]; + self?: KnexRunner; + moduleVersion?: string; + result?: unknown; + error?: unknown; +} + +/** Context orchestrion attaches to the `Client.queryBuilder`/`schemaBuilder`/`raw` channels. */ +interface KnexBuilderChannelContext { + arguments: unknown[]; + self?: unknown; + // The builder returned by the wrapped method. + result?: KnexBuilder; +} + +const _knexChannelIntegration = (() => { + return { + name: INTEGRATION_NAME, + setupOnce() { + // `tracingChannel` is unavailable before Node 18.19 so do nothing in that case. + if (!diagnosticsChannel.tracingChannel) { + return; + } + + DEBUG_BUILD && debug.log(`[orchestrion:knex] subscribing to channel "${CHANNELS.KNEX_QUERY}"`); + + waitForTracingChannelBinding(() => { + subscribeBuilder(CHANNELS.KNEX_QUERY_BUILDER); + subscribeBuilder(CHANNELS.KNEX_SCHEMA_BUILDER); + subscribeBuilder(CHANNELS.KNEX_RAW); + subscribeQuery(); + }); + }, + }; +}) satisfies IntegrationFn; + +/** + * Bookkeeping only: stash the span active at builder-creation time onto the returned builder. It is + * read back as the parent when the query runs (see `subscribeQuery`), because the query often executes + * in a different async context. A `Client` builder method is synchronous, so its `result` (the builder) + * is available on the channel's `end`. + */ +function subscribeBuilder(channelName: string): void { + diagnosticsChannel.tracingChannel(channelName).end.subscribe(message => { + const builder = (message as KnexBuilderChannelContext).result; + if (!builder || typeof builder !== 'object' || parentSpanSymbol in builder) { + return; + } + + const activeSpan = getActiveSpan(); + if (!activeSpan) { + return; + } + + Object.defineProperty(builder, parentSpanSymbol, { value: activeSpan }); + }); +} + +function subscribeQuery(): void { + bindTracingChannelToSpan( + diagnosticsChannel.tracingChannel(CHANNELS.KNEX_QUERY), + data => { + const runner = data.self; + const builder = runner?.builder; + + // The builder captures the span active when it was created (see `subscribeBuilder`). + // `onlyIfParent`: only instrument queries that run as part of an existing trace, matching OTel. + const parentSpan = builder?.[parentSpanSymbol] ?? getActiveSpan(); + if (!parentSpan) { + return undefined; + } + + const query = data.arguments[0]; + const client = runner?.client; + const connection = client?.config?.connection; + const connectionString = connection?.connectionString; + const table = extractTableName(builder); + const operation = query?.method; + const name = + connection?.filename || connection?.database || extractDatabaseFromConnectionString(connectionString); + + const attributes: SpanAttributes = { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + 'knex.version': data.moduleVersion, + [DB_SYSTEM]: mapSystem(client?.driverName), + [ATTR_DB_SQL_TABLE]: table, + [DB_OPERATION]: operation, + [DB_USER]: connection?.user, + [DB_NAME]: name, + [NET_PEER_NAME]: connection?.host ?? extractHostFromConnectionString(connectionString), + [NET_PEER_PORT]: connection?.port ?? extractPortFromConnectionString(connectionString), + [NET_TRANSPORT]: connection?.filename === ':memory:' ? 'inproc' : undefined, + [DB_STATEMENT]: query?.sql != null ? truncate(query.sql, MAX_QUERY_LENGTH) : undefined, + }; + + return startInactiveSpan({ + name: getName(name, operation, table) ?? 'knex.query', + kind: SPAN_KIND.CLIENT, + op: 'db', + parentSpan, + attributes, + }); + }, + { + beforeSpanEnd(span, data) { + // knex prefixes query errors with the full SQL (" - "). Strip it so the span + // status message is just the driver error, matching the vendored OTel instrumentation. + if ('error' in data) { + const message = cleanErrorMessage(data); + if (message !== undefined) { + span.setStatus({ code: SPAN_STATUS_ERROR, message }); + } + } + }, + }, + ); +} + +function cleanErrorMessage(data: KnexQueryChannelContext): string | undefined { + const error = data.error; + if (!error || typeof error !== 'object' || typeof (error as { message?: unknown }).message !== 'string') { + return undefined; + } + + const rawMessage = (error as { message: string }).message; + const query = data.arguments[0]; + if (!query?.sql) { + return rawMessage; + } + + try { + const formatter = getFormatter(data.self); + const fullQuery = formatter(query.sql, query.bindings || []); + return rawMessage.replace(`${fullQuery} - `, ''); + } catch { + return rawMessage; + } +} + +function getFormatter(runner: KnexRunner | undefined): (sql: string, bindings: unknown[]) => string { + if (runner) { + const client = runner.client; + if (client) { + if (client._formatQuery) { + return client._formatQuery.bind(client); + } else if (client.SqlString) { + return client.SqlString.format.bind(client.SqlString); + } + } + if (runner.builder?.toString) { + return runner.builder.toString.bind(runner.builder); + } + } + + return () => ''; +} + +function mapSystem(driverName: string | undefined): string | undefined { + if (driverName === 'sqlite3') { + return DB_SYSTEM_SQLITE; + } + if (driverName === 'pg') { + return DB_SYSTEM_POSTGRESQL; + } + return driverName; +} + +function getName(db: string | undefined, operation?: string, table?: string): string | undefined { + // `db` can be undefined when the database can't be determined from the knex config. Guard against + // interpolating it (which would emit the literal "undefined") and let the call site fall back. + if (operation && db) { + return table ? `${operation} ${db}.${table}` : `${operation} ${db}`; + } + return db; +} + +function extractTableName(builder: KnexBuilder | undefined): string | undefined { + const table = builder?._single?.table; + if (table && typeof table === 'object') { + return extractTableName(table as KnexBuilder); + } + return typeof table === 'string' ? table : undefined; +} + +function extractDatabaseFromConnectionString(connectionString: string | undefined): string | undefined { + if (!connectionString) { + return undefined; + } + try { + const db = new URL(connectionString).pathname?.replace(/^\//, ''); + return db || undefined; + } catch { + return undefined; + } +} + +function extractHostFromConnectionString(connectionString: string | undefined): string | undefined { + if (!connectionString) { + return undefined; + } + try { + return new URL(connectionString).hostname || undefined; + } catch { + return undefined; + } +} + +function extractPortFromConnectionString(connectionString: string | undefined): number | undefined { + if (!connectionString) { + return undefined; + } + try { + const port = new URL(connectionString).port; + return port ? parseInt(port, 10) : undefined; + } catch { + return undefined; + } +} + +/** + * EXPERIMENTAL - orchestrion-driven knex integration. + * + * Subscribes to the `orchestrion:knex:*` diagnostics_channels that the orchestrion code transform + * injects into knex's `Runner.query` (span) and `Client.queryBuilder`/`schemaBuilder`/`raw` (parent-span + * bookkeeping). Requires the orchestrion runtime hook or bundler plugin to be active. + */ +export const knexChannelIntegration = defineIntegration(_knexChannelIntegration); diff --git a/packages/server-utils/src/orchestrion/config/knex.ts b/packages/server-utils/src/orchestrion/config/knex.ts index f268e2f3df4c..5e9ef9d7c7c0 100644 --- a/packages/server-utils/src/orchestrion/config/knex.ts +++ b/packages/server-utils/src/orchestrion/config/knex.ts @@ -1,6 +1,53 @@ import type { InstrumentationConfig } from '@apm-js-collab/code-transformer'; -// TODO: Stub for the `knex` orchestrion integration (ports `@opentelemetry/instrumentation-knex`). -export const knexConfig: InstrumentationConfig[] = []; +const MODULE_NAME = 'knex'; -export const knexChannels = {} as const; +// knex ships different file layouts across versions (see `SUPPORTED_VERSIONS` in the vendored OTel +// instrumentation). Modern builds keep the runner under `lib/execution`, older ones under `lib`, and +// the source-tree build under `src`. A file that doesn't exist for a given version simply never +// matches, so overlapping ranges are safe. +const RUNNER_FILES = [ + { filePath: 'lib/execution/runner.js', versionRange: '>=0.22.0 <4' }, + { filePath: 'lib/runner.js', versionRange: '>=0.10.0 <0.22.0' }, + { filePath: 'src/runner.js', versionRange: '>=0.18.0 <0.19.0' }, +]; + +const CLIENT_FILES = [ + { filePath: 'lib/client.js', versionRange: '>=0.10.0 <4' }, + { filePath: 'src/client.js', versionRange: '>=0.18.0 <0.19.0' }, +]; + +// The three `Client` methods that create a builder. Wrapping them lets the subscriber capture the +// span active at builder-creation time, which is reused as the parent when the query runs later +// (often in a different async context). +const CLIENT_METHODS = ['queryBuilder', 'schemaBuilder', 'raw'] as const; + +function runnerQuery(filePath: string, versionRange: string): InstrumentationConfig { + return { + channelName: 'query', + module: { name: MODULE_NAME, versionRange, filePath }, + functionQuery: { className: 'Runner', methodName: 'query', kind: 'Async' }, + }; +} + +function clientMethod(methodName: string, filePath: string, versionRange: string): InstrumentationConfig { + return { + channelName: methodName, + module: { name: MODULE_NAME, versionRange, filePath }, + functionQuery: { className: 'Client', methodName, kind: 'Sync' }, + }; +} + +export const knexConfig: InstrumentationConfig[] = [ + ...RUNNER_FILES.map(({ filePath, versionRange }) => runnerQuery(filePath, versionRange)), + ...CLIENT_FILES.flatMap(({ filePath, versionRange }) => + CLIENT_METHODS.map(methodName => clientMethod(methodName, filePath, versionRange)), + ), +]; + +export const knexChannels = { + KNEX_QUERY: 'orchestrion:knex:query', + KNEX_QUERY_BUILDER: 'orchestrion:knex:queryBuilder', + KNEX_SCHEMA_BUILDER: 'orchestrion:knex:schemaBuilder', + KNEX_RAW: 'orchestrion:knex:raw', +} as const; diff --git a/packages/server-utils/src/orchestrion/index.ts b/packages/server-utils/src/orchestrion/index.ts index 6cbb67f942d6..f9baeddf1406 100644 --- a/packages/server-utils/src/orchestrion/index.ts +++ b/packages/server-utils/src/orchestrion/index.ts @@ -10,6 +10,7 @@ import { import { hapiChannelIntegration } from '../integrations/tracing-channel/hapi'; import { ioredisChannelIntegration } from '../integrations/tracing-channel/ioredis'; import { kafkajsChannelIntegration } from '../integrations/tracing-channel/kafkajs'; +import { knexChannelIntegration } from '../integrations/tracing-channel/knex'; import { lruMemoizerChannelIntegration } from '../integrations/tracing-channel/lru-memoizer'; import { mysqlChannelIntegration } from '../integrations/tracing-channel/mysql'; import { openaiChannelIntegration } from '../integrations/tracing-channel/openai'; @@ -32,6 +33,7 @@ export { hapiChannelIntegration, ioredisChannelIntegration, kafkajsChannelIntegration, + knexChannelIntegration, lruMemoizerChannelIntegration, mysqlChannelIntegration, openaiChannelIntegration, From 0fe09bf4322e9b5413dbf930b5342e637d64ac54 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 14 Jul 2026 12:21:24 -0400 Subject: [PATCH 2/2] feat(deno): Expose knex orchestrion integration Add an opt-in `denoKnexIntegration` that reuses the shared knex channel subscriber from `@sentry/server-utils` and installs Deno's AsyncLocalStorage context strategy, mirroring the other Deno DB integrations. Exported but kept out of the default set, matching knex being opt-in on Node. --- packages/deno/src/index.ts | 1 + packages/deno/src/integrations/knex.ts | 34 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 packages/deno/src/integrations/knex.ts diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 927871f883ec..44cfd21ebcb8 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -115,6 +115,7 @@ 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 { denoContextIntegration } from './integrations/context'; export { globalHandlersIntegration } from './integrations/globalhandlers'; export { normalizePathsIntegration } from './integrations/normalizepaths'; diff --git a/packages/deno/src/integrations/knex.ts b/packages/deno/src/integrations/knex.ts new file mode 100644 index 000000000000..84924ed1c2dc --- /dev/null +++ b/packages/deno/src/integrations/knex.ts @@ -0,0 +1,34 @@ +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. + * + * 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. + */ +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; +};