From b988ec55e7a47cf35bc27d935be71828f0044dbe Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 28 Jul 2026 11:53:44 +0200 Subject: [PATCH] feat(node)!: Remove iitm-based instrumentations from `@sentry/node` All performance integrations are now channel-based via `@sentry/server-utils`, so the old import-in-the-middle / OpenTelemetry vendored instrumentations are removed: - Remove the `registerEsmLoaderHooks` option and the `import-in-the-middle` dependency; the SDK no longer registers ESM loader hooks. - Make `knexIntegration` and `dataloaderIntegration` channel-only (no OTel fallback on runtimes without diagnostics-channel injection). - Drop support for Fastify `<3.21.0`. - Remove the deprecated `SentryHttpInstrumentation` and `SentryNodeFetchInstrumentation` classes. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 4 + .../suites/esm/import-in-the-middle/app.mjs | 22 - .../esm/import-in-the-middle/sub-module.mjs | 2 - .../suites/esm/import-in-the-middle/test.ts | 15 - packages/node/package.json | 3 +- packages/node/src/index.ts | 4 +- .../http/SentryHttpInstrumentation.ts | 32 - .../node/src/integrations/http/constants.ts | 1 - .../SentryNodeFetchInstrumentation.ts | 188 ------ .../tracing/InstrumentationNodeModuleFile.ts | 45 -- .../src/integrations/tracing/amqplib/index.ts | 6 - .../tracing/amqplib/vendored/amqplib-types.ts | 47 -- .../amqplib/vendored/instrumentation.ts | 143 ----- .../tracing/amqplib/vendored/patches.ts | 299 ---------- .../tracing/amqplib/vendored/semconv.ts | 45 -- .../tracing/amqplib/vendored/types.ts | 68 --- .../tracing/amqplib/vendored/utils.ts | 206 ------- .../tracing/anthropic-ai/index.ts | 9 - .../tracing/anthropic-ai/instrumentation.ts | 111 ---- .../integrations/tracing/dataloader/index.ts | 45 -- .../dataloader/vendored/instrumentation.ts | 295 --------- .../tracing/dataloader/vendored/types.ts | 35 -- .../node/src/integrations/tracing/express.ts | 57 +- .../src/integrations/tracing/fastify/index.ts | 23 +- .../tracing/fastify/v3/constants.ts | 34 -- .../fastify/v3/enums/AttributeNames.ts | 34 -- .../tracing/fastify/v3/instrumentation.ts | 334 ----------- .../tracing/fastify/v3/internal-types.ts | 24 - .../integrations/tracing/fastify/v3/types.ts | 41 -- .../integrations/tracing/fastify/v3/utils.ts | 137 ----- .../integrations/tracing/firebase/firebase.ts | 6 - .../integrations/tracing/firebase/index.ts | 1 - .../firebase/otel/firebaseInstrumentation.ts | 31 - .../tracing/firebase/otel/index.ts | 3 - .../firebase/otel/patches/firestore.ts | 272 --------- .../firebase/otel/patches/functions.ts | 191 ------ .../tracing/firebase/otel/types.ts | 144 ----- .../integrations/tracing/genericPool/index.ts | 6 - .../vendored/generic-pool-types.ts | 8 - .../genericPool/vendored/instrumentation.ts | 160 ----- .../tracing/google-genai/index.ts | 9 - .../tracing/google-genai/instrumentation.ts | 98 --- .../src/integrations/tracing/graphql/index.ts | 48 -- .../tracing/graphql/vendored/enum.ts | 48 -- .../graphql/vendored/enums/AttributeNames.ts | 18 - .../tracing/graphql/vendored/graphql-types.ts | 26 - .../graphql/vendored/instrumentation.ts | 478 --------------- .../graphql/vendored/internal-types.ts | 106 ---- .../tracing/graphql/vendored/symbols.ts | 12 - .../tracing/graphql/vendored/types.ts | 48 -- .../tracing/graphql/vendored/utils.ts | 372 ------------ .../src/integrations/tracing/hapi/index.ts | 6 - .../hapi/vendored/enums/AttributeNames.ts | 15 - .../tracing/hapi/vendored/hapi-types.ts | 86 --- .../tracing/hapi/vendored/instrumentation.ts | 316 ---------- .../tracing/hapi/vendored/internal-types.ts | 62 -- .../tracing/hapi/vendored/utils.ts | 123 ---- .../node/src/integrations/tracing/index.ts | 11 +- .../src/integrations/tracing/kafka/index.ts | 6 - .../tracing/kafka/vendored/instrumentation.ts | 332 ----------- .../tracing/kafka/vendored/kafkajs-types.ts | 91 --- .../tracing/kafka/vendored/semconv.ts | 77 --- .../tracing/kafka/vendored/utils.ts | 180 ------ .../src/integrations/tracing/knex/index.ts | 41 -- .../tracing/knex/vendored/instrumentation.ts | 184 ------ .../tracing/knex/vendored/semconv.ts | 17 - .../tracing/knex/vendored/utils.ts | 90 --- .../src/integrations/tracing/koa/index.ts | 22 - .../koa/vendored/enums/AttributeNames.ts | 13 - .../tracing/koa/vendored/instrumentation.ts | 186 ------ .../tracing/koa/vendored/internal-types.ts | 53 -- .../tracing/koa/vendored/types.ts | 23 - .../tracing/koa/vendored/utils.ts | 54 -- .../integrations/tracing/langchain/index.ts | 9 - .../tracing/langchain/instrumentation.ts | 246 -------- .../integrations/tracing/langgraph/index.ts | 9 - .../tracing/langgraph/instrumentation.ts | 119 ---- .../integrations/tracing/lrumemoizer/index.ts | 6 - .../lrumemoizer/vendored/instrumentation.ts | 58 -- .../src/integrations/tracing/mongo/index.ts | 6 - .../tracing/mongo/vendored/instrumentation.ts | 159 ----- .../tracing/mongo/vendored/internal-types.ts | 137 ----- .../tracing/mongo/vendored/patches.ts | 238 -------- .../tracing/mongo/vendored/utils.ts | 67 --- .../integrations/tracing/mongoose/index.ts | 6 - .../mongoose/vendored/mongoose-types.ts | 42 -- .../tracing/mongoose/vendored/mongoose.ts | 333 ----------- .../tracing/mongoose/vendored/utils.ts | 61 -- .../src/integrations/tracing/mysql/index.ts | 6 - .../tracing/mysql/vendored/instrumentation.ts | 270 --------- .../tracing/mysql/vendored/mysql-types.ts | 103 ---- .../tracing/mysql/vendored/semconv.ts | 34 -- .../tracing/mysql/vendored/utils.ts | 60 -- .../src/integrations/tracing/mysql2/index.ts | 6 - .../mysql2/vendored/instrumentation.ts | 174 ------ .../tracing/mysql2/vendored/mysql2-types.ts | 47 -- .../tracing/mysql2/vendored/semconv.ts | 11 - .../tracing/mysql2/vendored/utils.ts | 119 ---- .../src/integrations/tracing/openai/index.ts | 9 - .../tracing/openai/instrumentation.ts | 125 ---- .../integrations/tracing/postgres/index.ts | 16 - .../postgres/vendored/enums/AttributeNames.ts | 15 - .../postgres/vendored/enums/SpanNames.ts | 15 - .../postgres/vendored/instrumentation.ts | 326 ---------- .../postgres/vendored/internal-types.ts | 59 -- .../postgres/vendored/pg-pool-types.ts | 34 -- .../tracing/postgres/vendored/pg-types.ts | 72 --- .../tracing/postgres/vendored/semconv.ts | 29 - .../tracing/postgres/vendored/types.ts | 21 - .../tracing/postgres/vendored/utils.ts | 248 -------- .../src/integrations/tracing/postgresjs.ts | 401 ------------- .../redis/vendored/ioredis-instrumentation.ts | 210 ------- .../redis/vendored/redis-instrumentation.ts | 562 ------------------ .../tracing/redis/vendored/semconv.ts | 21 - .../src/integrations/tracing/tedious/index.ts | 6 - .../tedious/vendored/instrumentation.ts | 214 ------- .../tracing/tedious/vendored/semconv.ts | 34 -- .../tracing/tedious/vendored/tedious-types.ts | 30 - .../tracing/tedious/vendored/utils.ts | 47 -- .../tracing/vercelai/constants.ts | 1 - .../integrations/tracing/vercelai/index.ts | 5 - .../tracing/vercelai/instrumentation.ts | 306 ---------- .../integrations/tracing/vercelai/types.ts | 59 -- packages/node/src/sdk/esmLoader.ts | 30 - packages/node/src/sdk/index.ts | 5 - packages/node/src/sdk/initOtel.ts | 3 - packages/node/src/types.ts | 10 - packages/node/src/utils/detection.ts | 16 - .../integrations/tracing/firebase.test.ts | 74 --- .../tracing/graphql/addSpanSource.test.ts | 53 -- .../test/integrations/tracing/hapi.test.ts | 90 --- .../instrumentationNodeModuleFile.test.ts | 39 -- .../test/integrations/tracing/koa.test.ts | 76 --- .../test/integrations/tracing/mysql2.test.ts | 37 -- .../integrations/tracing/postgresjs.test.ts | 411 ------------- .../test/integrations/tracing/tedious.test.ts | 28 - .../tracing/vercelai/instrumentation.test.ts | 295 --------- 137 files changed, 13 insertions(+), 12727 deletions(-) delete mode 100644 dev-packages/node-integration-tests/suites/esm/import-in-the-middle/app.mjs delete mode 100644 dev-packages/node-integration-tests/suites/esm/import-in-the-middle/sub-module.mjs delete mode 100644 dev-packages/node-integration-tests/suites/esm/import-in-the-middle/test.ts delete mode 100644 packages/node/src/integrations/http/constants.ts delete mode 100644 packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/InstrumentationNodeModuleFile.ts delete mode 100644 packages/node/src/integrations/tracing/amqplib/index.ts delete mode 100644 packages/node/src/integrations/tracing/amqplib/vendored/amqplib-types.ts delete mode 100644 packages/node/src/integrations/tracing/amqplib/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/amqplib/vendored/patches.ts delete mode 100644 packages/node/src/integrations/tracing/amqplib/vendored/semconv.ts delete mode 100644 packages/node/src/integrations/tracing/amqplib/vendored/types.ts delete mode 100644 packages/node/src/integrations/tracing/amqplib/vendored/utils.ts delete mode 100644 packages/node/src/integrations/tracing/anthropic-ai/index.ts delete mode 100644 packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/dataloader/index.ts delete mode 100644 packages/node/src/integrations/tracing/dataloader/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/dataloader/vendored/types.ts delete mode 100644 packages/node/src/integrations/tracing/fastify/v3/constants.ts delete mode 100644 packages/node/src/integrations/tracing/fastify/v3/enums/AttributeNames.ts delete mode 100644 packages/node/src/integrations/tracing/fastify/v3/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/fastify/v3/internal-types.ts delete mode 100644 packages/node/src/integrations/tracing/fastify/v3/types.ts delete mode 100644 packages/node/src/integrations/tracing/fastify/v3/utils.ts delete mode 100644 packages/node/src/integrations/tracing/firebase/firebase.ts delete mode 100644 packages/node/src/integrations/tracing/firebase/index.ts delete mode 100644 packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/firebase/otel/index.ts delete mode 100644 packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts delete mode 100644 packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts delete mode 100644 packages/node/src/integrations/tracing/firebase/otel/types.ts delete mode 100644 packages/node/src/integrations/tracing/genericPool/index.ts delete mode 100644 packages/node/src/integrations/tracing/genericPool/vendored/generic-pool-types.ts delete mode 100644 packages/node/src/integrations/tracing/genericPool/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/google-genai/index.ts delete mode 100644 packages/node/src/integrations/tracing/google-genai/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/index.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/enum.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/enums/AttributeNames.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/graphql-types.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/internal-types.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/symbols.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/types.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/utils.ts delete mode 100644 packages/node/src/integrations/tracing/hapi/vendored/enums/AttributeNames.ts delete mode 100644 packages/node/src/integrations/tracing/hapi/vendored/hapi-types.ts delete mode 100644 packages/node/src/integrations/tracing/hapi/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/hapi/vendored/internal-types.ts delete mode 100644 packages/node/src/integrations/tracing/hapi/vendored/utils.ts delete mode 100644 packages/node/src/integrations/tracing/kafka/index.ts delete mode 100644 packages/node/src/integrations/tracing/kafka/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/kafka/vendored/kafkajs-types.ts delete mode 100644 packages/node/src/integrations/tracing/kafka/vendored/semconv.ts delete mode 100644 packages/node/src/integrations/tracing/kafka/vendored/utils.ts delete mode 100644 packages/node/src/integrations/tracing/knex/index.ts delete mode 100644 packages/node/src/integrations/tracing/knex/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/knex/vendored/semconv.ts delete mode 100644 packages/node/src/integrations/tracing/knex/vendored/utils.ts delete mode 100644 packages/node/src/integrations/tracing/koa/vendored/enums/AttributeNames.ts delete mode 100644 packages/node/src/integrations/tracing/koa/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/koa/vendored/internal-types.ts delete mode 100644 packages/node/src/integrations/tracing/koa/vendored/types.ts delete mode 100644 packages/node/src/integrations/tracing/koa/vendored/utils.ts delete mode 100644 packages/node/src/integrations/tracing/langchain/index.ts delete mode 100644 packages/node/src/integrations/tracing/langchain/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/langgraph/index.ts delete mode 100644 packages/node/src/integrations/tracing/langgraph/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/lrumemoizer/index.ts delete mode 100644 packages/node/src/integrations/tracing/lrumemoizer/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/mongo/index.ts delete mode 100644 packages/node/src/integrations/tracing/mongo/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/mongo/vendored/internal-types.ts delete mode 100644 packages/node/src/integrations/tracing/mongo/vendored/patches.ts delete mode 100644 packages/node/src/integrations/tracing/mongo/vendored/utils.ts delete mode 100644 packages/node/src/integrations/tracing/mongoose/index.ts delete mode 100644 packages/node/src/integrations/tracing/mongoose/vendored/mongoose-types.ts delete mode 100644 packages/node/src/integrations/tracing/mongoose/vendored/mongoose.ts delete mode 100644 packages/node/src/integrations/tracing/mongoose/vendored/utils.ts delete mode 100644 packages/node/src/integrations/tracing/mysql/index.ts delete mode 100644 packages/node/src/integrations/tracing/mysql/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/mysql/vendored/mysql-types.ts delete mode 100644 packages/node/src/integrations/tracing/mysql/vendored/semconv.ts delete mode 100644 packages/node/src/integrations/tracing/mysql/vendored/utils.ts delete mode 100644 packages/node/src/integrations/tracing/mysql2/index.ts delete mode 100644 packages/node/src/integrations/tracing/mysql2/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/mysql2/vendored/mysql2-types.ts delete mode 100644 packages/node/src/integrations/tracing/mysql2/vendored/semconv.ts delete mode 100644 packages/node/src/integrations/tracing/mysql2/vendored/utils.ts delete mode 100644 packages/node/src/integrations/tracing/openai/index.ts delete mode 100644 packages/node/src/integrations/tracing/openai/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/postgres/index.ts delete mode 100644 packages/node/src/integrations/tracing/postgres/vendored/enums/AttributeNames.ts delete mode 100644 packages/node/src/integrations/tracing/postgres/vendored/enums/SpanNames.ts delete mode 100644 packages/node/src/integrations/tracing/postgres/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/postgres/vendored/internal-types.ts delete mode 100644 packages/node/src/integrations/tracing/postgres/vendored/pg-pool-types.ts delete mode 100644 packages/node/src/integrations/tracing/postgres/vendored/pg-types.ts delete mode 100644 packages/node/src/integrations/tracing/postgres/vendored/semconv.ts delete mode 100644 packages/node/src/integrations/tracing/postgres/vendored/types.ts delete mode 100644 packages/node/src/integrations/tracing/postgres/vendored/utils.ts delete mode 100644 packages/node/src/integrations/tracing/postgresjs.ts delete mode 100644 packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/redis/vendored/redis-instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/redis/vendored/semconv.ts delete mode 100644 packages/node/src/integrations/tracing/tedious/index.ts delete mode 100644 packages/node/src/integrations/tracing/tedious/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/tedious/vendored/semconv.ts delete mode 100644 packages/node/src/integrations/tracing/tedious/vendored/tedious-types.ts delete mode 100644 packages/node/src/integrations/tracing/tedious/vendored/utils.ts delete mode 100644 packages/node/src/integrations/tracing/vercelai/constants.ts delete mode 100644 packages/node/src/integrations/tracing/vercelai/index.ts delete mode 100644 packages/node/src/integrations/tracing/vercelai/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/vercelai/types.ts delete mode 100644 packages/node/src/sdk/esmLoader.ts delete mode 100644 packages/node/src/utils/detection.ts delete mode 100644 packages/node/test/integrations/tracing/firebase.test.ts delete mode 100644 packages/node/test/integrations/tracing/graphql/addSpanSource.test.ts delete mode 100644 packages/node/test/integrations/tracing/hapi.test.ts delete mode 100644 packages/node/test/integrations/tracing/instrumentationNodeModuleFile.test.ts delete mode 100644 packages/node/test/integrations/tracing/koa.test.ts delete mode 100644 packages/node/test/integrations/tracing/mysql2.test.ts delete mode 100644 packages/node/test/integrations/tracing/postgresjs.test.ts delete mode 100644 packages/node/test/integrations/tracing/tedious.test.ts delete mode 100644 packages/node/test/integrations/tracing/vercelai/instrumentation.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 86436b9158be..c40e6fb165b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ Work in this release was contributed by @psh4607, @trinitiwowka, @nehaprasad-dev - `DenoMongoose` => `Mongoose` - `DenoMysql` => `Mysql` - `DenoPostgres` => `Postgres` +- feat(node)!: Remove `import-in-the-middle`-based instrumentations from `@sentry/node`. All performance integrations are now channel-based via `@sentry/server-utils`. As part of this: + - The `registerEsmLoaderHooks` option was removed from `NodeOptions`. The SDK no longer registers `import-in-the-middle` ESM loader hooks, and `import-in-the-middle` is no longer a dependency. + - `knexIntegration` and `dataloaderIntegration` are now channel-only and no longer fall back to OpenTelemetry instrumentation on runtimes without diagnostics-channel injection. + - Support for Fastify `<3.21.0` was dropped. ## 10.67.0 diff --git a/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/app.mjs b/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/app.mjs deleted file mode 100644 index 3f58aba41357..000000000000 --- a/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/app.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; -import * as iitm from 'import-in-the-middle'; - -new iitm.Hook((_, name) => { - if (name !== 'http') { - throw new Error(`'http' should be the only hooked modules but we just hooked '${name}'`); - } -}); - -Sentry.init({ - traceLifecycle: 'static', - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - transport: loggingTransport, -}); - -(async () => { - await import('./sub-module.mjs'); - await import('http'); - await import('os'); -})(); diff --git a/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/sub-module.mjs b/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/sub-module.mjs deleted file mode 100644 index 9940c57857eb..000000000000 --- a/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/sub-module.mjs +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-console -console.assert(true); diff --git a/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/test.ts b/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/test.ts deleted file mode 100644 index 99dea0e9193a..000000000000 --- a/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { spawnSync } from 'child_process'; -import { join } from 'path'; -import { afterAll, describe, expect, test } from 'vitest'; -import { cleanupChildProcesses } from '../../../utils/runner'; - -afterAll(() => { - cleanupChildProcesses(); -}); - -describe('import-in-the-middle', () => { - test('should only instrument modules that we have instrumentation for', () => { - const result = spawnSync('node', [join(__dirname, 'app.mjs')], { encoding: 'utf-8' }); - expect(result.stderr).not.toMatch('should be the only hooked modules but we just hooked'); - }); -}); diff --git a/packages/node/package.json b/packages/node/package.json index 62f389db2b8d..cfc950d8669a 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -105,8 +105,7 @@ "@sentry/core": "10.67.0", "@sentry/opentelemetry": "10.67.0", "@sentry/server-utils": "10.67.0", - "@sentry/bundler-plugins": "10.67.0", - "import-in-the-middle": "^3.0.0" + "@sentry/bundler-plugins": "10.67.0" }, "devDependencies": { "@types/node": "^18.19.1" diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 69ef5b6067be..850a00a549a5 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -6,6 +6,7 @@ export { fastifyIntegration, setupFastifyErrorHandler } from './integrations/tra export { amqplibIntegration, anthropicIntegration as anthropicAIIntegration, + dataloaderIntegration, expressIntegration, firebaseIntegration, genericPoolIntegration, @@ -13,6 +14,7 @@ export { graphqlDiagnosticsIntegration as graphqlIntegration, hapiIntegration, kafkajsIntegration as kafkaIntegration, + knexIntegration, koaIntegration, langChainIntegration, langGraphIntegration, @@ -31,8 +33,6 @@ export { redisIntegration } from './integrations/tracing/redis'; export { prismaIntegration } from '@sentry/server-utils'; export { setupHapiErrorHandler } from './integrations/tracing/hapi'; export { setupKoaErrorHandler } from './integrations/tracing/koa'; -export { knexIntegration } from './integrations/tracing/knex'; -export { dataloaderIntegration } from './integrations/tracing/dataloader'; export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler, diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts index e909b40bc944..f4b482b2f5c2 100644 --- a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts @@ -2,17 +2,14 @@ import type { ChannelListener } from 'node:diagnostics_channel'; import { subscribe, unsubscribe } from 'node:diagnostics_channel'; import { context, trace } from '@opentelemetry/api'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { InstrumentationBase } from '@opentelemetry/instrumentation'; import type { ClientRequest, IncomingMessage, ServerResponse } from 'node:http'; import type { HttpClientRequest, HttpIncomingMessage, HttpInstrumentationOptions, Span } from '@sentry/core'; import { getHttpClientSubscriptions, patchHttpModuleClient, - SDK_VERSION, getRequestOptions, isTracingSuppressed, } from '@sentry/core'; -import { INSTRUMENTATION_NAME } from './constants'; import { HTTP_ON_CLIENT_REQUEST } from '@sentry/core'; import { NODE_VERSION } from '../../nodeVersion'; import { errorMonitor } from 'node:events'; @@ -224,32 +221,3 @@ function instrumentHttpOutgoingRequestsViaMonkeyPatching(options: HttpInstrument // Patching http also patches https, as this uses the same underlying object patchHttpModuleClient(http, options); } - -/** - * This custom HTTP instrumentation handles outgoing HTTP requests. - * - * It provides: - * - Breadcrumbs for all outgoing requests - * - Trace propagation headers (when enabled) - * - Span creation for outgoing requests (when createSpansForOutgoingRequests is enabled) - * - * Span creation requires Node 22+ and uses diagnostic channels to avoid monkey-patching. - * By default, this is only enabled in the node SDK, not in other runtime SDKs that reuse this instrumentation. - * - * Important note: Contrary to other OTEL instrumentation, this one cannot be unwrapped. - * - * This is heavily inspired & adapted from: - * https://github.com/open-telemetry/opentelemetry-js/blob/f8ab5592ddea5cba0a3b33bf8d74f27872c0367f/experimental/packages/opentelemetry-instrumentation-http/src/http.ts - * - * @deprecated This will be removed in v11. Use instrumentHttpOutgoingRequests() instead. - */ -export class SentryHttpInstrumentation extends InstrumentationBase { - public constructor(config: SentryHttpInstrumentationOptions = {}) { - super(INSTRUMENTATION_NAME, SDK_VERSION, config); - } - - /** @inheritdoc */ - public init(): void { - instrumentHttpOutgoingRequests(this.getConfig()); - } -} diff --git a/packages/node/src/integrations/http/constants.ts b/packages/node/src/integrations/http/constants.ts deleted file mode 100644 index 35073ae0491d..000000000000 --- a/packages/node/src/integrations/http/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const INSTRUMENTATION_NAME = '@sentry/instrumentation-http'; diff --git a/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts b/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts deleted file mode 100644 index ffdd56bb2f8c..000000000000 --- a/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts +++ /dev/null @@ -1,188 +0,0 @@ -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { InstrumentationBase } from '@opentelemetry/instrumentation'; -import { LRUMap, SDK_VERSION, isTracingSuppressed } from '@sentry/core'; -import * as diagch from 'diagnostics_channel'; -import { - addFetchRequestBreadcrumb, - addTracePropagationHeadersToFetchRequest, - getAbsoluteUrl, -} from '../../utils/outgoingFetchRequest'; -import type { UndiciRequest, UndiciResponse } from './types'; - -export type SentryNodeFetchInstrumentationOptions = InstrumentationConfig & { - /** - * Whether breadcrumbs should be recorded for requests. - * - * @default `true` - */ - breadcrumbs?: boolean; - - /** - * Whether to inject trace propagation headers (sentry-trace, baggage, traceparent) into outgoing fetch requests. - * - * When set to `false`, Sentry will not inject any trace propagation headers, but will still create breadcrumbs - * (if `breadcrumbs` is enabled). This is useful when `skipOpenTelemetrySetup: true` is configured and you want - * to avoid duplicate trace headers being injected by both Sentry and OpenTelemetry's UndiciInstrumentation. - * - * @default `true` - */ - tracePropagation?: boolean; - - /** - * Do not capture breadcrumbs or inject headers for outgoing fetch requests to URLs where the given callback returns `true`. - * The same option can be passed to the top-level httpIntegration where it controls both, breadcrumb and - * span creation. - * - * @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the outgoing request. - */ - ignoreOutgoingRequests?: (url: string) => boolean; -}; - -interface ListenerRecord { - name: string; - unsubscribe: () => void; -} - -/** - * This custom node-fetch instrumentation is used to instrument outgoing fetch requests. - * It does not emit any spans. - * - * The reason this is isolated from the OpenTelemetry instrumentation is that users may overwrite this, - * which would lead to Sentry not working as expected. - * - * This is heavily inspired & adapted from: - * https://github.com/open-telemetry/opentelemetry-js-contrib/blob/28e209a9da36bc4e1f8c2b0db7360170ed46cb80/plugins/node/instrumentation-undici/src/undici.ts - * - * @deprecated This class is no longer used internally and will be removed in a future major version. Use `nativeNodeFetchIntegration` instead. - */ -export class SentryNodeFetchInstrumentation extends InstrumentationBase { - // Keep ref to avoid https://github.com/nodejs/node/issues/42170 bug and for - // unsubscribing. - private _channelSubs: Array; - private _propagationDecisionMap: LRUMap; - private _ignoreOutgoingRequestsMap: WeakMap; - - public constructor(config: SentryNodeFetchInstrumentationOptions = {}) { - super('@sentry/instrumentation-node-fetch', SDK_VERSION, config); - this._channelSubs = []; - this._propagationDecisionMap = new LRUMap(100); - this._ignoreOutgoingRequestsMap = new WeakMap(); - } - - /** No need to instrument files/modules. */ - public init(): void { - return undefined; - } - - /** Disable the instrumentation. */ - public disable(): void { - super.disable(); - this._channelSubs.forEach(sub => sub.unsubscribe()); - this._channelSubs = []; - } - - /** Enable the instrumentation. */ - public enable(): void { - // "enabled" handling is currently a bit messy with InstrumentationBase. - // If constructed with `{enabled: false}`, this `.enable()` is still called, - // and `this.getConfig().enabled !== this.isEnabled()`, creating confusion. - // - // For now, this class will setup for instrumenting if `.enable()` is - // called, but use `this.getConfig().enabled` to determine if - // instrumentation should be generated. This covers the more likely common - // case of config being given a construction time, rather than later via - // `instance.enable()`, `.disable()`, or `.setConfig()` calls. - super.enable(); - - // This method is called by the super-class constructor before ours is - // called. So we need to ensure the property is initalized. - this._channelSubs = this._channelSubs || []; - - // Avoid to duplicate subscriptions - if (this._channelSubs.length > 0) { - return; - } - - this._subscribeToChannel('undici:request:create', this._onRequestCreated.bind(this)); - this._subscribeToChannel('undici:request:headers', this._onResponseHeaders.bind(this)); - } - - /** - * This method is called when a request is created. - * You can still mutate the request here before it is sent. - */ - private _onRequestCreated({ request }: { request: UndiciRequest }): void { - const config = this.getConfig(); - const enabled = config.enabled !== false; - - if (!enabled) { - return; - } - - const shouldIgnore = this._shouldIgnoreOutgoingRequest(request); - // We store this decisision for later so we do not need to re-evaluate it - // Additionally, the active context is not correct in _onResponseHeaders, so we need to make sure it is evaluated here - this._ignoreOutgoingRequestsMap.set(request, shouldIgnore); - - if (shouldIgnore) { - return; - } - - if (config.tracePropagation !== false) { - addTracePropagationHeadersToFetchRequest(request, this._propagationDecisionMap); - } - } - - /** - * This method is called when a response is received. - */ - private _onResponseHeaders({ request, response }: { request: UndiciRequest; response: UndiciResponse }): void { - const config = this.getConfig(); - const enabled = config.enabled !== false; - - if (!enabled) { - return; - } - - const _breadcrumbs = config.breadcrumbs; - const breadCrumbsEnabled = typeof _breadcrumbs === 'undefined' ? true : _breadcrumbs; - - const shouldIgnore = this._ignoreOutgoingRequestsMap.get(request); - - if (breadCrumbsEnabled && !shouldIgnore) { - addFetchRequestBreadcrumb(request, response); - } - } - - /** Subscribe to a diagnostics channel. */ - private _subscribeToChannel( - diagnosticChannel: string, - onMessage: (message: unknown, name: string | symbol) => void, - ): void { - diagch.subscribe?.(diagnosticChannel, onMessage); - - this._channelSubs.push({ - name: diagnosticChannel, - unsubscribe: () => diagch.unsubscribe?.(diagnosticChannel, onMessage), - }); - } - - /** - * Check if the given outgoing request should be ignored. - */ - private _shouldIgnoreOutgoingRequest(request: UndiciRequest): boolean { - if (isTracingSuppressed()) { - return true; - } - - // Add trace propagation headers - const url = getAbsoluteUrl(request.origin, request.path); - const ignoreOutgoingRequests = this.getConfig().ignoreOutgoingRequests; - - if (typeof ignoreOutgoingRequests !== 'function' || !url) { - return false; - } - - return ignoreOutgoingRequests(url); - } -} diff --git a/packages/node/src/integrations/tracing/InstrumentationNodeModuleFile.ts b/packages/node/src/integrations/tracing/InstrumentationNodeModuleFile.ts deleted file mode 100644 index 429c937eaa31..000000000000 --- a/packages/node/src/integrations/tracing/InstrumentationNodeModuleFile.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * NOTICE from the Sentry authors: - * - Vendored locally from @opentelemetry/instrumentation to work around a Bun - * --bytecode bug (https://github.com/getsentry/sentry-javascript/issues/21256). - * - The upstream class imports `normalize` via an indirect platform/index - * re-export chain, which Bun's bytecode bundler renames and loses scope for. - * - This copy imports `normalize` directly from 'path' to break that chain. - */ -/* oxlint-disable */ - -import { normalize } from 'path'; -import type { InstrumentationModuleFile } from '@opentelemetry/instrumentation'; - -export class InstrumentationNodeModuleFile implements InstrumentationModuleFile { - public name: string; - public supportedVersions: string[]; - public patch: (moduleExports: any, moduleVersion?: string) => any; - public unpatch: (moduleExports?: any, moduleVersion?: string) => void; - - constructor( - name: string, - supportedVersions: string[], - patch: (moduleExports: any, moduleVersion?: string) => any, - unpatch: (moduleExports?: any, moduleVersion?: string) => void, - ) { - this.name = normalize(name); - this.supportedVersions = supportedVersions; - this.patch = patch; - this.unpatch = unpatch; - } -} diff --git a/packages/node/src/integrations/tracing/amqplib/index.ts b/packages/node/src/integrations/tracing/amqplib/index.ts deleted file mode 100644 index 5b6181291c71..000000000000 --- a/packages/node/src/integrations/tracing/amqplib/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { generateInstrumentOnce } from '../../../otel/instrument'; -import { AmqplibInstrumentation } from './vendored/instrumentation'; - -const INTEGRATION_NAME = 'Amqplib' as const; - -export const instrumentAmqplib = generateInstrumentOnce(INTEGRATION_NAME, () => new AmqplibInstrumentation()); diff --git a/packages/node/src/integrations/tracing/amqplib/vendored/amqplib-types.ts b/packages/node/src/integrations/tracing/amqplib/vendored/amqplib-types.ts deleted file mode 100644 index 71a03a7e998d..000000000000 --- a/packages/node/src/integrations/tracing/amqplib/vendored/amqplib-types.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Simplified types inlined from @types/amqplib (DefinitelyTyped). - * Only includes members accessed by this instrumentation. - * Other amqplib types (Message, ConsumeMessage, Options.Publish, etc.) are already - * vendored in types.ts by the upstream OTel instrumentation. - */ - -export interface Connection { - connection: { serverProperties: { product?: string; [key: string]: any } }; - [key: string]: any; -} - -export interface Channel { - connection: Connection; - [key: string]: any; -} - -export interface ConfirmChannel extends Channel {} - -export namespace Options { - export interface Connect { - protocol?: string; - hostname?: string; - port?: number; - username?: string; - vhost?: string; - } - export interface Consume { - consumerTag?: string; - noLocal?: boolean; - noAck?: boolean; - exclusive?: boolean; - priority?: number; - arguments?: any; - } - export interface Publish { - headers?: any; - [key: string]: any; - } -} - -export namespace Replies { - export interface Empty {} - export interface Consume { - consumerTag: string; - } -} diff --git a/packages/node/src/integrations/tracing/amqplib/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/amqplib/vendored/instrumentation.ts deleted file mode 100644 index e4a7d630e2f5..000000000000 --- a/packages/node/src/integrations/tracing/amqplib/vendored/instrumentation.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-amqplib - * - Upstream version: @opentelemetry/instrumentation-amqplib@0.65.0 - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs; the channel/connection - * patches live in `patches.ts` and span creation in `utils.ts` - * - Dropped the instrumentation config and all hooks; origin is folded into span creation instead of - * `index.ts` hooks (see `patches.ts`/`utils.ts` for details) - */ - -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import { SDK_VERSION } from '@sentry/core'; -import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile'; -import { - getAckAllPatch, - getAckPatch, - getChannelEmitPatch, - getConfirmedPublishPatch, - getConnectPatch, - getConsumePatch, - getPublishPatch, -} from './patches'; -import { EndOperation } from './types'; - -const PACKAGE_NAME = '@sentry/instrumentation-amqplib'; -const supportedVersions = ['>=0.5.5 <2']; - -export class AmqplibInstrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - protected init(): InstrumentationNodeModuleDefinition { - const channelModelModuleFile = new InstrumentationNodeModuleFile( - 'amqplib/lib/channel_model.js', - supportedVersions, - this.patchChannelModel.bind(this), - this.unpatchChannelModel.bind(this), - ); - - const callbackModelModuleFile = new InstrumentationNodeModuleFile( - 'amqplib/lib/callback_model.js', - supportedVersions, - this.patchChannelModel.bind(this), - this.unpatchChannelModel.bind(this), - ); - - const connectModuleFile = new InstrumentationNodeModuleFile( - 'amqplib/lib/connect.js', - supportedVersions, - this.patchConnect.bind(this), - this.unpatchConnect.bind(this), - ); - - const module = new InstrumentationNodeModuleDefinition('amqplib', supportedVersions, undefined, undefined, [ - channelModelModuleFile, - connectModuleFile, - callbackModelModuleFile, - ]); - return module; - } - - private patchConnect(moduleExports: any): any { - const unpatchedExports = this.unpatchConnect(moduleExports); - if (!isWrapped(unpatchedExports.connect)) { - this._wrap(unpatchedExports, 'connect', getConnectPatch); - } - return unpatchedExports; - } - - private unpatchConnect(moduleExports: any): any { - if (isWrapped(moduleExports.connect)) { - this._unwrap(moduleExports, 'connect'); - } - return moduleExports; - } - - private patchChannelModel(moduleExports: any): any { - if (!isWrapped(moduleExports.Channel.prototype.publish)) { - this._wrap(moduleExports.Channel.prototype, 'publish', getPublishPatch); - } - if (!isWrapped(moduleExports.Channel.prototype.consume)) { - this._wrap(moduleExports.Channel.prototype, 'consume', getConsumePatch); - } - if (!isWrapped(moduleExports.Channel.prototype.ack)) { - this._wrap(moduleExports.Channel.prototype, 'ack', getAckPatch(false, EndOperation.Ack)); - } - if (!isWrapped(moduleExports.Channel.prototype.nack)) { - this._wrap(moduleExports.Channel.prototype, 'nack', getAckPatch(true, EndOperation.Nack)); - } - if (!isWrapped(moduleExports.Channel.prototype.reject)) { - this._wrap(moduleExports.Channel.prototype, 'reject', getAckPatch(true, EndOperation.Reject)); - } - if (!isWrapped(moduleExports.Channel.prototype.ackAll)) { - this._wrap(moduleExports.Channel.prototype, 'ackAll', getAckAllPatch(false, EndOperation.AckAll)); - } - if (!isWrapped(moduleExports.Channel.prototype.nackAll)) { - this._wrap(moduleExports.Channel.prototype, 'nackAll', getAckAllPatch(true, EndOperation.NackAll)); - } - if (!isWrapped(moduleExports.Channel.prototype.emit)) { - this._wrap(moduleExports.Channel.prototype, 'emit', getChannelEmitPatch); - } - if (!isWrapped(moduleExports.ConfirmChannel.prototype.publish)) { - this._wrap(moduleExports.ConfirmChannel.prototype, 'publish', getConfirmedPublishPatch); - } - return moduleExports; - } - - private unpatchChannelModel(moduleExports: any): any { - if (isWrapped(moduleExports.Channel.prototype.publish)) { - this._unwrap(moduleExports.Channel.prototype, 'publish'); - } - if (isWrapped(moduleExports.Channel.prototype.consume)) { - this._unwrap(moduleExports.Channel.prototype, 'consume'); - } - if (isWrapped(moduleExports.Channel.prototype.ack)) { - this._unwrap(moduleExports.Channel.prototype, 'ack'); - } - if (isWrapped(moduleExports.Channel.prototype.nack)) { - this._unwrap(moduleExports.Channel.prototype, 'nack'); - } - if (isWrapped(moduleExports.Channel.prototype.reject)) { - this._unwrap(moduleExports.Channel.prototype, 'reject'); - } - if (isWrapped(moduleExports.Channel.prototype.ackAll)) { - this._unwrap(moduleExports.Channel.prototype, 'ackAll'); - } - if (isWrapped(moduleExports.Channel.prototype.nackAll)) { - this._unwrap(moduleExports.Channel.prototype, 'nackAll'); - } - if (isWrapped(moduleExports.Channel.prototype.emit)) { - this._unwrap(moduleExports.Channel.prototype, 'emit'); - } - if (isWrapped(moduleExports.ConfirmChannel.prototype.publish)) { - this._unwrap(moduleExports.ConfirmChannel.prototype, 'publish'); - } - return moduleExports; - } -} diff --git a/packages/node/src/integrations/tracing/amqplib/vendored/patches.ts b/packages/node/src/integrations/tracing/amqplib/vendored/patches.ts deleted file mode 100644 index cf1233f47bf7..000000000000 --- a/packages/node/src/integrations/tracing/amqplib/vendored/patches.ts +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-amqplib - * - Upstream version: @opentelemetry/instrumentation-amqplib@0.65.0 - * - The channel/connection patches were extracted from the instrumentation class into standalone factories - * and migrated to Sentry's span APIs; origin is folded into span creation instead of `index.ts` hooks - * - Cross-service trace propagation uses Sentry's `getTraceData`/`continueTrace` instead of the OTel propagator - * - Replaced the OTel context-key confirm-channel marker with a synchronous flag on the channel instance - * - Dropped the instrumentation config and all hooks (publish/publishConfirm/consume/consumeEnd) and the - * `useLinksForConsume` path; the SDK never used them - */ - -import { continueTrace, SPAN_STATUS_ERROR, timestampInSeconds, withActiveSpan } from '@sentry/core'; -import type { Connection, Options, Replies } from './amqplib-types'; -import { EndOperation, type ConsumeMessage, type Message } from './types'; -import type { - InstrumentationConnection, - InstrumentationConsumeChannel, - InstrumentationConsumeMessage, - InstrumentationMessage, - InstrumentationPublishChannel, -} from './utils'; -import { - CHANNEL_CONSUME_TIMEOUT_TIMER, - CHANNEL_IS_CONFIRM_PUBLISHING, - CHANNEL_SPANS_NOT_ENDED, - CONNECTION_ATTRIBUTES, - getConnectionAttributesFromServer, - getConnectionAttributesFromUrl, - getHeaderAsString, - MESSAGE_STORED_SPAN, - startConsumeSpan, - startPublishSpan, -} from './utils'; - -// To prevent reference leaks from un-acked messages, their spans are closed after this timeout. The -// upstream instrumentation exposed this as the `consumeTimeoutMs` option; the SDK always used the default. -const CONSUME_TIMEOUT_MS = 1000 * 60; // 1 minute - -function endConsumerSpan( - message: InstrumentationMessage, - isRejected: boolean | null, - operation: EndOperation, - requeue: boolean | undefined, -): void { - const storedSpan = message[MESSAGE_STORED_SPAN]; - if (!storedSpan) { - return; - } - if (isRejected !== false) { - storedSpan.setStatus({ - code: SPAN_STATUS_ERROR, - message: - operation !== EndOperation.ChannelClosed && operation !== EndOperation.ChannelError - ? `${operation} called on message${ - requeue === true ? ' with requeue' : requeue === false ? ' without requeue' : '' - }` - : operation, - }); - } - storedSpan.end(); - message[MESSAGE_STORED_SPAN] = undefined; -} - -function endAllSpansOnChannel( - channel: InstrumentationConsumeChannel, - isRejected: boolean, - operation: EndOperation, - requeue: boolean | undefined, -): void { - const spansNotEnded: { msg: Message }[] = channel[CHANNEL_SPANS_NOT_ENDED] ?? []; - spansNotEnded.forEach(msgDetails => { - endConsumerSpan(msgDetails.msg, isRejected, operation, requeue); - }); - channel[CHANNEL_SPANS_NOT_ENDED] = []; -} - -function checkConsumeTimeoutOnChannel(channel: InstrumentationConsumeChannel): void { - const currentTime = timestampInSeconds(); - const spansNotEnded = channel[CHANNEL_SPANS_NOT_ENDED] ?? []; - let i: number; - for (i = 0; i < spansNotEnded.length; i++) { - const currMessage = spansNotEnded[i]!; - const timeFromConsumeMs = (currentTime - currMessage.timeOfConsume) * 1000; - if (timeFromConsumeMs < CONSUME_TIMEOUT_MS) { - break; - } - endConsumerSpan(currMessage.msg, null, EndOperation.InstrumentationTimeout, true); - } - spansNotEnded.splice(0, i); -} - -export function getConnectPatch( - original: ( - url: string | Options.Connect, - socketOptions: any, - openCallback: (err: any, connection: Connection) => void, - ) => Connection, -) { - return function patchedConnect( - this: unknown, - url: string | Options.Connect, - socketOptions: any, - openCallback: Function, - ): Connection { - return original.call(this, url, socketOptions, function (this: unknown, err: any, conn: InstrumentationConnection) { - if (err == null) { - const urlAttributes = getConnectionAttributesFromUrl(url); - const serverAttributes = getConnectionAttributesFromServer(conn); - conn[CONNECTION_ATTRIBUTES] = { - ...urlAttributes, - ...serverAttributes, - }; - } - openCallback.apply(this, arguments); - }); - }; -} - -export function getChannelEmitPatch(original: Function) { - return function emit(this: InstrumentationConsumeChannel, eventName: string): void { - if (eventName === 'close') { - endAllSpansOnChannel(this, true, EndOperation.ChannelClosed, undefined); - const activeTimer = this[CHANNEL_CONSUME_TIMEOUT_TIMER]; - if (activeTimer) { - clearInterval(activeTimer); - } - this[CHANNEL_CONSUME_TIMEOUT_TIMER] = undefined; - } else if (eventName === 'error') { - endAllSpansOnChannel(this, true, EndOperation.ChannelError, undefined); - } - return original.apply(this, arguments); - }; -} - -export function getAckAllPatch(isRejected: boolean, endOperation: EndOperation) { - return (original: Function) => - function ackAll(this: InstrumentationConsumeChannel, requeueOrEmpty?: boolean): void { - endAllSpansOnChannel(this, isRejected, endOperation, requeueOrEmpty); - return original.apply(this, arguments); - }; -} - -export function getAckPatch(isRejected: boolean, endOperation: EndOperation) { - return (original: Function) => - function ack( - this: InstrumentationConsumeChannel, - message: Message, - allUpToOrRequeue?: boolean, - requeue?: boolean, - ): void { - const channel = this; - // we use this patch in the reject function as well, but it has a different signature - const requeueResolved = endOperation === EndOperation.Reject ? allUpToOrRequeue : requeue; - - const spansNotEnded: { msg: Message }[] = channel[CHANNEL_SPANS_NOT_ENDED] ?? []; - const msgIndex = spansNotEnded.findIndex(msgDetails => msgDetails.msg === message); - if (msgIndex < 0) { - // should not happen in the happy flow, but possible if the user calls ack twice with the same message - endConsumerSpan(message, isRejected, endOperation, requeueResolved); - } else if (endOperation !== EndOperation.Reject && allUpToOrRequeue) { - for (let i = 0; i <= msgIndex; i++) { - endConsumerSpan(spansNotEnded[i]!.msg, isRejected, endOperation, requeueResolved); - } - spansNotEnded.splice(0, msgIndex + 1); - } else { - endConsumerSpan(message, isRejected, endOperation, requeueResolved); - spansNotEnded.splice(msgIndex, 1); - } - return original.apply(this, arguments); - }; -} - -export function getConsumePatch(original: Function) { - return function consume( - this: InstrumentationConsumeChannel, - queue: string, - onMessage: (msg: ConsumeMessage | null) => void, - options?: Options.Consume, - ): Promise { - const channel = this; - if (!Object.prototype.hasOwnProperty.call(channel, CHANNEL_SPANS_NOT_ENDED)) { - const timer = setInterval(() => { - checkConsumeTimeoutOnChannel(channel); - }, CONSUME_TIMEOUT_MS); - timer.unref(); - channel[CHANNEL_CONSUME_TIMEOUT_TIMER] = timer; - channel[CHANNEL_SPANS_NOT_ENDED] = []; - } - - const patchedOnMessage = function (this: unknown, msg: InstrumentationConsumeMessage | null): void { - // msg is expected to be null for a consumer cancel notification - // https://www.rabbitmq.com/consumer-cancel.html - // in this case, we do not start a span, as this is not a real message. - if (!msg) { - return onMessage.call(this, msg); - } - - const headers = msg.properties.headers ?? {}; - const sentryTrace = getHeaderAsString(headers, 'sentry-trace'); - const baggage = getHeaderAsString(headers, 'baggage'); - - // Continue the producer's trace so the consumer span is parented to the message's producer. - continueTrace({ sentryTrace, baggage }, () => { - const span = startConsumeSpan(queue, msg, channel); - - if (!options?.noAck) { - // store the message on the channel so we can close the span on ackAll etc - channel[CHANNEL_SPANS_NOT_ENDED]!.push({ msg, timeOfConsume: timestampInSeconds() }); - // store the span on the message so we can end it when the user calls 'ack' on it - msg[MESSAGE_STORED_SPAN] = span; - } - withActiveSpan(span, () => { - onMessage.call(this, msg); - }); - - if (options?.noAck) { - span.end(); - } - }); - }; - - // Copy `arguments` instead of mutating it: in CJS builds it's aliased to the named parameters, - // so `arguments[1] = ...` would also reassign `onMessage` to `patchedOnMessage`, making the wrapper - // call itself and recurse infinitely. - const callArgs = Array.prototype.slice.call(arguments); - callArgs[1] = patchedOnMessage; - return original.apply(this, callArgs); - }; -} - -export function getConfirmedPublishPatch(original: Function) { - return function confirmedPublish( - this: InstrumentationPublishChannel, - exchange: string, - routingKey: string, - content: Buffer, - options?: Options.Publish, - callback?: (err: any, ok: Replies.Empty) => void, - ): boolean { - const channel = this; - const { span, modifiedOptions } = startPublishSpan(exchange, routingKey, channel, options); - - const patchedOnConfirm = function (this: unknown, err: any, ok: Replies.Empty): void { - try { - withActiveSpan(span, () => { - callback?.call(this, err, ok); - }); - } finally { - if (err) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: "message confirmation has been nack'ed" }); - } - span.end(); - } - }; - - // The confirm channel publish stores the message and registers a broker-confirm callback; the span - // ends in that callback. The confirm publish internally delegates to the base channel publish, so we - // flag the channel to stop the base publish patch from creating a second span. The inner call is - // synchronous, so a flag on the instance is enough and avoids the OTel context machinery. - const argumentsCopy = [...arguments]; - argumentsCopy[3] = modifiedOptions; - argumentsCopy[4] = patchedOnConfirm; - channel[CHANNEL_IS_CONFIRM_PUBLISHING] = true; - try { - return original.apply(this, argumentsCopy); - } finally { - channel[CHANNEL_IS_CONFIRM_PUBLISHING] = false; - } - }; -} - -export function getPublishPatch(original: Function) { - return function publish( - this: InstrumentationPublishChannel, - exchange: string, - routingKey: string, - content: Buffer, - options?: Options.Publish, - ): boolean { - if (this[CHANNEL_IS_CONFIRM_PUBLISHING]) { - // already instrumented by the confirm-channel publish patch - return original.apply(this, arguments); - } - const channel = this; - const { span, modifiedOptions } = startPublishSpan(exchange, routingKey, channel, options); - - // calling the normal channel publish function only stores the message in the queue; it does not send - // it and wait for an ack, so the span duration is expected to be very short. - const argumentsCopy = [...arguments]; - argumentsCopy[3] = modifiedOptions; - const originalRes = original.apply(this, argumentsCopy); - span.end(); - return originalRes; - }; -} diff --git a/packages/node/src/integrations/tracing/amqplib/vendored/semconv.ts b/packages/node/src/integrations/tracing/amqplib/vendored/semconv.ts deleted file mode 100644 index ffabc460056c..000000000000 --- a/packages/node/src/integrations/tracing/amqplib/vendored/semconv.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-amqplib - * - Upstream version: @opentelemetry/instrumentation-amqplib@0.65.0 - * - Merged the upstream `semconv.ts` and `semconv-obsolete.ts` into a single file containing only the - * constants this instrumentation emits. These mirror the (now legacy) messaging semantic conventions - * that the SDK has always emitted; the `@deprecated` annotations were dropped since these vendored - * copies are intentionally the chosen output and we don't want to flag every usage site. - */ - -/** A string identifying the kind of message consumption. */ -export const ATTR_MESSAGING_OPERATION = 'messaging.operation' as const; - -/** The message destination name (the exchange for amqplib). */ -export const ATTR_MESSAGING_DESTINATION = 'messaging.destination' as const; - -/** The kind of message destination. */ -export const ATTR_MESSAGING_DESTINATION_KIND = 'messaging.destination_kind' as const; - -/** RabbitMQ message routing key. */ -export const ATTR_MESSAGING_RABBITMQ_ROUTING_KEY = 'messaging.rabbitmq.routing_key' as const; - -/** The name of the transport protocol. */ -export const ATTR_MESSAGING_PROTOCOL = 'messaging.protocol' as const; - -/** The version of the transport protocol. */ -export const ATTR_MESSAGING_PROTOCOL_VERSION = 'messaging.protocol_version' as const; - -/** Connection string. */ -export const ATTR_MESSAGING_URL = 'messaging.url' as const; - -/** A value used by the messaging system as an identifier for the message, represented as a string. */ -export const OLD_ATTR_MESSAGING_MESSAGE_ID = 'messaging.message_id' as const; - -/** The conversation ID (a.k.a. correlation ID) identifying the conversation the message belongs to. */ -export const ATTR_MESSAGING_CONVERSATION_ID = 'messaging.conversation_id' as const; - -/** Value for `messaging.destination_kind` when the destination is a topic. */ -export const MESSAGING_DESTINATION_KIND_VALUE_TOPIC = 'topic' as const; - -/** Value for `messaging.operation` when the message is being processed by a consumer. */ -export const MESSAGING_OPERATION_VALUE_PROCESS = 'process' as const; diff --git a/packages/node/src/integrations/tracing/amqplib/vendored/types.ts b/packages/node/src/integrations/tracing/amqplib/vendored/types.ts deleted file mode 100644 index 1b444d6723c1..000000000000 --- a/packages/node/src/integrations/tracing/amqplib/vendored/types.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-amqplib - * - Upstream version: @opentelemetry/instrumentation-amqplib@0.65.0 - * - Some types vendored from @types/amqplib with simplifications - * - Dropped the instrumentation config and all hooks; the SDK folds origin into span creation instead - */ - -export enum EndOperation { - AutoAck = 'auto ack', - Ack = 'ack', - AckAll = 'ackAll', - Reject = 'reject', - Nack = 'nack', - NackAll = 'nackAll', - ChannelClosed = 'channel closed', - ChannelError = 'channel error', - InstrumentationTimeout = 'instrumentation timeout', -} - -// The following types are vendored from `@types/amqplib@0.10.1` - commit SHA: 4205e03127692a40b4871709a7134fe4e2ed5510 - -// Vendored from: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/4205e03127692a40b4871709a7134fe4e2ed5510/types/amqplib/properties.d.ts#L142 -export interface Message { - content: Buffer; - fields: MessageFields; - properties: MessageProperties; -} - -export interface ConsumeMessage extends Message { - fields: ConsumeMessageFields; -} - -export interface CommonMessageFields { - deliveryTag: number; - redelivered: boolean; - exchange: string; - routingKey: string; -} - -export interface MessageFields extends CommonMessageFields { - messageCount?: number; - consumerTag?: string; -} - -export interface ConsumeMessageFields extends CommonMessageFields { - deliveryTag: number; -} - -export interface MessageProperties { - contentType: any | undefined; - contentEncoding: any | undefined; - headers: any; - deliveryMode: any | undefined; - priority: any | undefined; - correlationId: any | undefined; - replyTo: any | undefined; - expiration: any | undefined; - messageId: any | undefined; - timestamp: any | undefined; - type: any | undefined; - userId: any | undefined; - appId: any | undefined; - clusterId: any | undefined; -} diff --git a/packages/node/src/integrations/tracing/amqplib/vendored/utils.ts b/packages/node/src/integrations/tracing/amqplib/vendored/utils.ts deleted file mode 100644 index eec57b724a8c..000000000000 --- a/packages/node/src/integrations/tracing/amqplib/vendored/utils.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-amqplib - * - Upstream version: @opentelemetry/instrumentation-amqplib@0.65.0 - * - Some types vendored from @types/amqplib with simplifications - * - Span creation extracted here and migrated to the @sentry/core API; origin folded into span creation - * - Cross-service trace propagation uses Sentry's `getTraceData` instead of the OTel propagator - * - Dropped the env-gated stable-semconv dual emission; only the (default) old semantic conventions are emitted - * - Replaced the OTel context-key confirm-channel marker with a synchronous flag on the channel instance - */ - -import type { Span, SpanAttributes } from '@sentry/core'; -import { getTraceData, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; -import { MESSAGING_SYSTEM, NET_PEER_NAME, NET_PEER_PORT, SENTRY_KIND } from '@sentry/conventions/attributes'; -import type { Channel, ConfirmChannel, Connection, Options } from './amqplib-types'; -import { - ATTR_MESSAGING_CONVERSATION_ID, - ATTR_MESSAGING_DESTINATION, - ATTR_MESSAGING_DESTINATION_KIND, - ATTR_MESSAGING_OPERATION, - ATTR_MESSAGING_PROTOCOL, - ATTR_MESSAGING_PROTOCOL_VERSION, - ATTR_MESSAGING_RABBITMQ_ROUTING_KEY, - ATTR_MESSAGING_URL, - MESSAGING_DESTINATION_KIND_VALUE_TOPIC, - MESSAGING_OPERATION_VALUE_PROCESS, - OLD_ATTR_MESSAGING_MESSAGE_ID, -} from './semconv'; -import type { ConsumeMessage, Message } from './types'; - -const PUBLISHER_ORIGIN = 'auto.amqplib.otel.publisher'; -const CONSUMER_ORIGIN = 'auto.amqplib.otel.consumer'; - -export const MESSAGE_STORED_SPAN: unique symbol = Symbol('opentelemetry.amqplib.message.stored-span'); -export const CHANNEL_SPANS_NOT_ENDED: unique symbol = Symbol('opentelemetry.amqplib.channel.spans-not-ended'); -export const CHANNEL_CONSUME_TIMEOUT_TIMER: unique symbol = Symbol( - 'opentelemetry.amqplib.channel.consumer-timeout-timer', -); -export const CONNECTION_ATTRIBUTES: unique symbol = Symbol('opentelemetry.amqplib.connection.attributes'); -export const CHANNEL_IS_CONFIRM_PUBLISHING: unique symbol = Symbol('sentry.amqplib.channel.is-confirm-publishing'); - -export type InstrumentationConnection = Connection & { - [CONNECTION_ATTRIBUTES]?: SpanAttributes; -}; -export type InstrumentationPublishChannel = (Channel | ConfirmChannel) & { - connection: InstrumentationConnection; - [CHANNEL_IS_CONFIRM_PUBLISHING]?: boolean; -}; -export type InstrumentationConsumeChannel = Channel & { - connection: InstrumentationConnection; - [CHANNEL_SPANS_NOT_ENDED]?: { - msg: ConsumeMessage; - timeOfConsume: number; - }[]; - [CHANNEL_CONSUME_TIMEOUT_TIMER]?: NodeJS.Timeout; -}; -export type InstrumentationMessage = Message & { - [MESSAGE_STORED_SPAN]?: Span; -}; -export type InstrumentationConsumeMessage = ConsumeMessage & { - [MESSAGE_STORED_SPAN]?: Span; -}; - -export const normalizeExchange = (exchangeName: string): string => (exchangeName !== '' ? exchangeName : ''); - -const censorPassword = (url: string): string => { - return url.replace(/:[^:@/]*@/, ':***@'); -}; - -const getPort = (portFromUrl: number | undefined, resolvedProtocol: string): number => { - // we are using the resolved protocol which is upper case - // this code mimics the behavior of amqplib which is used to set connection params - return portFromUrl || (resolvedProtocol === 'AMQP' ? 5672 : 5671); -}; - -const getProtocol = (protocolFromUrl: string | undefined): string => { - const resolvedProtocol = protocolFromUrl || 'amqp'; - // the substring removes the ':' part of the protocol ('amqp:' -> 'amqp') - const noEndingColon = resolvedProtocol.endsWith(':') - ? resolvedProtocol.substring(0, resolvedProtocol.length - 1) - : resolvedProtocol; - // upper case to match spec - return noEndingColon.toUpperCase(); -}; - -const getHostname = (hostnameFromUrl: string | undefined): string => { - // if user supplies empty hostname, it gets forwarded to 'net' package which defaults it to localhost. - // https://nodejs.org/docs/latest-v12.x/api/net.html#net_socket_connect_options_connectlistener - return hostnameFromUrl || 'localhost'; -}; - -export const getConnectionAttributesFromServer = (conn: Connection): SpanAttributes => { - const product = conn.serverProperties.product?.toLowerCase?.(); - if (product) { - return { - [MESSAGING_SYSTEM]: product, - }; - } else { - return {}; - } -}; - -export const getConnectionAttributesFromUrl = (url: string | Options.Connect): SpanAttributes => { - const attributes: SpanAttributes = { - [ATTR_MESSAGING_PROTOCOL_VERSION]: '0.9.1', // this is the only protocol supported by the instrumented library - }; - - const resolvedUrl = url || 'amqp://localhost'; - if (typeof resolvedUrl === 'object') { - const connectOptions = resolvedUrl; - - const protocol = getProtocol(connectOptions?.protocol); - attributes[ATTR_MESSAGING_PROTOCOL] = protocol; - // oxlint-disable-next-line typescript/no-deprecated - attributes[NET_PEER_NAME] = getHostname(connectOptions?.hostname); - // oxlint-disable-next-line typescript/no-deprecated - attributes[NET_PEER_PORT] = getPort(connectOptions.port, protocol); - } else { - const censoredUrl = censorPassword(resolvedUrl); - attributes[ATTR_MESSAGING_URL] = censoredUrl; - try { - const urlParts = new URL(censoredUrl); - - const protocol = getProtocol(urlParts.protocol); - attributes[ATTR_MESSAGING_PROTOCOL] = protocol; - // oxlint-disable-next-line typescript/no-deprecated - attributes[NET_PEER_NAME] = getHostname(urlParts.hostname); - // oxlint-disable-next-line typescript/no-deprecated - attributes[NET_PEER_PORT] = getPort(urlParts.port ? parseInt(urlParts.port) : undefined, protocol); - } catch { - // best-effort: a malformed url simply yields fewer connection attributes - } - } - return attributes; -}; - -/** Reads a propagation header value off an amqplib message as a string. */ -export function getHeaderAsString(headers: Record | undefined, key: string): string | undefined { - const value = headers?.[key]; - if (value == null) { - return undefined; - } - return Array.isArray(value) ? String(value[0]) : String(value); -} - -/** Starts an inactive producer span and propagates its trace into the publish `options.headers`. */ -export function startPublishSpan( - exchange: string, - routingKey: string, - channel: InstrumentationPublishChannel, - options?: Options.Publish, -): { span: Span; modifiedOptions: Options.Publish } { - const normalizedExchange = normalizeExchange(exchange); - - const span = startInactiveSpan({ - name: `publish ${normalizedExchange}`, - attributes: { - [SENTRY_KIND]: 'producer', - ...channel.connection[CONNECTION_ATTRIBUTES], - [ATTR_MESSAGING_DESTINATION]: exchange, - [ATTR_MESSAGING_DESTINATION_KIND]: MESSAGING_DESTINATION_KIND_VALUE_TOPIC, - [ATTR_MESSAGING_RABBITMQ_ROUTING_KEY]: routingKey, - [OLD_ATTR_MESSAGING_MESSAGE_ID]: options?.messageId, - [ATTR_MESSAGING_CONVERSATION_ID]: options?.correlationId, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: PUBLISHER_ORIGIN, - }, - }); - - const modifiedOptions = options ?? {}; - modifiedOptions.headers = modifiedOptions.headers ?? {}; - - const traceData = getTraceData({ span }); - if (traceData['sentry-trace']) { - modifiedOptions.headers['sentry-trace'] = traceData['sentry-trace']; - } - if (traceData.baggage) { - modifiedOptions.headers['baggage'] = traceData.baggage; - } - - return { span, modifiedOptions }; -} - -/** Starts an inactive consumer (process) span carrying the amqplib messaging attributes. */ -export function startConsumeSpan( - queue: string, - msg: InstrumentationConsumeMessage, - channel: InstrumentationConsumeChannel, -): Span { - return startInactiveSpan({ - name: `${queue} process`, - attributes: { - [SENTRY_KIND]: 'consumer', - ...channel?.connection?.[CONNECTION_ATTRIBUTES], - [ATTR_MESSAGING_DESTINATION]: msg.fields?.exchange, - [ATTR_MESSAGING_DESTINATION_KIND]: MESSAGING_DESTINATION_KIND_VALUE_TOPIC, - [ATTR_MESSAGING_RABBITMQ_ROUTING_KEY]: msg.fields?.routingKey, - [ATTR_MESSAGING_OPERATION]: MESSAGING_OPERATION_VALUE_PROCESS, - [OLD_ATTR_MESSAGING_MESSAGE_ID]: msg?.properties.messageId, - [ATTR_MESSAGING_CONVERSATION_ID]: msg?.properties.correlationId, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: CONSUMER_ORIGIN, - }, - }); -} diff --git a/packages/node/src/integrations/tracing/anthropic-ai/index.ts b/packages/node/src/integrations/tracing/anthropic-ai/index.ts deleted file mode 100644 index ed86f6e83d9c..000000000000 --- a/packages/node/src/integrations/tracing/anthropic-ai/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { AnthropicAiOptions } from '@sentry/core'; -import { ANTHROPIC_AI_INTEGRATION_NAME } from '@sentry/core'; -import { generateInstrumentOnce } from '../../../otel/instrument'; -import { SentryAnthropicAiInstrumentation } from './instrumentation'; - -export const instrumentAnthropicAi = generateInstrumentOnce( - ANTHROPIC_AI_INTEGRATION_NAME, - options => new SentryAnthropicAiInstrumentation(options), -); diff --git a/packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts b/packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts deleted file mode 100644 index b01b34f2f8ad..000000000000 --- a/packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { - InstrumentationBase, - type InstrumentationConfig, - type InstrumentationModuleDefinition, - InstrumentationNodeModuleDefinition, -} from '@opentelemetry/instrumentation'; -import type { AnthropicAiClient, AnthropicAiOptions } from '@sentry/core'; -import { - _INTERNAL_shouldSkipAiProviderWrapping, - ANTHROPIC_AI_INTEGRATION_NAME, - instrumentAnthropicAiClient, - SDK_VERSION, -} from '@sentry/core'; - -const supportedVersions = ['>=0.19.2 <1.0.0']; - -type AnthropicAiInstrumentationOptions = InstrumentationConfig & AnthropicAiOptions; - -/** - * Represents the patched shape of the Anthropic AI module export. - */ -interface PatchedModuleExports { - [key: string]: unknown; - Anthropic: abstract new (...args: unknown[]) => AnthropicAiClient; -} - -/** - * Sentry Anthropic AI instrumentation using OpenTelemetry. - */ -export class SentryAnthropicAiInstrumentation extends InstrumentationBase { - public constructor(config: AnthropicAiInstrumentationOptions = {}) { - super('@sentry/instrumentation-anthropic-ai', SDK_VERSION, config); - } - - /** - * Initializes the instrumentation by defining the modules to be patched. - */ - public init(): InstrumentationModuleDefinition { - const module = new InstrumentationNodeModuleDefinition( - '@anthropic-ai/sdk', - supportedVersions, - this._patch.bind(this), - ); - return module; - } - - /** - * Core patch logic applying instrumentation to the Anthropic AI client constructor. - */ - private _patch(exports: PatchedModuleExports): PatchedModuleExports | void { - const Original = exports.Anthropic; - - const config = this.getConfig(); - - const WrappedAnthropic = function (this: unknown, ...args: unknown[]) { - // Check if wrapping should be skipped (e.g., when LangChain is handling instrumentation) - if (_INTERNAL_shouldSkipAiProviderWrapping(ANTHROPIC_AI_INTEGRATION_NAME)) { - return Reflect.construct(Original, args) as AnthropicAiClient; - } - - const instance = Reflect.construct(Original, args); - - return instrumentAnthropicAiClient(instance as AnthropicAiClient, config); - } as unknown as abstract new (...args: unknown[]) => AnthropicAiClient; - - // Preserve static and prototype chains - Object.setPrototypeOf(WrappedAnthropic, Original); - Object.setPrototypeOf(WrappedAnthropic.prototype, Original.prototype); - - for (const key of Object.getOwnPropertyNames(Original)) { - if (!['length', 'name', 'prototype'].includes(key)) { - const descriptor = Object.getOwnPropertyDescriptor(Original, key); - if (descriptor) { - Object.defineProperty(WrappedAnthropic, key, descriptor); - } - } - } - - // Constructor replacement - handle read-only properties - // The Anthropic property might have only a getter, so use defineProperty - try { - exports.Anthropic = WrappedAnthropic; - } catch { - // If direct assignment fails, override the property descriptor - Object.defineProperty(exports, 'Anthropic', { - value: WrappedAnthropic, - writable: true, - configurable: true, - enumerable: true, - }); - } - - // Wrap the default export if it points to the original constructor - // Constructor replacement - handle read-only properties - // The Anthropic property might have only a getter, so use defineProperty - if (exports.default === Original) { - try { - exports.default = WrappedAnthropic; - } catch { - // If direct assignment fails, override the property descriptor - Object.defineProperty(exports, 'default', { - value: WrappedAnthropic, - writable: true, - configurable: true, - enumerable: true, - }); - } - } - return exports; - } -} diff --git a/packages/node/src/integrations/tracing/dataloader/index.ts b/packages/node/src/integrations/tracing/dataloader/index.ts deleted file mode 100644 index 0f20888f3207..000000000000 --- a/packages/node/src/integrations/tracing/dataloader/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { IntegrationFn } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce } from '../../../otel/instrument'; -import { - dataloaderIntegration as dataloaderChannelIntegration, - isOrchestrionInjected, -} from '@sentry/server-utils/orchestrion'; -import { DataloaderInstrumentation } from './vendored/instrumentation'; - -const INTEGRATION_NAME = 'Dataloader' as const; - -export const instrumentDataloader = generateInstrumentOnce(INTEGRATION_NAME, () => new DataloaderInstrumentation()); - -const _dataloaderIntegration = (() => { - return { - name: INTEGRATION_NAME, - setupOnce() { - // Decide here, not in the factory: the runtime channel injection runs inside `Sentry.init()`, - // after the integrations array has already been built, so `isOrchestrionInjected()` is only - // reliable by `setupOnce`. When the diagnostics channels are injected (runtime hook or bundler - // plugin), subscribe to them; otherwise fall back to the vendored OTel instrumentation. - if (isOrchestrionInjected()) { - dataloaderChannelIntegration().setupOnce?.(); - } else { - instrumentDataloader(); - } - }, - }; -}) satisfies IntegrationFn; - -/** - * Adds Sentry tracing instrumentation for the [dataloader](https://www.npmjs.com/package/dataloader) library. - * - * For more information, see the [`dataloaderIntegration` documentation](https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/dataloader/). - * - * @example - * ```javascript - * const Sentry = require('@sentry/node'); - * - * Sentry.init({ - * integrations: [Sentry.dataloaderIntegration()], - * }); - * ``` - */ -export const dataloaderIntegration = defineIntegration(_dataloaderIntegration); diff --git a/packages/node/src/integrations/tracing/dataloader/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/dataloader/vendored/instrumentation.ts deleted file mode 100644 index 4fb13c0755b4..000000000000 --- a/packages/node/src/integrations/tracing/dataloader/vendored/instrumentation.ts +++ /dev/null @@ -1,295 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-dataloader - * - Upstream version: @opentelemetry/instrumentation-dataloader@0.35.0 - * - Minor TypeScript strictness adjustments for this repository's compiler settings - */ - -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import { CACHE_KEY, SENTRY_KIND } from '@sentry/conventions/attributes'; -import type { BatchLoadFn, DataLoader, DataLoaderConstructor } from './types'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; - -const MODULE_NAME = 'dataloader'; -const PACKAGE_NAME = '@sentry/instrumentation-dataloader'; -const ORIGIN = 'auto.db.otel.dataloader'; - -type LoadFn = DataLoader['load']; -type LoadManyFn = DataLoader['loadMany']; -type PrimeFn = DataLoader['prime']; -type ClearFn = DataLoader['clear']; -type ClearAllFn = DataLoader['clearAll']; - -function isModule(module: unknown): module is { [Symbol.toStringTag]: 'Module'; default: DataLoaderConstructor } { - return (module as { [Symbol.toStringTag]: string })[Symbol.toStringTag] === 'Module'; -} - -function extractModuleExports(module: unknown): DataLoaderConstructor { - return isModule(module) - ? module.default // ESM - : (module as DataLoaderConstructor); // CommonJS -} - -function getSpanName( - dataloader: DataLoader, - operation: 'load' | 'loadMany' | 'batch' | 'prime' | 'clear' | 'clearAll', -): string { - const dataloaderName = dataloader.name; - if (dataloaderName) { - return `${MODULE_NAME}.${operation} ${dataloaderName}`; - } - - return `${MODULE_NAME}.${operation}`; -} - -// `load`, `loadMany` and `batch` are all cache reads -function getSpanOp(operation: 'load' | 'loadMany' | 'batch' | 'prime' | 'clear' | 'clearAll'): string | undefined { - if (operation === 'load' || operation === 'loadMany' || operation === 'batch') { - return 'cache.get'; - } - - return undefined; -} - -// `load` receives a single key, `loadMany`/`batch` receive a key array. Normalize both to the -// `string[]` shape `cache.key` expects. -function getCacheKey(keyArg: unknown): string[] | undefined { - if (Array.isArray(keyArg)) { - return keyArg.map(key => String(key)); - } - - return keyArg == null ? undefined : [String(keyArg)]; -} - -export class DataloaderInstrumentation extends InstrumentationBase { - constructor(config = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - protected init() { - return [ - new InstrumentationNodeModuleDefinition( - MODULE_NAME, - ['>=2.0.0 <3'], - module => { - const dataloader = extractModuleExports(module); - this._patchLoad(dataloader.prototype); - this._patchLoadMany(dataloader.prototype); - this._patchPrime(dataloader.prototype); - this._patchClear(dataloader.prototype); - this._patchClearAll(dataloader.prototype); - - return this._getPatchedConstructor(dataloader); - }, - module => { - const dataloader = extractModuleExports(module); - ['load', 'loadMany', 'prime', 'clear', 'clearAll'].forEach(method => { - if (isWrapped(dataloader.prototype[method])) { - this._unwrap(dataloader.prototype, method); - } - }); - }, - ), - ]; - } - - private _wrapBatchLoadFn(batchLoadFn: BatchLoadFn): BatchLoadFn { - // oxlint-disable-next-line typescript/no-this-alias - const instrumentation = this; - - return function patchedBatchLoadFn(this: DataLoader, ...args: Parameters>) { - if (!instrumentation.isEnabled()) { - return batchLoadFn.call(this, ...args); - } - - return startSpan( - { - name: getSpanName(this, 'batch'), - links: this._batch?.spanLinks, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: getSpanOp('batch'), - [CACHE_KEY]: getCacheKey(args[0]), - }, - onlyIfParent: true, - }, - () => batchLoadFn.apply(this, args), - ); - }; - } - - private _getPatchedConstructor(constructor: DataLoaderConstructor): DataLoaderConstructor { - // oxlint-disable-next-line typescript/no-this-alias - const instrumentation = this; - const prototype = constructor.prototype; - - if (!instrumentation.isEnabled()) { - return constructor; - } - - // oxlint-disable-next-line typescript/no-explicit-any - function PatchedDataloader(this: DataLoader, ...args: any[]) { - // BatchLoadFn is the first constructor argument - // https://github.com/graphql/dataloader/blob/77c2cd7ca97e8795242018ebc212ce2487e729d2/src/index.js#L47 - if (typeof args[0] === 'function') { - if (isWrapped(args[0])) { - instrumentation._unwrap(args, 0); - } - - args[0] = instrumentation._wrapBatchLoadFn(args[0]); - } - - return constructor.apply(this, args); - } - - PatchedDataloader.prototype = prototype; - return PatchedDataloader as unknown as DataLoaderConstructor; - } - - private _patchLoad(proto: DataLoader) { - // oxlint-disable-next-line typescript/unbound-method - if (isWrapped(proto.load)) { - this._unwrap(proto, 'load'); - } - - this._wrap(proto, 'load', this._getPatchedLoad.bind(this)); - } - - private _getPatchedLoad(original: LoadFn): LoadFn { - return function patchedLoad(this: DataLoader, ...args: Parameters) { - return startSpan( - { - name: getSpanName(this, 'load'), - attributes: { - [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: getSpanOp('load'), - [CACHE_KEY]: getCacheKey(args[0]), - }, - onlyIfParent: true, - }, - span => { - const result = original.call(this, ...args); - - if (this._batch && span.isRecording()) { - if (!this._batch.spanLinks) { - this._batch.spanLinks = []; - } - - this._batch.spanLinks.push({ context: span.spanContext() }); - } - - return result; - }, - ); - }; - } - - private _patchLoadMany(proto: DataLoader) { - // oxlint-disable-next-line typescript/unbound-method - if (isWrapped(proto.loadMany)) { - this._unwrap(proto, 'loadMany'); - } - - this._wrap(proto, 'loadMany', this._getPatchedLoadMany.bind(this)); - } - - private _getPatchedLoadMany(original: LoadManyFn): LoadManyFn { - return function patchedLoadMany(this: DataLoader, ...args: Parameters) { - return startSpan( - { - name: getSpanName(this, 'loadMany'), - attributes: { - [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: getSpanOp('loadMany'), - [CACHE_KEY]: getCacheKey(args[0]), - }, - onlyIfParent: true, - }, - () => original.call(this, ...args), - ); - }; - } - - private _patchPrime(proto: DataLoader) { - // oxlint-disable-next-line typescript/unbound-method - if (isWrapped(proto.prime)) { - this._unwrap(proto, 'prime'); - } - - this._wrap(proto, 'prime', this._getPatchedPrime.bind(this)); - } - - private _getPatchedPrime(original: PrimeFn): PrimeFn { - return function patchedPrime(this: DataLoader, ...args: Parameters) { - return startSpan( - { - name: getSpanName(this, 'prime'), - attributes: { - [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: getSpanOp('prime'), - }, - onlyIfParent: true, - }, - () => original.call(this, ...args), - ); - }; - } - - private _patchClear(proto: DataLoader) { - // oxlint-disable-next-line typescript/unbound-method - if (isWrapped(proto.clear)) { - this._unwrap(proto, 'clear'); - } - - this._wrap(proto, 'clear', this._getPatchedClear.bind(this)); - } - - private _getPatchedClear(original: ClearFn): ClearFn { - return function patchedClear(this: DataLoader, ...args: Parameters) { - return startSpan( - { - name: getSpanName(this, 'clear'), - attributes: { - [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: getSpanOp('clear'), - }, - onlyIfParent: true, - }, - () => original.call(this, ...args), - ); - }; - } - - private _patchClearAll(proto: DataLoader) { - // oxlint-disable-next-line typescript/unbound-method - if (isWrapped(proto.clearAll)) { - this._unwrap(proto, 'clearAll'); - } - - this._wrap(proto, 'clearAll', this._getPatchedClearAll.bind(this)); - } - - private _getPatchedClearAll(original: ClearAllFn): ClearAllFn { - return function patchedClearAll(this: DataLoader, ...args: Parameters) { - return startSpan( - { - name: getSpanName(this, 'clearAll'), - attributes: { - [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: getSpanOp('clearAll'), - }, - onlyIfParent: true, - }, - () => original.call(this, ...args), - ); - }; - } -} diff --git a/packages/node/src/integrations/tracing/dataloader/vendored/types.ts b/packages/node/src/integrations/tracing/dataloader/vendored/types.ts deleted file mode 100644 index 6d4f9ccb9a15..000000000000 --- a/packages/node/src/integrations/tracing/dataloader/vendored/types.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-dataloader - * - Upstream version: @opentelemetry/instrumentation-dataloader@0.35.0 - */ - -import type { SpanLink } from '@sentry/core'; - -/* Simplified types inlined from dataloader. */ - -export type BatchLoadFn = (keys: ReadonlyArray) => PromiseLike>; - -/** A `DataLoader` instance. */ -export interface DataLoader { - _batchLoadFn: BatchLoadFn; - _batch: { spanLinks?: SpanLink[] } | null; - load(key: K): Promise; - loadMany(keys: ArrayLike): Promise>; - prime(key: K, value: V | Error): this; - clear(key: K): this; - clearAll(): this; - name: string | undefined; - // oxlint-disable-next-line typescript/no-explicit-any - [key: string]: any; -} - -/** The `DataLoader` class/constructor. */ -export interface DataLoaderConstructor { - // oxlint-disable-next-line typescript/no-explicit-any - new (batchLoadFn: BatchLoadFn, options?: any): DataLoader; - prototype: DataLoader; -} diff --git a/packages/node/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts index fe48ec31bac3..d7212ac5b009 100644 --- a/packages/node/src/integrations/tracing/express.ts +++ b/packages/node/src/integrations/tracing/express.ts @@ -1,23 +1,6 @@ -// Automatic istrumentation for Express using OTel -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; - -import { generateInstrumentOnce } from '../../otel/instrument'; import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; -import { - type ExpressIntegrationOptions, - debug, - patchExpressModule, - SDK_VERSION, - setupExpressErrorHandler as coreSetupExpressErrorHandler, - type ExpressHandlerOptions, -} from '@sentry/core'; +import { setupExpressErrorHandler as coreSetupExpressErrorHandler, type ExpressHandlerOptions } from '@sentry/core'; export { expressErrorHandler } from '@sentry/core'; -import { DEBUG_BUILD } from '../../debug-build'; -import { setHttpServerSpanRouteAttribute } from '../../utils/setHttpServerSpanRouteAttribute'; - -const INTEGRATION_NAME = 'Express' as const; -const SUPPORTED_VERSIONS = ['>=4.0.0 <6']; export function setupExpressErrorHandler( //oxlint-disable-next-line no-explicit-any @@ -27,41 +10,3 @@ export function setupExpressErrorHandler( coreSetupExpressErrorHandler(app, options); ensureIsWrapped(app.use, 'express'); } - -export type ExpressInstrumentationConfig = InstrumentationConfig & - Omit; - -export const instrumentExpress = generateInstrumentOnce( - INTEGRATION_NAME, - (options?: ExpressInstrumentationConfig) => new ExpressInstrumentation(options), -); - -export class ExpressInstrumentation extends InstrumentationBase { - public constructor(config: ExpressInstrumentationConfig = {}) { - super('sentry-express', SDK_VERSION, config); - } - public init(): InstrumentationNodeModuleDefinition { - const module = new InstrumentationNodeModuleDefinition( - 'express', - SUPPORTED_VERSIONS, - express => { - try { - patchExpressModule(express, () => ({ - ...this.getConfig(), - onRouteResolved(route) { - if (route) { - setHttpServerSpanRouteAttribute(route); - } - }, - })); - } catch (e) { - DEBUG_BUILD && debug.error('Failed to patch express module:', e); - } - return express; - }, - // we do not ever actually unpatch in our SDKs - express => express, - ); - return module; - } -} diff --git a/packages/node/src/integrations/tracing/fastify/index.ts b/packages/node/src/integrations/tracing/fastify/index.ts index 5b52965144aa..3b4977416908 100644 --- a/packages/node/src/integrations/tracing/fastify/index.ts +++ b/packages/node/src/integrations/tracing/fastify/index.ts @@ -1,8 +1,6 @@ -import type { Integration, IntegrationFn } from '@sentry/core'; -import { defineIntegration, extendIntegration, getClient } from '@sentry/core'; -import { generateInstrumentOnce } from '../../../otel/instrument'; +import type { Integration } from '@sentry/core'; +import { defineIntegration, getClient } from '@sentry/core'; import type { FastifyInstance, FastifyMinimal, FastifyReply, FastifyRequest } from './types'; -import { FastifyInstrumentationV3 } from './v3/instrumentation'; import { fastifyIntegration as serverUtilsFastifyIntegration, instrumentFastify, @@ -94,11 +92,6 @@ interface FastifyHandlerOptions { const INTEGRATION_NAME = 'Fastify' as const; -export const instrumentFastifyV3 = generateInstrumentOnce( - `${INTEGRATION_NAME}.v3`, - () => new FastifyInstrumentationV3(), -); - function getFastifyIntegration(): FastifyIntegration | undefined { const client = getClient(); if (!client) { @@ -108,16 +101,6 @@ function getFastifyIntegration(): FastifyIntegration | undefined { } } -const _fastifyIntegration = ((options: Partial) => { - const parentIntegration = serverUtilsFastifyIntegration(options) as FastifyIntegration; - - return extendIntegration(parentIntegration, { - setupOnce() { - instrumentFastifyV3(); - }, - }); -}) satisfies IntegrationFn; - /** * Adds Sentry tracing instrumentation for [Fastify](https://fastify.dev/). * @@ -135,7 +118,7 @@ const _fastifyIntegration = ((options: Partial) => { * ``` */ export const fastifyIntegration = defineIntegration((options: Partial = {}) => - _fastifyIntegration(options), + serverUtilsFastifyIntegration(options), ); /** diff --git a/packages/node/src/integrations/tracing/fastify/v3/constants.ts b/packages/node/src/integrations/tracing/fastify/v3/constants.ts deleted file mode 100644 index 10a64ed3b420..000000000000 --- a/packages/node/src/integrations/tracing/fastify/v3/constants.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Vendored from https://github.com/open-telemetry/opentelemetry-js-contrib/blob/407f61591ba69a39a6908264379d4d98a48dbec4/plugins/node/opentelemetry-instrumentation-fastify/src/constants.ts -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export const spanRequestSymbol = Symbol('opentelemetry.instrumentation.fastify.request_active_span'); - -// The instrumentation creates a span for invocations of lifecycle hook handlers -// that take `(request, reply, ...[, done])` arguments. Currently this is all -// lifecycle hooks except `onRequestAbort`. -// https://fastify.dev/docs/latest/Reference/Hooks -export const hooksNamesToWrap = new Set([ - 'onTimeout', - 'onRequest', - 'preParsing', - 'preValidation', - 'preSerialization', - 'preHandler', - 'onSend', - 'onResponse', - 'onError', -]); diff --git a/packages/node/src/integrations/tracing/fastify/v3/enums/AttributeNames.ts b/packages/node/src/integrations/tracing/fastify/v3/enums/AttributeNames.ts deleted file mode 100644 index 343b68747f06..000000000000 --- a/packages/node/src/integrations/tracing/fastify/v3/enums/AttributeNames.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Vendored from https://github.com/open-telemetry/opentelemetry-js-contrib/blob/407f61591ba69a39a6908264379d4d98a48dbec4/plugins/node/opentelemetry-instrumentation-fastify/src/enums/AttributeNames.ts -// -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export enum AttributeNames { - FASTIFY_NAME = 'fastify.name', - FASTIFY_TYPE = 'fastify.type', - HOOK_NAME = 'hook.name', - PLUGIN_NAME = 'plugin.name', -} - -export enum FastifyTypes { - MIDDLEWARE = 'middleware', - REQUEST_HANDLER = 'request_handler', -} - -export enum FastifyNames { - MIDDLEWARE = 'middleware', - REQUEST_HANDLER = 'request handler', -} diff --git a/packages/node/src/integrations/tracing/fastify/v3/instrumentation.ts b/packages/node/src/integrations/tracing/fastify/v3/instrumentation.ts deleted file mode 100644 index d464b3d4d8cf..000000000000 --- a/packages/node/src/integrations/tracing/fastify/v3/instrumentation.ts +++ /dev/null @@ -1,334 +0,0 @@ -// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/407f61591ba69a39a6908264379d4d98a48dbec4/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-this-alias */ -/* eslint-disable jsdoc/require-jsdoc */ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ - -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { type Attributes, context, SpanStatusCode, trace } from '@opentelemetry/api'; -import { - InstrumentationBase, - InstrumentationNodeModuleDefinition, - safeExecuteInTheMiddle, -} from '@opentelemetry/instrumentation'; -import { HTTP_ROUTE } from '@sentry/conventions/attributes'; -import type { Span } from '@sentry/core'; -import { - getClient, - getIsolationScope, - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - spanToJSON, -} from '@sentry/core'; -import { setHttpServerSpanRouteAttribute } from '../../../../utils/setHttpServerSpanRouteAttribute'; -import type { - FastifyErrorCodes, - FastifyInstance, - FastifyReply, - FastifyRequest, - HandlerOriginal, - HookHandlerDoneFunction, -} from '../types'; -import { AttributeNames, FastifyNames, FastifyTypes } from './enums/AttributeNames'; -import type { PluginFastifyReply } from './internal-types'; -import type { FastifyInstrumentationConfig } from './types'; -import { endSpan, safeExecuteInTheMiddleMaybePromise, startSpan } from './utils'; -/** @knipignore */ - -const PACKAGE_NAME = '@sentry/instrumentation-fastify-v3'; -const ANONYMOUS_NAME = 'anonymous'; - -// The instrumentation creates a span for invocations of lifecycle hook handlers -// that take `(request, reply, ...[, done])` arguments. Currently this is all -// lifecycle hooks except `onRequestAbort`. -// https://fastify.dev/docs/latest/Reference/Hooks -const hooksNamesToWrap = new Set([ - 'onTimeout', - 'onRequest', - 'preParsing', - 'preValidation', - 'preSerialization', - 'preHandler', - 'onSend', - 'onResponse', - 'onError', -]); - -/** - * Fastify instrumentation for OpenTelemetry - */ -export class FastifyInstrumentationV3 extends InstrumentationBase { - public constructor(config: FastifyInstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - public init(): InstrumentationNodeModuleDefinition[] { - return [ - new InstrumentationNodeModuleDefinition('fastify', ['>=3.0.0 <3.21.0'], moduleExports => { - return this._patchConstructor(moduleExports); - }), - ]; - } - - private _hookOnRequest() { - const instrumentation = this; - - return function onRequest(request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) { - if (!instrumentation.isEnabled()) { - return done(); - } - instrumentation._wrap(reply, 'send', instrumentation._patchSend()); - - const anyRequest = request as any; - - const routeName = anyRequest.routeOptions - ? anyRequest.routeOptions.url // since fastify@4.10.0 - : request.routerPath; - if (routeName) { - setHttpServerSpanRouteAttribute(routeName); - } - - const method = request.method || 'GET'; - - getIsolationScope().setTransactionName(`${method} ${routeName}`); - done(); - }; - } - - private _wrapHandler( - pluginName: string, - hookName: string, - original: HandlerOriginal, - syncFunctionWithDone: boolean, - ): () => Promise { - const instrumentation = this; - this._diag.debug('Patching fastify route.handler function'); - - return function (this: any, ...args: unknown[]): Promise { - if (!instrumentation.isEnabled()) { - return original.apply(this, args); - } - - const name = original.name || pluginName || ANONYMOUS_NAME; - const spanName = `${FastifyNames.MIDDLEWARE} - ${name}`; - - const reply = args[1] as PluginFastifyReply; - - const span = startSpan(reply, instrumentation.tracer, spanName, { - [AttributeNames.FASTIFY_TYPE]: FastifyTypes.MIDDLEWARE, - [AttributeNames.PLUGIN_NAME]: pluginName, - [AttributeNames.HOOK_NAME]: hookName, - }); - - const origDone = syncFunctionWithDone && (args[args.length - 1] as HookHandlerDoneFunction); - if (origDone) { - args[args.length - 1] = function (...doneArgs: Parameters) { - endSpan(reply); - origDone.apply(this, doneArgs); - }; - } - - return context.with(trace.setSpan(context.active(), span), () => { - return safeExecuteInTheMiddleMaybePromise( - () => { - return original.apply(this, args); - }, - err => { - if (err instanceof Error) { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: err.message, - }); - span.recordException(err); - } - // async hooks should end the span as soon as the promise is resolved - if (!syncFunctionWithDone) { - endSpan(reply); - } - }, - ); - }); - }; - } - - private _wrapAddHook(): (original: FastifyInstance['addHook']) => () => FastifyInstance { - const instrumentation = this; - this._diag.debug('Patching fastify server.addHook function'); - - // biome-ignore lint/complexity/useArrowFunction: - return function (original: FastifyInstance['addHook']): () => FastifyInstance { - return function wrappedAddHook(this: any, ...args: any) { - const name = args[0] as string; - const handler = args[1] as HandlerOriginal; - const pluginName = this.pluginName; - if (!hooksNamesToWrap.has(name)) { - return original.apply(this, args); - } - - const syncFunctionWithDone = - typeof args[args.length - 1] === 'function' && handler.constructor.name !== 'AsyncFunction'; - - return original.apply(this, [ - name, - instrumentation._wrapHandler(pluginName, name, handler, syncFunctionWithDone), - ] as never); - }; - }; - } - - private _patchConstructor(moduleExports: { - fastify: () => FastifyInstance; - errorCodes: FastifyErrorCodes | undefined; - }): () => FastifyInstance { - const instrumentation = this; - - function fastify(this: FastifyInstance, ...args: any) { - const app: FastifyInstance = moduleExports.fastify.apply(this, args); - app.addHook('onRequest', instrumentation._hookOnRequest()); - app.addHook('preHandler', instrumentation._hookPreHandler()); - - instrumentClient(); - - instrumentation._wrap(app, 'addHook', instrumentation._wrapAddHook()); - - return app; - } - - if (moduleExports.errorCodes !== undefined) { - fastify.errorCodes = moduleExports.errorCodes; - } - fastify.fastify = fastify; - fastify.default = fastify; - return fastify; - } - - private _patchSend() { - const instrumentation = this; - this._diag.debug('Patching fastify reply.send function'); - - return function patchSend(original: () => FastifyReply): () => FastifyReply { - return function send(this: FastifyReply, ...args: any) { - const maybeError: any = args[0]; - - if (!instrumentation.isEnabled()) { - return original.apply(this, args); - } - - return safeExecuteInTheMiddle( - () => { - return original.apply(this, args); - }, - err => { - if (!err && maybeError instanceof Error) { - // eslint-disable-next-line no-param-reassign - err = maybeError; - } - endSpan(this, err); - }, - ); - }; - }; - } - - private _hookPreHandler() { - const instrumentation = this; - this._diag.debug('Patching fastify preHandler function'); - - return function preHandler(this: any, request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) { - if (!instrumentation.isEnabled()) { - return done(); - } - const anyRequest = request as any; - - const handler = anyRequest.routeOptions?.handler || anyRequest.context?.handler; - const handlerName = handler?.name.startsWith('bound ') ? handler.name.substring(6) : handler?.name; - const spanName = `${FastifyNames.REQUEST_HANDLER} - ${handlerName || this.pluginName || ANONYMOUS_NAME}`; - - const spanAttributes: Attributes = { - [AttributeNames.PLUGIN_NAME]: this.pluginName, - [AttributeNames.FASTIFY_TYPE]: FastifyTypes.REQUEST_HANDLER, - // eslint-disable-next-line typescript/no-deprecated - [HTTP_ROUTE]: anyRequest.routeOptions - ? anyRequest.routeOptions.url // since fastify@4.10.0 - : request.routerPath, - }; - if (handlerName) { - spanAttributes[AttributeNames.FASTIFY_NAME] = handlerName; - } - const span = startSpan(reply, instrumentation.tracer, spanName, spanAttributes); - - addFastifyV3SpanAttributes(span); - - const { requestHook } = instrumentation.getConfig(); - if (requestHook) { - safeExecuteInTheMiddle( - () => requestHook(span, { request }), - e => { - if (e) { - instrumentation._diag.error('request hook failed', e); - } - }, - true, - ); - } - - return context.with(trace.setSpan(context.active(), span), () => { - done(); - }); - }; - } -} - -function instrumentClient(): void { - const client = getClient(); - if (client) { - client.on('spanStart', (span: Span) => { - addFastifyV3SpanAttributes(span); - }); - } -} - -function addFastifyV3SpanAttributes(span: Span): void { - const attributes = spanToJSON(span).data; - - // this is one of: middleware, request_handler - const type = attributes['fastify.type']; - - // If this is already set, or we have no fastify span, no need to process again... - if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) { - return; - } - - span.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.fastify', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.fastify`, - }); - - // Also update the name, we don't need to "middleware - " prefix - const name = attributes['fastify.name'] || attributes['plugin.name'] || attributes['hook.name']; - if (typeof name === 'string') { - // Try removing `fastify -> ` and `@fastify/otel -> ` prefixes - // This is a bit of a hack, and not always working for all spans - // But it's the best we can do without a proper API - const updatedName = name.replace(/^fastify -> /, '').replace(/^@fastify\/otel -> /, ''); - - span.updateName(updatedName); - } -} diff --git a/packages/node/src/integrations/tracing/fastify/v3/internal-types.ts b/packages/node/src/integrations/tracing/fastify/v3/internal-types.ts deleted file mode 100644 index f71a92df0685..000000000000 --- a/packages/node/src/integrations/tracing/fastify/v3/internal-types.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/407f61591ba69a39a6908264379d4d98a48dbec4/plugins/node/opentelemetry-instrumentation-fastify/src/internal-types.ts -import type { Span } from '@opentelemetry/api'; -import type { FastifyReply } from '../types'; -import type { spanRequestSymbol } from './constants'; - -export type PluginFastifyReply = FastifyReply & { - [spanRequestSymbol]?: Span[]; -}; diff --git a/packages/node/src/integrations/tracing/fastify/v3/types.ts b/packages/node/src/integrations/tracing/fastify/v3/types.ts deleted file mode 100644 index 4d41a729f9d0..000000000000 --- a/packages/node/src/integrations/tracing/fastify/v3/types.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/407f61591ba69a39a6908264379d4d98a48dbec4/plugins/node/opentelemetry-instrumentation-fastify/src/types.ts -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Span } from '@opentelemetry/api'; -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; - -export interface FastifyRequestInfo { - request: any; // FastifyRequest object from fastify package -} - -/** - * Function that can be used to add custom attributes to the current span - * @param span - The Fastify handler span. - * @param info - The Fastify request info object. - */ -export interface FastifyCustomAttributeFunction { - (span: Span, info: FastifyRequestInfo): void; -} - -/** - * Options available for the Fastify Instrumentation - */ -export interface FastifyInstrumentationConfig extends InstrumentationConfig { - /** Function for adding custom attributes to each handler span */ - requestHook?: FastifyCustomAttributeFunction; -} diff --git a/packages/node/src/integrations/tracing/fastify/v3/utils.ts b/packages/node/src/integrations/tracing/fastify/v3/utils.ts deleted file mode 100644 index 26f0014a67e1..000000000000 --- a/packages/node/src/integrations/tracing/fastify/v3/utils.ts +++ /dev/null @@ -1,137 +0,0 @@ -// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/407f61591ba69a39a6908264379d4d98a48dbec4/plugins/node/opentelemetry-instrumentation-fastify/src/utils.ts -/* eslint-disable jsdoc/require-jsdoc */ -/* eslint-disable @typescript-eslint/no-dynamic-delete */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { type Attributes, type Span, SpanStatusCode, type Tracer } from '@opentelemetry/api'; -import { spanRequestSymbol } from './constants'; -import type { PluginFastifyReply } from './internal-types'; - -/** - * Starts Span - * @param reply - reply function - * @param tracer - tracer - * @param spanName - span name - * @param spanAttributes - span attributes - */ -export function startSpan( - reply: PluginFastifyReply, - tracer: Tracer, - spanName: string, - spanAttributes: Attributes = {}, -) { - const span = tracer.startSpan(spanName, { attributes: spanAttributes }); - - const spans: Span[] = reply[spanRequestSymbol] || []; - spans.push(span); - - Object.defineProperty(reply, spanRequestSymbol, { - enumerable: false, - configurable: true, - value: spans, - }); - - return span; -} - -/** - * Ends span - * @param reply - reply function - * @param err - error - */ -export function endSpan(reply: PluginFastifyReply, err?: any) { - const spans = reply[spanRequestSymbol] || []; - // there is no active span, or it has already ended - if (!spans.length) { - return; - } - // biome-ignore lint/complexity/noForEach: - spans.forEach((span: Span) => { - if (err) { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: err.message, - }); - span.recordException(err); - } - span.end(); - }); - delete reply[spanRequestSymbol]; -} - -// @TODO after approve add this to instrumentation package and replace usage -// when it will be released - -/** - * This function handles the missing case from instrumentation package when - * execute can either return a promise or void. And using async is not an - * option as it is producing unwanted side effects. - * @param execute - function to be executed - * @param onFinish - function called when function executed - * @param preventThrowingError - prevent to throw error when execute - * function fails - */ -export function safeExecuteInTheMiddleMaybePromise( - execute: () => Promise, - onFinish: (e: unknown, result?: T) => void, - preventThrowingError?: boolean, -): Promise; -export function safeExecuteInTheMiddleMaybePromise( - execute: () => T, - onFinish: (e: unknown, result?: T) => void, - preventThrowingError?: boolean, -): T; -export function safeExecuteInTheMiddleMaybePromise( - execute: () => T | Promise, - onFinish: (e: unknown, result?: T) => void, - preventThrowingError?: boolean, -): T | Promise | undefined { - let error: unknown; - let result: T | Promise | undefined = undefined; - try { - result = execute(); - - if (isPromise(result)) { - result.then( - res => onFinish(undefined, res), - err => onFinish(err), - ); - } - } catch (e) { - error = e; - } finally { - if (!isPromise(result)) { - onFinish(error, result); - if (error && !preventThrowingError) { - // eslint-disable-next-line no-unsafe-finally - throw error; - } - } - // eslint-disable-next-line no-unsafe-finally - return result; - } -} - -function isPromise(val: T | Promise): val is Promise { - return ( - (typeof val === 'object' && val && typeof Object.getOwnPropertyDescriptor(val, 'then')?.value === 'function') || - false - ); -} diff --git a/packages/node/src/integrations/tracing/firebase/firebase.ts b/packages/node/src/integrations/tracing/firebase/firebase.ts deleted file mode 100644 index c56199f897e7..000000000000 --- a/packages/node/src/integrations/tracing/firebase/firebase.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { generateInstrumentOnce } from '../../../otel/instrument'; -import { FirebaseInstrumentation } from './otel'; - -const INTEGRATION_NAME = 'Firebase' as const; - -export const instrumentFirebase = generateInstrumentOnce(INTEGRATION_NAME, () => new FirebaseInstrumentation()); diff --git a/packages/node/src/integrations/tracing/firebase/index.ts b/packages/node/src/integrations/tracing/firebase/index.ts deleted file mode 100644 index 5588511bf303..000000000000 --- a/packages/node/src/integrations/tracing/firebase/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './firebase'; diff --git a/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts b/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts deleted file mode 100644 index ed57b02b1035..000000000000 --- a/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { InstrumentationConfig, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; -import { InstrumentationBase } from '@opentelemetry/instrumentation'; -import { SDK_VERSION } from '@sentry/core'; -import { patchFirestore } from './patches/firestore'; -import { patchFunctions } from './patches/functions'; - -const firestoreSupportedVersions = ['>=3.0.0 <5']; // firebase 9+ -const functionsSupportedVersions = ['>=6.0.0 <7']; // firebase-functions v2 - -/** - * Instrumentation for Firebase services, specifically Firestore. - */ -export class FirebaseInstrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { - super('@sentry/instrumentation-firebase', SDK_VERSION, config); - } - - /** - * - * @protected - */ - // eslint-disable-next-line @typescript-eslint/naming-convention - protected init(): InstrumentationNodeModuleDefinition | InstrumentationNodeModuleDefinition[] | void { - const modules: InstrumentationNodeModuleDefinition[] = []; - - modules.push(patchFirestore(firestoreSupportedVersions, this._wrap, this._unwrap)); - modules.push(patchFunctions(functionsSupportedVersions, this._wrap, this._unwrap)); - - return modules; - } -} diff --git a/packages/node/src/integrations/tracing/firebase/otel/index.ts b/packages/node/src/integrations/tracing/firebase/otel/index.ts deleted file mode 100644 index 88520ba3efee..000000000000 --- a/packages/node/src/integrations/tracing/firebase/otel/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// The structure inside OTEL is to be kept as close as possible to an opentelemetry plugin. -export * from './firebaseInstrumentation'; -export * from './types'; diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts deleted file mode 100644 index 0f09d0209fe3..000000000000 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/firestore.ts +++ /dev/null @@ -1,272 +0,0 @@ -import * as net from 'node:net'; -import { InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import { InstrumentationNodeModuleFile } from '../../../InstrumentationNodeModuleFile'; -import { - DB_COLLECTION_NAME, - DB_NAMESPACE, - DB_OPERATION_NAME, - DB_SYSTEM_NAME, - SENTRY_KIND, - SERVER_ADDRESS, - SERVER_PORT, -} from '@sentry/conventions/attributes'; -import type { SpanAttributes } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; -import type { FirebaseInstrumentation } from '../firebaseInstrumentation'; -import type { - AddDocType, - CollectionReference, - DeleteDocType, - DocumentData, - DocumentReference, - FirebaseApp, - FirebaseOptions, - FirestoreSettings, - GetDocsType, - PartialWithFieldValue, - QuerySnapshot, - SetDocType, - SetOptions, - WithFieldValue, -} from '../types'; - -// Inline minimal types used from `shimmer` to avoid importing shimmer's types directly. -// We only need the shape for `wrap` and `unwrap` used in this file. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type ShimmerWrap = (target: any, name: string, wrapper: (...args: any[]) => any) => void; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type ShimmerUnwrap = (target: any, name: string) => void; - -/** - * - * @param firestoreSupportedVersions - supported version of firebase/firestore - * @param wrap - reference to native instrumentation wrap function - * @param unwrap - reference to native instrumentation wrap function - */ -export function patchFirestore( - firestoreSupportedVersions: string[], - wrap: ShimmerWrap, - unwrap: ShimmerUnwrap, -): InstrumentationNodeModuleDefinition { - const moduleFirestoreCJS = new InstrumentationNodeModuleDefinition( - '@firebase/firestore', - firestoreSupportedVersions, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (moduleExports: any) => wrapMethods(moduleExports, wrap, unwrap), - ); - const files: string[] = [ - '@firebase/firestore/dist/lite/index.node.cjs.js', - '@firebase/firestore/dist/lite/index.node.mjs.js', - '@firebase/firestore/dist/lite/index.rn.esm2017.js', - '@firebase/firestore/dist/lite/index.cjs.js', - ]; - - for (const file of files) { - moduleFirestoreCJS.files.push( - new InstrumentationNodeModuleFile( - file, - firestoreSupportedVersions, - moduleExports => wrapMethods(moduleExports, wrap, unwrap), - moduleExports => unwrapMethods(moduleExports, unwrap), - ), - ); - } - - return moduleFirestoreCJS; -} - -function wrapMethods( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - moduleExports: any, - wrap: ShimmerWrap, - unwrap: ShimmerUnwrap, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): any { - unwrapMethods(moduleExports, unwrap); - - wrap(moduleExports, 'addDoc', patchAddDoc()); - wrap(moduleExports, 'getDocs', patchGetDocs()); - wrap(moduleExports, 'setDoc', patchSetDoc()); - wrap(moduleExports, 'deleteDoc', patchDeleteDoc()); - - return moduleExports; -} - -function unwrapMethods( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - moduleExports: any, - unwrap: ShimmerUnwrap, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): any { - for (const method of ['addDoc', 'getDocs', 'setDoc', 'deleteDoc']) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (isWrapped(moduleExports[method])) { - unwrap(moduleExports, method); - } - } - return moduleExports; -} - -function patchAddDoc(): ( - original: AddDocType, -) => ( - this: FirebaseInstrumentation, - reference: CollectionReference, - data: WithFieldValue, -) => Promise> { - return function addDoc(original: AddDocType) { - return function ( - reference: CollectionReference, - data: WithFieldValue, - ): Promise> { - return startFirestoreSpan('addDoc', reference, () => original(reference, data)); - }; - }; -} - -function patchDeleteDoc(): ( - original: DeleteDocType, -) => (this: FirebaseInstrumentation, reference: DocumentReference) => Promise { - return function deleteDoc(original: DeleteDocType) { - return function (reference: DocumentReference): Promise { - return startFirestoreSpan('deleteDoc', reference.parent || reference, () => original(reference)); - }; - }; -} - -function patchGetDocs(): ( - original: GetDocsType, -) => ( - this: FirebaseInstrumentation, - reference: CollectionReference, -) => Promise> { - return function getDocs(original: GetDocsType) { - return function ( - reference: CollectionReference, - ): Promise> { - return startFirestoreSpan('getDocs', reference, () => original(reference)); - }; - }; -} - -function patchSetDoc(): ( - original: SetDocType, -) => ( - this: FirebaseInstrumentation, - reference: DocumentReference, - data: WithFieldValue & PartialWithFieldValue, - options?: SetOptions, -) => Promise { - return function setDoc(original: SetDocType) { - return function ( - reference: DocumentReference, - data: WithFieldValue & PartialWithFieldValue, - options?: SetOptions, - ): Promise { - return startFirestoreSpan('setDoc', reference.parent || reference, () => { - return typeof options !== 'undefined' ? original(reference, data, options) : original(reference, data); - }); - }; - }; -} - -function startFirestoreSpan( - spanName: string, - reference: CollectionReference | DocumentReference, - callback: () => T, -): T { - return startSpan( - { - name: `${spanName} ${reference.path}`, - op: 'db.query', - attributes: { - [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.firebase.otel.firestore', - [DB_OPERATION_NAME]: spanName, - ...buildAttributes(reference), - }, - }, - callback, - ); -} - -/** - * Gets the server address and port attributes from the Firestore settings. - * It's best effort to extract the address and port from the settings, especially for IPv6. - * @param settings - The Firestore settings containing host information. - */ -export function getPortAndAddress(settings: FirestoreSettings): { - address?: string; - port?: number; -} { - let address: string | undefined; - let port: string | undefined; - - if (typeof settings.host === 'string') { - if (settings.host.startsWith('[')) { - // IPv6 addresses can be enclosed in square brackets, e.g., [2001:db8::1]:8080 - if (settings.host.endsWith(']')) { - // IPv6 with square brackets without port - address = settings.host.replace(/^\[|\]$/g, ''); - } else if (settings.host.includes(']:')) { - // IPv6 with square brackets with port - const lastColonIndex = settings.host.lastIndexOf(':'); - if (lastColonIndex !== -1) { - address = settings.host.slice(1, lastColonIndex).replace(/^\[|\]$/g, ''); - port = settings.host.slice(lastColonIndex + 1); - } - } - } else { - // IPv4 or IPv6 without square brackets - // If it's an IPv6 address without square brackets, we assume it does not have a port. - if (net.isIPv6(settings.host)) { - address = settings.host; - } - // If it's an IPv4 address, we can extract the port if it exists. - else { - const lastColonIndex = settings.host.lastIndexOf(':'); - if (lastColonIndex !== -1) { - address = settings.host.slice(0, lastColonIndex); - port = settings.host.slice(lastColonIndex + 1); - } else { - address = settings.host; - } - } - } - } - return { - address: address, - port: port ? parseInt(port, 10) : undefined, - }; -} - -function buildAttributes( - reference: CollectionReference | DocumentReference, -): SpanAttributes { - const firestoreApp: FirebaseApp = reference.firestore.app; - const firestoreOptions: FirebaseOptions = firestoreApp.options; - const json: { settings?: FirestoreSettings } = reference.firestore.toJSON() || {}; - const settings: FirestoreSettings = json.settings || {}; - - const attributes: SpanAttributes = { - [DB_COLLECTION_NAME]: reference.path, - [DB_NAMESPACE]: firestoreApp.name, - [DB_SYSTEM_NAME]: 'firebase.firestore', - 'firebase.firestore.type': reference.type, - 'firebase.firestore.options.projectId': firestoreOptions.projectId, - 'firebase.firestore.options.appId': firestoreOptions.appId, - 'firebase.firestore.options.messagingSenderId': firestoreOptions.messagingSenderId, - 'firebase.firestore.options.storageBucket': firestoreOptions.storageBucket, - }; - - const { address, port } = getPortAndAddress(settings); - - if (address) { - attributes[SERVER_ADDRESS] = address; - } - if (port) { - attributes[SERVER_PORT] = port; - } - - return attributes; -} diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts deleted file mode 100644 index e54167d43d27..000000000000 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts +++ /dev/null @@ -1,191 +0,0 @@ -import type { InstrumentationBase } from '@opentelemetry/instrumentation'; -import { InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import { InstrumentationNodeModuleFile } from '../../../InstrumentationNodeModuleFile'; -import { SENTRY_KIND } from '@sentry/conventions/attributes'; -import type { SpanAttributes } from '@sentry/core'; -import { - captureException, - flush, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startSpanManual, -} from '@sentry/core'; -import type { FirebaseInstrumentation } from '../firebaseInstrumentation'; -import type { AvailableFirebaseFunctions, FirebaseFunctions, OverloadedParameters } from '../types'; - -/** - * Patches Firebase Functions v2 to add OpenTelemetry instrumentation - * @param functionsSupportedVersions - supported versions of firebase-functions - * @param wrap - reference to native instrumentation wrap function - * @param unwrap - reference to native instrumentation unwrap function - */ -export function patchFunctions( - functionsSupportedVersions: string[], - wrap: InstrumentationBase['_wrap'], - unwrap: InstrumentationBase['_unwrap'], -): InstrumentationNodeModuleDefinition { - const moduleFunctionsCJS = new InstrumentationNodeModuleDefinition('firebase-functions', functionsSupportedVersions); - const modulesToInstrument = [ - { name: 'firebase-functions/lib/v2/providers/https.js', triggerType: 'function' }, - { name: 'firebase-functions/lib/v2/providers/firestore.js', triggerType: 'firestore' }, - { name: 'firebase-functions/lib/v2/providers/scheduler.js', triggerType: 'scheduler' }, - { name: 'firebase-functions/lib/v2/storage.js', triggerType: 'storage' }, - ] as const; - - modulesToInstrument.forEach(({ name, triggerType }) => { - moduleFunctionsCJS.files.push( - new InstrumentationNodeModuleFile( - name, - functionsSupportedVersions, - moduleExports => wrapCommonFunctions(moduleExports, wrap, unwrap, triggerType), - moduleExports => unwrapCommonFunctions(moduleExports, unwrap), - ), - ); - }); - - return moduleFunctionsCJS; -} - -/** - * Patches Cloud Functions for Firebase (v2) to add OpenTelemetry instrumentation - * - * @param triggerType - Type of trigger - * @returns A function that patches the function - */ -export function patchV2Functions( - triggerType: string, -): (original: T) => (...args: OverloadedParameters) => ReturnType { - return function v2FunctionsWrapper(original: T): (...args: OverloadedParameters) => ReturnType { - return function (this: FirebaseInstrumentation, ...args: OverloadedParameters): ReturnType { - const handler = typeof args[0] === 'function' ? args[0] : args[1]; - const documentOrOptions = typeof args[0] === 'function' ? undefined : args[0]; - - if (!handler) { - return original.call(this, ...args); - } - - const wrappedHandler = async function (this: unknown, ...handlerArgs: unknown[]): Promise { - const functionName = process.env.FUNCTION_TARGET || process.env.K_SERVICE || 'unknown'; - - const attributes: SpanAttributes = { - [SENTRY_KIND]: 'server', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.firebase.otel.functions', - 'faas.name': functionName, - 'faas.trigger': triggerType, - 'faas.provider': 'firebase', - }; - - if (process.env.GCLOUD_PROJECT) { - attributes['cloud.project_id'] = process.env.GCLOUD_PROJECT; - } - - if (process.env.EVENTARC_CLOUD_EVENT_SOURCE) { - attributes['cloud.event_source'] = process.env.EVENTARC_CLOUD_EVENT_SOURCE; - } - - // `startSpanManual` to keep the span active but while still allowing us to end it before flushing on error. - return startSpanManual( - { - name: `firebase.function.${triggerType}`, - op: 'http.request', - attributes, - }, - async span => { - try { - const result = await handler.apply(this, handlerArgs); - span.end(); - return result; - } catch (error) { - span.setStatus({ code: SPAN_STATUS_ERROR }); - captureException(error, { - mechanism: { - type: 'auto.firebase.otel.functions', - handled: false, - }, - }); - span.end(); - await flush(2000); - throw error; - } - }, - ); - }; - - if (documentOrOptions) { - return original.call(this, documentOrOptions, wrappedHandler); - } else { - return original.call(this, wrappedHandler); - } - }; - }; -} - -function wrapCommonFunctions( - moduleExports: AvailableFirebaseFunctions, - wrap: InstrumentationBase['_wrap'], - unwrap: InstrumentationBase['_unwrap'], - triggerType: 'function' | 'firestore' | 'scheduler' | 'storage', -): AvailableFirebaseFunctions { - unwrapCommonFunctions(moduleExports, unwrap); - - switch (triggerType) { - case 'function': - wrap(moduleExports, 'onRequest', patchV2Functions('http.request')); - wrap(moduleExports, 'onCall', patchV2Functions('http.call')); - break; - - case 'firestore': - wrap(moduleExports, 'onDocumentCreated', patchV2Functions('firestore.document.created')); - wrap(moduleExports, 'onDocumentUpdated', patchV2Functions('firestore.document.updated')); - wrap(moduleExports, 'onDocumentDeleted', patchV2Functions('firestore.document.deleted')); - wrap(moduleExports, 'onDocumentWritten', patchV2Functions('firestore.document.written')); - wrap(moduleExports, 'onDocumentCreatedWithAuthContext', patchV2Functions('firestore.document.created')); - wrap(moduleExports, 'onDocumentUpdatedWithAuthContext', patchV2Functions('firestore.document.updated')); - wrap(moduleExports, 'onDocumentDeletedWithAuthContext', patchV2Functions('firestore.document.deleted')); - wrap(moduleExports, 'onDocumentWrittenWithAuthContext', patchV2Functions('firestore.document.written')); - break; - - case 'scheduler': - wrap(moduleExports, 'onSchedule', patchV2Functions('scheduler.scheduled')); - break; - - case 'storage': - wrap(moduleExports, 'onObjectFinalized', patchV2Functions('storage.object.finalized')); - wrap(moduleExports, 'onObjectArchived', patchV2Functions('storage.object.archived')); - wrap(moduleExports, 'onObjectDeleted', patchV2Functions('storage.object.deleted')); - wrap(moduleExports, 'onObjectMetadataUpdated', patchV2Functions('storage.object.metadataUpdated')); - break; - } - - return moduleExports; -} - -function unwrapCommonFunctions( - moduleExports: AvailableFirebaseFunctions, - unwrap: InstrumentationBase['_unwrap'], -): AvailableFirebaseFunctions { - const methods: (keyof AvailableFirebaseFunctions)[] = [ - 'onSchedule', - 'onRequest', - 'onCall', - 'onObjectFinalized', - 'onObjectArchived', - 'onObjectDeleted', - 'onObjectMetadataUpdated', - 'onDocumentCreated', - 'onDocumentUpdated', - 'onDocumentDeleted', - 'onDocumentWritten', - 'onDocumentCreatedWithAuthContext', - 'onDocumentUpdatedWithAuthContext', - 'onDocumentDeletedWithAuthContext', - 'onDocumentWrittenWithAuthContext', - ]; - - for (const method of methods) { - if (isWrapped(moduleExports[method])) { - unwrap(moduleExports, method); - } - } - return moduleExports; -} diff --git a/packages/node/src/integrations/tracing/firebase/otel/types.ts b/packages/node/src/integrations/tracing/firebase/otel/types.ts deleted file mode 100644 index 6bba7d59a35b..000000000000 --- a/packages/node/src/integrations/tracing/firebase/otel/types.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -// Inlined types from 'firebase/app' -export interface FirebaseOptions { - [key: string]: any; - apiKey?: string; - authDomain?: string; - databaseURL?: string; - projectId?: string; - storageBucket?: string; - messagingSenderId?: string; - appId?: string; - measurementId?: string; -} - -export interface FirebaseApp { - name: string; - options: FirebaseOptions; - automaticDataCollectionEnabled: boolean; - delete(): Promise; -} - -// Inlined types from 'firebase/firestore' -export interface DocumentData { - [field: string]: any; -} - -export type WithFieldValue = T; - -export type PartialWithFieldValue = Partial; - -export interface SetOptions { - merge?: boolean; - mergeFields?: (string | number | symbol)[]; -} - -export interface DocumentReference { - id: string; - firestore: { - app: FirebaseApp; - settings: FirestoreSettings; - useEmulator: (host: string, port: number) => void; - toJSON: () => { - app: FirebaseApp; - settings: FirestoreSettings; - }; - }; - type: 'collection' | 'document' | string; - path: string; - parent: CollectionReference; -} - -export interface CollectionReference { - id: string; - firestore: { - app: FirebaseApp; - settings: FirestoreSettings; - useEmulator: (host: string, port: number) => void; - toJSON: () => { - app: FirebaseApp; - settings: FirestoreSettings; - }; - }; - type: string; // 'collection' or 'document' - path: string; - parent: DocumentReference | null; -} - -export interface QuerySnapshot { - docs: Array>; - size: number; - empty: boolean; -} - -export interface FirestoreSettings { - host?: string; - ssl?: boolean; - ignoreUndefinedProperties?: boolean; - cacheSizeBytes?: number; - experimentalForceLongPolling?: boolean; - experimentalAutoDetectLongPolling?: boolean; - useFetchStreams?: boolean; -} - -// Function types (addDoc, getDocs, setDoc, deleteDoc) are defined below as types -export type GetDocsType = ( - query: CollectionReference, -) => Promise>; - -export type SetDocType = (( - reference: DocumentReference, - data: WithFieldValue, -) => Promise) & - (( - reference: DocumentReference, - data: PartialWithFieldValue, - options: SetOptions, - ) => Promise); - -export type AddDocType = ( - reference: CollectionReference, - data: WithFieldValue, -) => Promise>; - -export type DeleteDocType = ( - reference: DocumentReference, -) => Promise; - -export type OverloadedParameters = T extends { - (...args: infer A1): unknown; - (...args: infer A2): unknown; -} - ? A1 | A2 - : T extends (...args: infer A) => unknown - ? A - : unknown; - -/** - * A bare minimum of how Cloud Functions for Firebase (v2) are defined. - */ -export type FirebaseFunctions = - | ((handler: () => Promise | unknown) => (...args: unknown[]) => Promise | unknown) - | (( - documentOrOptions: string | string[] | Record, - handler: () => Promise | unknown, - ) => (...args: unknown[]) => Promise | unknown); - -export type AvailableFirebaseFunctions = { - onRequest: FirebaseFunctions; - onCall: FirebaseFunctions; - onDocumentCreated: FirebaseFunctions; - onDocumentUpdated: FirebaseFunctions; - onDocumentDeleted: FirebaseFunctions; - onDocumentWritten: FirebaseFunctions; - onDocumentCreatedWithAuthContext: FirebaseFunctions; - onDocumentUpdatedWithAuthContext: FirebaseFunctions; - onDocumentDeletedWithAuthContext: FirebaseFunctions; - onDocumentWrittenWithAuthContext: FirebaseFunctions; - onSchedule: FirebaseFunctions; - onObjectFinalized: FirebaseFunctions; - onObjectArchived: FirebaseFunctions; - onObjectDeleted: FirebaseFunctions; - onObjectMetadataUpdated: FirebaseFunctions; -}; diff --git a/packages/node/src/integrations/tracing/genericPool/index.ts b/packages/node/src/integrations/tracing/genericPool/index.ts deleted file mode 100644 index 0d5090622ad1..000000000000 --- a/packages/node/src/integrations/tracing/genericPool/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { GenericPoolInstrumentation } from './vendored/instrumentation'; -import { generateInstrumentOnce } from '../../../otel/instrument'; - -const INTEGRATION_NAME = 'GenericPool' as const; - -export const instrumentGenericPool = generateInstrumentOnce(INTEGRATION_NAME, () => new GenericPoolInstrumentation({})); diff --git a/packages/node/src/integrations/tracing/genericPool/vendored/generic-pool-types.ts b/packages/node/src/integrations/tracing/genericPool/vendored/generic-pool-types.ts deleted file mode 100644 index 361d2081173e..000000000000 --- a/packages/node/src/integrations/tracing/genericPool/vendored/generic-pool-types.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Simplified types inlined from generic-pool. - */ - -export declare class Pool { - acquire(priority?: number): PromiseLike; - [key: string]: unknown; -} diff --git a/packages/node/src/integrations/tracing/genericPool/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/genericPool/vendored/instrumentation.ts deleted file mode 100644 index d9987d65f66d..000000000000 --- a/packages/node/src/integrations/tracing/genericPool/vendored/instrumentation.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-generic-pool - * - Upstream version: @opentelemetry/instrumentation-generic-pool@0.61.0 - * - Minor TypeScript strictness adjustments for this repository's compiler settings - */ - -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import { - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startSpan, - startSpanManual, -} from '@sentry/core'; -import type * as genericPool from './generic-pool-types'; - -const MODULE_NAME = 'generic-pool'; -const PACKAGE_NAME = '@sentry/instrumentation-generic-pool'; - -type AcquireFn = (this: unknown, ...args: unknown[]) => unknown; -interface PoolConstructor { - prototype: { acquire: AcquireFn }; -} -interface GenericPoolModule { - Pool: PoolConstructor; -} - -export class GenericPoolInstrumentation extends InstrumentationBase { - // only used for v2 - v2.3) - private _isDisabled = false; - - constructor(config: InstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - init() { - return [ - new InstrumentationNodeModuleDefinition( - MODULE_NAME, - ['>=3.0.0 <4'], - (moduleExports: GenericPoolModule) => { - const Pool = moduleExports.Pool; - if (isWrapped(Pool.prototype.acquire)) { - this._unwrap(Pool.prototype, 'acquire'); - } - this._wrap(Pool.prototype, 'acquire', this._acquirePatcher.bind(this)); - return moduleExports; - }, - (moduleExports: GenericPoolModule) => { - const Pool = moduleExports.Pool; - this._unwrap(Pool.prototype, 'acquire'); - return moduleExports; - }, - ), - new InstrumentationNodeModuleDefinition( - MODULE_NAME, - ['>=2.4.0 <3'], - (moduleExports: GenericPoolModule) => { - const Pool = moduleExports.Pool; - if (isWrapped(Pool.prototype.acquire)) { - this._unwrap(Pool.prototype, 'acquire'); - } - this._wrap(Pool.prototype, 'acquire', this._acquireWithCallbacksPatcher.bind(this)); - return moduleExports; - }, - (moduleExports: GenericPoolModule) => { - const Pool = moduleExports.Pool; - this._unwrap(Pool.prototype, 'acquire'); - return moduleExports; - }, - ), - new InstrumentationNodeModuleDefinition( - MODULE_NAME, - ['>=2.0.0 <2.4'], - (moduleExports: GenericPoolModule) => { - this._isDisabled = false; - if (isWrapped(moduleExports.Pool)) { - this._unwrap(moduleExports, 'Pool'); - } - this._wrap(moduleExports, 'Pool', this._poolWrapper.bind(this)); - return moduleExports; - }, - (moduleExports: GenericPoolModule) => { - // since the object is created on the fly every time, we need to use - // a boolean switch here to disable the instrumentation - this._isDisabled = true; - return moduleExports; - }, - ), - ]; - } - - private _acquirePatcher(original: AcquireFn) { - return function wrapped_acquire(this: genericPool.Pool, ...args: unknown[]) { - return startSpan( - { - name: 'generic-pool.acquire', - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.otel.generic_pool' }, - }, - () => { - return original.call(this, ...args) as PromiseLike; - }, - ); - }; - } - - private _poolWrapper(original: (this: unknown, ...args: unknown[]) => { acquire: AcquireFn }) { - const wrap = this._wrap.bind(this); - const acquireWithCallbacksPatcher = this._acquireWithCallbacksPatcher.bind(this); - return function wrapped_pool(this: unknown, ...args: unknown[]) { - const pool = original.apply(this, args); - wrap(pool, 'acquire', acquireWithCallbacksPatcher); - return pool; - }; - } - - private _acquireWithCallbacksPatcher(original: AcquireFn) { - const isDisabled = (): boolean => this._isDisabled; - return function wrapped_acquire( - this: genericPool.Pool, - cb: (err: unknown, client: unknown) => unknown, - priority: number, - ) { - // only used for v2 - v2.3 - if (isDisabled()) { - return original.call(this, cb, priority); - } - - return startSpanManual( - { - name: 'generic-pool.acquire', - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.otel.generic_pool' }, - }, - span => { - original.call( - this, - (err: unknown, client: unknown) => { - if (err) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - } - span.end(); - // Not checking whether cb is a function because - // the original code doesn't do that either. - // The callback's return value is unused by generic-pool, so we don't return it. - if (cb) { - cb(err, client); - } - }, - priority, - ); - }, - ); - }; - } -} diff --git a/packages/node/src/integrations/tracing/google-genai/index.ts b/packages/node/src/integrations/tracing/google-genai/index.ts deleted file mode 100644 index 3ac27c922db4..000000000000 --- a/packages/node/src/integrations/tracing/google-genai/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { GoogleGenAIOptions } from '@sentry/core'; -import { GOOGLE_GENAI_INTEGRATION_NAME } from '@sentry/core'; -import { generateInstrumentOnce } from '../../../otel/instrument'; -import { SentryGoogleGenAiInstrumentation } from './instrumentation'; - -export const instrumentGoogleGenAI = generateInstrumentOnce( - GOOGLE_GENAI_INTEGRATION_NAME, - options => new SentryGoogleGenAiInstrumentation(options), -); diff --git a/packages/node/src/integrations/tracing/google-genai/instrumentation.ts b/packages/node/src/integrations/tracing/google-genai/instrumentation.ts deleted file mode 100644 index 1de0e7418c42..000000000000 --- a/packages/node/src/integrations/tracing/google-genai/instrumentation.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { InstrumentationConfig, InstrumentationModuleDefinition } from '@opentelemetry/instrumentation'; -import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; -import { InstrumentationNodeModuleFile } from '../InstrumentationNodeModuleFile'; -import type { GoogleGenAIClient, GoogleGenAIOptions } from '@sentry/core'; -import { - _INTERNAL_shouldSkipAiProviderWrapping, - GOOGLE_GENAI_INTEGRATION_NAME, - instrumentGoogleGenAIClient, - replaceExports, - SDK_VERSION, -} from '@sentry/core'; - -const supportedVersions = ['>=0.10.0 <2']; - -/** - * Represents the patched shape of the Google GenAI module export. - */ -interface PatchedModuleExports { - [key: string]: unknown; - GoogleGenAI?: unknown; -} - -type GoogleGenAIInstrumentationOptions = GoogleGenAIOptions & InstrumentationConfig; - -/** - * Sentry Google GenAI instrumentation using OpenTelemetry. - */ -export class SentryGoogleGenAiInstrumentation extends InstrumentationBase { - public constructor(config: GoogleGenAIInstrumentationOptions = {}) { - super('@sentry/instrumentation-google-genai', SDK_VERSION, config); - } - - /** - * Initializes the instrumentation by defining the modules to be patched. - */ - public init(): InstrumentationModuleDefinition { - const module = new InstrumentationNodeModuleDefinition( - '@google/genai', - supportedVersions, - exports => this._patch(exports), - exports => exports, - // In CJS, @google/genai re-exports from (dist/node/index.cjs) file. - // Patching only the root module sometimes misses the real implementation or - // gets overwritten when that file is loaded. We add a file-level patch so that - // _patch runs again on the concrete implementation - [ - new InstrumentationNodeModuleFile( - '@google/genai/dist/node/index.cjs', - supportedVersions, - exports => this._patch(exports), - exports => exports, - ), - ], - ); - return module; - } - - /** - * Core patch logic applying instrumentation to the Google GenAI client constructor. - */ - private _patch(exports: PatchedModuleExports): PatchedModuleExports | void { - const Original = exports.GoogleGenAI; - const config = this.getConfig(); - - if (typeof Original !== 'function') { - return exports; - } - - const WrappedGoogleGenAI = function (this: unknown, ...args: unknown[]): GoogleGenAIClient { - // Check if wrapping should be skipped (e.g., when LangChain is handling instrumentation) - if (_INTERNAL_shouldSkipAiProviderWrapping(GOOGLE_GENAI_INTEGRATION_NAME)) { - return Reflect.construct(Original, args) as GoogleGenAIClient; - } - - const instance = Reflect.construct(Original, args); - - return instrumentGoogleGenAIClient(instance, config); - }; - - // Preserve static and prototype chains - Object.setPrototypeOf(WrappedGoogleGenAI, Original); - Object.setPrototypeOf(WrappedGoogleGenAI.prototype, Original.prototype); - - for (const key of Object.getOwnPropertyNames(Original)) { - if (!['length', 'name', 'prototype'].includes(key)) { - const descriptor = Object.getOwnPropertyDescriptor(Original, key); - if (descriptor) { - Object.defineProperty(WrappedGoogleGenAI, key, descriptor); - } - } - } - - // Replace google genai exports with the wrapped constructor - replaceExports(exports, 'GoogleGenAI', WrappedGoogleGenAI); - - return exports; - } -} diff --git a/packages/node/src/integrations/tracing/graphql/index.ts b/packages/node/src/integrations/tracing/graphql/index.ts deleted file mode 100644 index 95c034e07ca0..000000000000 --- a/packages/node/src/integrations/tracing/graphql/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { GraphQLInstrumentation } from './vendored/instrumentation'; -import { generateInstrumentOnce } from '../../../otel/instrument'; - -interface GraphqlOptions { - /** - * Do not create spans for resolvers. - * - * Defaults to true. - */ - ignoreResolveSpans?: boolean; - - /** - * Don't create spans for the execution of the default resolver on object properties. - * - * When a resolver function is not defined on the schema for a field, graphql will - * use the default resolver which just looks for a property with that name on the object. - * If the property is not a function, it's not very interesting to trace. - * This option can reduce noise and number of spans created. - * - * Defaults to true. - */ - ignoreTrivialResolveSpans?: boolean; - - /** - * If this is enabled, a http.server root span containing this span will automatically be renamed to include the operation name. - * Set this to `false` if you do not want this behavior, and want to keep the default http.server span name. - * - * Defaults to true. - */ - useOperationNameForRootSpan?: boolean; -} - -const INTEGRATION_NAME = 'Graphql' as const; - -export const instrumentGraphql = generateInstrumentOnce( - INTEGRATION_NAME, - GraphQLInstrumentation, - (_options: GraphqlOptions) => getOptionsWithDefaults(_options), -); - -function getOptionsWithDefaults(options?: GraphqlOptions): GraphqlOptions { - return { - ignoreResolveSpans: true, - ignoreTrivialResolveSpans: true, - useOperationNameForRootSpan: true, - ...options, - }; -} diff --git a/packages/node/src/integrations/tracing/graphql/vendored/enum.ts b/packages/node/src/integrations/tracing/graphql/vendored/enum.ts deleted file mode 100644 index 2e9fc3468168..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/enum.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-graphql - * - Upstream version: @opentelemetry/instrumentation-graphql@0.66.0 - */ - -export enum AllowedOperationTypes { - QUERY = 'query', - MUTATION = 'mutation', - SUBSCRIPTION = 'subscription', -} - -export enum TokenKind { - SOF = '', - EOF = '', - BANG = '!', - DOLLAR = '$', - AMP = '&', - PAREN_L = '(', - PAREN_R = ')', - SPREAD = '...', - COLON = ':', - EQUALS = '=', - AT = '@', - BRACKET_L = '[', - BRACKET_R = ']', - BRACE_L = '{', - PIPE = '|', - BRACE_R = '}', - NAME = 'Name', - INT = 'Int', - FLOAT = 'Float', - STRING = 'String', - BLOCK_STRING = 'BlockString', - COMMENT = 'Comment', -} - -export enum SpanNames { - EXECUTE = 'graphql.execute', - PARSE = 'graphql.parse', - RESOLVE = 'graphql.resolve', - VALIDATE = 'graphql.validate', - SCHEMA_VALIDATE = 'graphql.validateSchema', - SCHEMA_PARSE = 'graphql.parseSchema', -} diff --git a/packages/node/src/integrations/tracing/graphql/vendored/enums/AttributeNames.ts b/packages/node/src/integrations/tracing/graphql/vendored/enums/AttributeNames.ts deleted file mode 100644 index 19fbe9964748..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/enums/AttributeNames.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-graphql - * - Upstream version: @opentelemetry/instrumentation-graphql@0.66.0 - */ - -export enum AttributeNames { - SOURCE = 'graphql.source', - FIELD_NAME = 'graphql.field.name', - FIELD_PATH = 'graphql.field.path', - FIELD_TYPE = 'graphql.field.type', - PARENT_NAME = 'graphql.parent.name', - OPERATION_TYPE = 'graphql.operation.type', - OPERATION_NAME = 'graphql.operation.name', -} diff --git a/packages/node/src/integrations/tracing/graphql/vendored/graphql-types.ts b/packages/node/src/integrations/tracing/graphql/vendored/graphql-types.ts deleted file mode 100644 index e0a2b72e64f0..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/graphql-types.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Single source of truth for the structural graphql types, shared with the orchestrion graphql -// integration in `@sentry/server-utils` so the two can't drift. -export type { - DefinitionNode, - DocumentNode, - ExecutionArgs, - ExecutionResult, - GraphQLError, - GraphQLFieldResolver, - GraphQLObjectType, - GraphQLOutputType, - GraphQLResolveInfo, - GraphQLSchema, - GraphQLType, - GraphQLTypeResolver, - GraphQLUnionType, - Location, - Maybe, - OperationDefinitionNode, - ParseOptions, - PromiseOrValue, - Source, - Token, - TypeInfo, - ValidationRule, -} from '@sentry/server-utils/orchestrion'; diff --git a/packages/node/src/integrations/tracing/graphql/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/graphql/vendored/instrumentation.ts deleted file mode 100644 index e7e3a99b8b8e..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/instrumentation.ts +++ /dev/null @@ -1,478 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-graphql - * - Upstream version: @opentelemetry/instrumentation-graphql@0.66.0 - * - Types from `graphql` package inlined as simplified interfaces - * - Minor TypeScript strictness adjustments - * - Span lifecycle migrated from the OpenTelemetry tracer to the @sentry/core span API - * - `auto.graphql.otel.graphql` origin baked into the execute span (previously set via a Sentry responseHook) - * - The generic `responseHook` config was removed; its Sentry-specific logic (error status + root span renaming - * via `useOperationNameForRootSpan`) is now applied directly when the execution result is handled - */ - -/* oxlint-disable max-lines */ - -import { - isWrapped, - InstrumentationBase, - InstrumentationNodeModuleDefinition, - safeExecuteInTheMiddle, -} from '@opentelemetry/instrumentation'; -import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile'; -import type { - DefinitionNode, - DocumentNode, - ExecutionArgs, - ExecutionResult, - GraphQLError, - GraphQLFieldResolver, - GraphQLSchema, - GraphQLTypeResolver, - OperationDefinitionNode, - ParseOptions, - PromiseOrValue, - Source, - TypeInfo, - ValidationRule, -} from './graphql-types'; -import type { Maybe } from './graphql-types'; -import { SpanNames } from './enum'; -import { AttributeNames } from './enums/AttributeNames'; -import { OTEL_GRAPHQL_DATA_SYMBOL } from './symbols'; - -import { - type executeFunctionWithObj, - type executeArgumentsArray, - type executeType, - type parseType, - type validateType, - type OtelExecutionArgs, - type ObjectWithGraphQLData, - OPERATION_NOT_SUPPORTED, -} from './internal-types'; -import { addSpanSource, endSpan, getOperation, isPromise, wrapFieldResolver, wrapFields } from './utils'; - -import type { Span, SpanAttributeValue } from '@sentry/core'; -import { - getRootSpan, - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - spanToJSON, - startInactiveSpan, - withActiveSpan, -} from '@sentry/core'; - -import type { GraphQLInstrumentationConfig, GraphQLInstrumentationParsedConfig } from './types'; -import { SENTRY_GRAPHQL_OPERATION } from '@sentry/conventions/attributes'; - -const PACKAGE_NAME = '@sentry/instrumentation-graphql'; - -const ORIGIN = 'auto.graphql.otel.graphql'; - -const DEFAULT_CONFIG: GraphQLInstrumentationParsedConfig = { - ignoreResolveSpans: false, -}; - -const supportedVersions = ['>=14.0.0 <17']; - -export class GraphQLInstrumentation extends InstrumentationBase { - constructor(config: GraphQLInstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, { ...DEFAULT_CONFIG, ...config }); - } - - override setConfig(config: GraphQLInstrumentationConfig = {}) { - super.setConfig({ ...DEFAULT_CONFIG, ...config }); - } - - protected init() { - const module = new InstrumentationNodeModuleDefinition('graphql', supportedVersions); - module.files.push(this._addPatchingExecute()); - module.files.push(this._addPatchingParser()); - module.files.push(this._addPatchingValidate()); - - return module; - } - - private _addPatchingExecute(): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - 'graphql/execution/execute.js', - supportedVersions, - // cannot make it work with appropriate type as execute function has 2 - //types and/cannot import function but only types - (moduleExports: any) => { - if (isWrapped(moduleExports.execute)) { - this._unwrap(moduleExports, 'execute'); - } - this._wrap(moduleExports, 'execute', this._patchExecute(moduleExports.defaultFieldResolver)); - return moduleExports; - }, - moduleExports => { - if (moduleExports) { - this._unwrap(moduleExports, 'execute'); - } - }, - ); - } - - private _addPatchingParser(): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - 'graphql/language/parser.js', - supportedVersions, - (moduleExports: { parse: parseType; [key: string]: any }) => { - if (isWrapped(moduleExports.parse)) { - this._unwrap(moduleExports, 'parse'); - } - this._wrap(moduleExports, 'parse', this._patchParse()); - return moduleExports; - }, - (moduleExports: { parse: parseType; [key: string]: any }) => { - if (moduleExports) { - this._unwrap(moduleExports, 'parse'); - } - }, - ); - } - - private _addPatchingValidate(): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - 'graphql/validation/validate.js', - supportedVersions, - moduleExports => { - if (isWrapped(moduleExports.validate)) { - this._unwrap(moduleExports, 'validate'); - } - this._wrap(moduleExports, 'validate', this._patchValidate()); - return moduleExports; - }, - moduleExports => { - if (moduleExports) { - this._unwrap(moduleExports, 'validate'); - } - }, - ); - } - - private _patchExecute(defaultFieldResolved: GraphQLFieldResolver): (original: executeType) => executeType { - const instrumentation = this; - return function execute(original) { - return function patchExecute(this: executeType): PromiseOrValue { - let processedArgs: OtelExecutionArgs; - - // case when apollo server is used for example - if (arguments.length >= 2) { - const args = arguments as unknown as executeArgumentsArray; - processedArgs = instrumentation._wrapExecuteArgs( - args[0], - args[1], - args[2], - args[3], - args[4], - args[5], - args[6], - args[7], - defaultFieldResolved, - ); - } else { - const args = arguments[0] as ExecutionArgs; - processedArgs = instrumentation._wrapExecuteArgs( - args.schema, - args.document, - args.rootValue, - args.contextValue, - args.variableValues, - args.operationName, - args.fieldResolver, - args.typeResolver, - defaultFieldResolved, - ); - } - - const operation = getOperation(processedArgs.document, processedArgs.operationName); - - const span = instrumentation._createExecuteSpan(operation, processedArgs); - - processedArgs.contextValue[OTEL_GRAPHQL_DATA_SYMBOL] = { - source: processedArgs.document - ? processedArgs.document || (processedArgs.document as ObjectWithGraphQLData)[OTEL_GRAPHQL_DATA_SYMBOL] - : undefined, - span, - fields: {}, - }; - - return withActiveSpan(span, () => { - return safeExecuteInTheMiddle>( - () => { - return (original as executeFunctionWithObj).apply(this, [processedArgs]); - }, - (err, result) => { - instrumentation._handleExecutionResult(span, err, result); - }, - ); - }); - }; - }; - } - - private _handleExecutionResult(span: Span, err?: Error, result?: PromiseOrValue) { - if (result === undefined || err) { - endSpan(span, err); - return; - } - - if (isPromise(result)) { - result.then( - resultData => { - this._updateSpanFromResult(span, resultData); - endSpan(span); - }, - error => { - endSpan(span, error); - }, - ); - } else { - this._updateSpanFromResult(span, result); - endSpan(span); - } - } - - /** - * Applies Sentry-specific span mutations based on the GraphQL execution result: - * - Marks the execute span as errored if the result contains errors (and no status was set yet) - * - Optionally renames the containing root span to include the GraphQL operation name(s) - */ - private _updateSpanFromResult(span: Span, result: ExecutionResult): void { - // We want to ensure spans are marked as errored if there are errors in the result - // We only do that if the span is not already marked with another error status - if (result.errors?.length && spanToJSON(span).status === 'ok') { - span.setStatus({ code: SPAN_STATUS_ERROR }); - } - - if (!this.getConfig().useOperationNameForRootSpan) { - return; - } - - const attributes = spanToJSON(span).data; - - // If operation.name is not set, we fall back to use operation.type only - const operationType = attributes[AttributeNames.OPERATION_TYPE]; - const operationName = attributes[AttributeNames.OPERATION_NAME]; - - if (!operationType) { - return; - } - - const rootSpan = getRootSpan(span); - const rootSpanAttributes = spanToJSON(rootSpan).data; - - const existingOperations = rootSpanAttributes[SENTRY_GRAPHQL_OPERATION] || []; - - const newOperation = operationName ? `${operationType} ${operationName}` : `${operationType}`; - - // We keep track of each operation on the root span - // This can either be a string, or an array of strings (if there are multiple operations) - if (Array.isArray(existingOperations)) { - (existingOperations as string[]).push(newOperation); - rootSpan.setAttribute(SENTRY_GRAPHQL_OPERATION, existingOperations); - } else if (typeof existingOperations === 'string') { - rootSpan.setAttribute(SENTRY_GRAPHQL_OPERATION, [existingOperations, newOperation]); - } else { - rootSpan.setAttribute(SENTRY_GRAPHQL_OPERATION, newOperation); - } - - if (!spanToJSON(rootSpan).data['original-description']) { - rootSpan.setAttribute('original-description', spanToJSON(rootSpan).description); - } - // Important for e.g. @sentry/aws-serverless because this would otherwise overwrite the name again - rootSpan.updateName( - `${spanToJSON(rootSpan).data['original-description']} (${getGraphqlOperationNamesFromAttribute( - existingOperations, - )})`, - ); - } - - private _patchParse(): (original: parseType) => parseType { - const instrumentation = this; - return function parse(original) { - return function patchParse(this: parseType, source: string | Source, options?: ParseOptions): DocumentNode { - return instrumentation._parse(this, original, source, options); - }; - }; - } - - private _patchValidate(): (original: validateType) => validateType { - const instrumentation = this; - return function validate(original: validateType) { - return function patchValidate( - this: validateType, - schema: GraphQLSchema, - documentAST: DocumentNode, - rules?: ReadonlyArray, - options?: { maxErrors?: number }, - typeInfo?: TypeInfo, - ): ReadonlyArray { - return instrumentation._validate(this, original, schema, documentAST, rules, typeInfo, options); - }; - }; - } - - private _parse(obj: parseType, original: parseType, source: string | Source, options?: ParseOptions): DocumentNode { - const span = startInactiveSpan({ name: SpanNames.PARSE }); - - return withActiveSpan(span, () => { - return safeExecuteInTheMiddle( - () => { - return original.call(obj, source, options); - }, - (err, result) => { - if (result) { - const operation = getOperation(result); - if (!operation) { - span.updateName(SpanNames.SCHEMA_PARSE); - } else if (result.loc) { - addSpanSource(span, result.loc); - } - } - endSpan(span, err); - }, - ); - }); - } - - private _validate( - obj: validateType, - original: validateType, - schema: GraphQLSchema, - documentAST: DocumentNode, - rules?: ReadonlyArray, - typeInfo?: TypeInfo, - options?: { maxErrors?: number }, - ): ReadonlyArray { - const span = startInactiveSpan({ name: SpanNames.VALIDATE }); - - return withActiveSpan(span, () => { - return safeExecuteInTheMiddle>( - () => { - return original.call(obj, schema, documentAST, rules, options, typeInfo); - }, - (err, _errors) => { - if (!documentAST.loc) { - span.updateName(SpanNames.SCHEMA_VALIDATE); - } - endSpan(span, err); - }, - ); - }); - } - - private _createExecuteSpan(operation: DefinitionNode | undefined, processedArgs: OtelExecutionArgs): Span { - const span = startInactiveSpan({ - name: SpanNames.EXECUTE, - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN }, - }); - if (operation) { - const { operation: operationType, name: nameNode } = operation as OperationDefinitionNode; - - span.setAttribute(AttributeNames.OPERATION_TYPE, operationType); - - const operationName = nameNode?.value; - - // https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/instrumentation/graphql/ - // > The span name MUST be of the format provided that graphql.operation.type and graphql.operation.name are available. - // > If graphql.operation.name is not available, the span SHOULD be named . - if (operationName) { - span.setAttribute(AttributeNames.OPERATION_NAME, operationName); - span.updateName(`${operationType} ${operationName}`); - } else { - span.updateName(operationType); - } - } else { - let operationName = ' '; - if (processedArgs.operationName) { - operationName = ` "${processedArgs.operationName}" `; - } - operationName = OPERATION_NOT_SUPPORTED.replace('$operationName$', operationName); - span.setAttribute(AttributeNames.OPERATION_NAME, operationName); - } - - if (processedArgs.document?.loc) { - addSpanSource(span, processedArgs.document.loc); - } - - return span; - } - - private _wrapExecuteArgs( - schema: GraphQLSchema, - document: DocumentNode, - rootValue: any, - contextValue: any, - variableValues: Maybe<{ [key: string]: any }>, - operationName: Maybe, - fieldResolver: Maybe>, - typeResolver: Maybe>, - defaultFieldResolved: GraphQLFieldResolver, - ): OtelExecutionArgs { - if (!contextValue) { - // oxlint-disable-next-line no-param-reassign - contextValue = {}; - } - - if (contextValue[OTEL_GRAPHQL_DATA_SYMBOL] || this.getConfig().ignoreResolveSpans) { - return { - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - typeResolver, - }; - } - - const isUsingDefaultResolver = fieldResolver == null; - // follows graphql implementation here: - // https://github.com/graphql/graphql-js/blob/0b7daed9811731362c71900e12e5ea0d1ecc7f1f/src/execution/execute.ts#L494 - const fieldResolverForExecute = fieldResolver ?? defaultFieldResolved; - // oxlint-disable-next-line no-param-reassign - fieldResolver = wrapFieldResolver(() => this.getConfig(), fieldResolverForExecute, isUsingDefaultResolver); - - if (schema) { - wrapFields(schema.getQueryType() as any, () => this.getConfig()); - wrapFields(schema.getMutationType() as any, () => this.getConfig()); - } - - return { - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - typeResolver, - }; - } -} - -// copy from packages/opentelemetry/utils -function getGraphqlOperationNamesFromAttribute(attr: SpanAttributeValue): string { - if (Array.isArray(attr)) { - // oxlint-disable-next-line typescript/require-array-sort-compare - const sorted = attr.slice().sort(); - - // Up to 5 items, we just add all of them - if (sorted.length <= 5) { - return sorted.join(', '); - } else { - // Else, we add the first 5 and the diff of other operations - return `${sorted.slice(0, 5).join(', ')}, +${sorted.length - 5}`; - } - } - - return `${attr}`; -} diff --git a/packages/node/src/integrations/tracing/graphql/vendored/internal-types.ts b/packages/node/src/integrations/tracing/graphql/vendored/internal-types.ts deleted file mode 100644 index e206200e3ffb..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/internal-types.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-graphql - * - Upstream version: @opentelemetry/instrumentation-graphql@0.66.0 - * - Types from `graphql` package inlined as simplified interfaces - * - Minor TypeScript strictness adjustments - */ - -import type { Span } from '@sentry/core'; -import type { - DocumentNode, - ExecutionArgs, - ExecutionResult, - GraphQLError, - GraphQLFieldResolver, - GraphQLSchema, - GraphQLTypeResolver, - Maybe, - ParseOptions, - PromiseOrValue, - Source, - TypeInfo, - ValidationRule, -} from './graphql-types'; -import type { OTEL_GRAPHQL_DATA_SYMBOL, OTEL_PATCHED_SYMBOL } from './symbols'; - -export type { Maybe } from './graphql-types'; - -export const OPERATION_NOT_SUPPORTED = 'Operation$operationName$not' + ' supported'; - -export type executeFunctionWithObj = (args: ExecutionArgs) => PromiseOrValue; - -export type executeArgumentsArray = [ - GraphQLSchema, - DocumentNode, - any, - any, - Maybe<{ [key: string]: any }>, - Maybe, - Maybe>, - Maybe>, -]; - -export type executeFunctionWithArgs = ( - schema: GraphQLSchema, - document: DocumentNode, - rootValue?: any, - contextValue?: any, - variableValues?: Maybe<{ [key: string]: any }>, - operationName?: Maybe, - fieldResolver?: Maybe>, - typeResolver?: Maybe>, -) => PromiseOrValue; - -export interface OtelExecutionArgs { - schema: GraphQLSchema; - document: DocumentNode & ObjectWithGraphQLData; - rootValue?: any; - contextValue?: any & ObjectWithGraphQLData; - variableValues?: Maybe<{ [key: string]: any }>; - operationName?: Maybe; - fieldResolver?: Maybe & OtelPatched>; - typeResolver?: Maybe>; -} - -export type executeType = executeFunctionWithObj | executeFunctionWithArgs; - -export type parseType = (source: string | Source, options?: ParseOptions) => DocumentNode; - -export type validateType = ( - schema: GraphQLSchema, - documentAST: DocumentNode, - rules?: ReadonlyArray, - options?: { maxErrors?: number }, - typeInfo?: TypeInfo, -) => ReadonlyArray; - -export interface GraphQLField { - span: Span; -} - -interface OtelGraphQLData { - source?: any; - span: Span; - fields: { [key: string]: GraphQLField }; -} - -export interface ObjectWithGraphQLData { - [OTEL_GRAPHQL_DATA_SYMBOL]?: OtelGraphQLData; -} - -export interface OtelPatched { - [OTEL_PATCHED_SYMBOL]?: boolean; -} - -export interface GraphQLPath { - prev: GraphQLPath | undefined; - key: string | number; - /** - * optional as it didn't exist yet in ver 14 - */ - typename?: string | undefined; -} diff --git a/packages/node/src/integrations/tracing/graphql/vendored/symbols.ts b/packages/node/src/integrations/tracing/graphql/vendored/symbols.ts deleted file mode 100644 index 064363ac48ba..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/symbols.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-graphql - * - Upstream version: @opentelemetry/instrumentation-graphql@0.66.0 - */ - -export const OTEL_PATCHED_SYMBOL = Symbol.for('opentelemetry.patched'); - -export const OTEL_GRAPHQL_DATA_SYMBOL = Symbol.for('opentelemetry.graphql_data'); diff --git a/packages/node/src/integrations/tracing/graphql/vendored/types.ts b/packages/node/src/integrations/tracing/graphql/vendored/types.ts deleted file mode 100644 index cbae5ba771f0..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/types.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-graphql - * - Upstream version: @opentelemetry/instrumentation-graphql@0.66.0 - */ - -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; - -export interface GraphQLInstrumentationConfig extends InstrumentationConfig { - /** - * Do not create spans for resolvers. - * - * @default false - */ - ignoreResolveSpans?: boolean; - - /** - * Don't create spans for the execution of the default resolver on object properties. - * - * When a resolver function is not defined on the schema for a field, graphql will - * use the default resolver which just looks for a property with that name on the object. - * If the property is not a function, it's not very interesting to trace. - * This option can reduce noise and number of spans created. - * - * @default false - */ - ignoreTrivialResolveSpans?: boolean; - - /** - * If this is enabled, a `http.server` root span containing the execute span will automatically be renamed - * to include the operation name. - * - * @default false - */ - useOperationNameForRootSpan?: boolean; -} - -// Utility type to make specific properties required -type RequireSpecificKeys = T & { [P in K]-?: T[P] }; - -// Merged and parsed config of default instrumentation config and GraphQL -export type GraphQLInstrumentationParsedConfig = RequireSpecificKeys< - GraphQLInstrumentationConfig, - 'ignoreResolveSpans' ->; diff --git a/packages/node/src/integrations/tracing/graphql/vendored/utils.ts b/packages/node/src/integrations/tracing/graphql/vendored/utils.ts deleted file mode 100644 index ce3562e67d35..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/utils.ts +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-graphql - * - Upstream version: @opentelemetry/instrumentation-graphql@0.66.0 - * - Types from `graphql` package inlined as simplified interfaces - * - Minor TypeScript strictness adjustments - * - Span lifecycle migrated from the OpenTelemetry tracer to the @sentry/core span API - */ - -import type { - DocumentNode, - GraphQLFieldResolver, - GraphQLObjectType, - GraphQLOutputType, - GraphQLResolveInfo, - GraphQLType, - GraphQLUnionType, - Location, - Maybe, - Token, -} from './graphql-types'; -import type { Span, SpanAttributes } from '@sentry/core'; -import { getClient, isObjectLike, SPAN_STATUS_ERROR, startInactiveSpan, withActiveSpan } from '@sentry/core'; -import { AllowedOperationTypes, SpanNames, TokenKind } from './enum'; -import { AttributeNames } from './enums/AttributeNames'; -import { OTEL_GRAPHQL_DATA_SYMBOL, OTEL_PATCHED_SYMBOL } from './symbols'; -import type { GraphQLField, GraphQLPath, ObjectWithGraphQLData, OtelPatched } from './internal-types'; -import type { GraphQLInstrumentationParsedConfig } from './types'; - -const OPERATION_VALUES = Object.values(AllowedOperationTypes); - -// https://github.com/graphql/graphql-js/blob/main/src/jsutils/isPromise.ts -export const isPromise = (value: any): value is Promise => { - return typeof value?.then === 'function'; -}; - -export function addSpanSource(span: Span, loc?: Location, start?: number, end?: number): void { - if (getClient()?.getDataCollectionOptions().graphQL.document === true) { - const source = getSourceFromLocation(loc, start, end); - span.setAttribute(AttributeNames.SOURCE, source); - } -} - -function createFieldIfNotExists( - contextValue: any, - info: GraphQLResolveInfo, - path: string[], -): { - field: GraphQLField; - spanAdded: boolean; -} { - let field = getField(contextValue, path); - if (field) { - return { field, spanAdded: false }; - } - - const parentSpan = getParentFieldSpan(contextValue, path); - - field = { - span: createResolverSpan(contextValue, info, path, parentSpan), - }; - - addField(contextValue, path, field); - - return { field, spanAdded: true }; -} - -function createResolverSpan(contextValue: any, info: GraphQLResolveInfo, path: string[], parentSpan?: Span): Span { - const attributes: SpanAttributes = { - [AttributeNames.FIELD_NAME]: info.fieldName, - [AttributeNames.FIELD_PATH]: path.join('.'), - [AttributeNames.FIELD_TYPE]: info.returnType.toString(), - [AttributeNames.PARENT_NAME]: info.parentType.name, - }; - - const span = startInactiveSpan({ - name: `${SpanNames.RESOLVE} ${attributes[AttributeNames.FIELD_PATH]}`, - attributes, - parentSpan, - }); - - const document = contextValue[OTEL_GRAPHQL_DATA_SYMBOL].source; - const fieldNode = info.fieldNodes.find(fieldNode => fieldNode.kind === 'Field'); - - if (fieldNode) { - addSpanSource(span, document.loc, fieldNode.loc?.start, fieldNode.loc?.end); - } - - return span; -} - -export function endSpan(span: Span, error?: Error): void { - if (error) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: error.message }); - } - span.end(); -} - -export function getOperation(document: DocumentNode, operationName?: Maybe): DefinitionNodeLike | undefined { - if (!document || !Array.isArray(document.definitions)) { - return undefined; - } - - if (operationName) { - return document.definitions - .filter(definition => OPERATION_VALUES.indexOf(definition?.operation) !== -1) - .find(definition => operationName === definition?.name?.value); - } else { - return document.definitions.find(definition => OPERATION_VALUES.indexOf(definition?.operation) !== -1); - } -} - -type DefinitionNodeLike = DocumentNode['definitions'][number]; - -function addField(contextValue: any, path: string[], field: GraphQLField) { - return (contextValue[OTEL_GRAPHQL_DATA_SYMBOL].fields[path.join('.')] = field); -} - -function getField(contextValue: any, path: string[]): GraphQLField { - return contextValue[OTEL_GRAPHQL_DATA_SYMBOL].fields[path.join('.')]; -} - -function getParentFieldSpan(contextValue: any, path: string[]): Span { - for (let i = path.length - 1; i > 0; i--) { - const field = getField(contextValue, path.slice(0, i)); - - if (field) { - return field.span; - } - } - - return getRootSpan(contextValue); -} - -function getRootSpan(contextValue: any): Span { - return contextValue[OTEL_GRAPHQL_DATA_SYMBOL].span; -} - -function pathToArray(path: GraphQLPath): string[] { - const flattened: string[] = []; - let curr: GraphQLPath | undefined = path; - while (curr) { - flattened.push(String(curr.key)); - curr = curr.prev; - } - return flattened.reverse(); -} - -function repeatBreak(i: number): string { - return repeatChar('\n', i); -} - -function repeatSpace(i: number): string { - return repeatChar(' ', i); -} - -function repeatChar(char: string, to: number): string { - let text = ''; - for (let i = 0; i < to; i++) { - text += char; - } - return text; -} - -const KindsToBeRemoved: string[] = [TokenKind.FLOAT, TokenKind.STRING, TokenKind.INT, TokenKind.BLOCK_STRING]; - -export function getSourceFromLocation(loc?: Location, inputStart?: number, inputEnd?: number): string { - let source = ''; - - if (loc?.startToken) { - const start = typeof inputStart === 'number' ? inputStart : loc.start; - const end = typeof inputEnd === 'number' ? inputEnd : loc.end; - - let next: Token | null = loc.startToken.next; - let previousLine: number | undefined = 1; - while (next) { - if (next.start < start) { - next = next.next; - previousLine = next?.line; - continue; - } - if (next.end > end) { - next = next.next; - previousLine = next?.line; - continue; - } - let value = next.value || next.kind; - let space = ''; - if (KindsToBeRemoved.indexOf(next.kind) >= 0) { - value = '*'; - } - if (next.kind === TokenKind.STRING) { - value = `"${value}"`; - } - if (next.kind === TokenKind.EOF) { - value = ''; - } - if (next.line > previousLine!) { - source += repeatBreak(next.line - previousLine!); - previousLine = next.line; - space = repeatSpace(next.column - 1); - } else { - if (next.line === next.prev?.line) { - space = repeatSpace(next.start - (next.prev?.end || 0)); - } - } - source += space + value; - if (next) { - next = next.next!; - } - } - } - - return source; -} - -export function wrapFields( - type: Maybe, - getConfig: () => GraphQLInstrumentationParsedConfig, -): void { - if (!type || (type as any)[OTEL_PATCHED_SYMBOL]) { - return; - } - const fields = type.getFields(); - - (type as any)[OTEL_PATCHED_SYMBOL] = true; - - Object.keys(fields).forEach(key => { - const field = fields[key]; - - if (!field) { - return; - } - - if (field.resolve) { - // The shared structural types narrow the resolver context to `ObjectWithGraphQLData`; cast back - // to the field's own resolver type (behavior is unchanged — this is a type-only adjustment). - field.resolve = wrapFieldResolver(getConfig, field.resolve) as GraphQLFieldResolver; - } - - if (field.type) { - const unwrappedTypes = unwrapType(field.type); - for (const unwrappedType of unwrappedTypes) { - wrapFields(unwrappedType as any, getConfig); - } - } - }); -} - -function unwrapType(type: GraphQLOutputType): readonly GraphQLObjectType[] { - // unwrap wrapping types (non-nullable and list types) - if ('ofType' in type) { - // The structural index signature widens `ofType` to `unknown`, so narrow it back explicitly. - return unwrapType(type.ofType as GraphQLOutputType); - } - - // unwrap union types - if (isGraphQLUnionType(type)) { - return type.getTypes(); - } - - // return object types - if (isGraphQLObjectType(type)) { - return [type]; - } - - return []; -} - -function isGraphQLUnionType(type: GraphQLType): type is GraphQLUnionType { - return 'getTypes' in type && typeof type.getTypes === 'function'; -} - -function isGraphQLObjectType(type: GraphQLType): type is GraphQLObjectType { - return 'getFields' in type && typeof type.getFields === 'function'; -} - -const handleResolveSpanError = (resolveSpan: Span, err: any, shouldEndSpan: boolean) => { - if (!shouldEndSpan) { - return; - } - resolveSpan.setStatus({ - code: SPAN_STATUS_ERROR, - message: err.message, - }); - resolveSpan.end(); -}; - -const handleResolveSpanSuccess = (resolveSpan: Span, shouldEndSpan: boolean) => { - if (!shouldEndSpan) { - return; - } - resolveSpan.end(); -}; - -export function wrapFieldResolver( - getConfig: () => GraphQLInstrumentationParsedConfig, - fieldResolver: Maybe & OtelPatched>, - isDefaultResolver = false, -): GraphQLFieldResolver & OtelPatched { - if ((wrappedFieldResolver as OtelPatched)[OTEL_PATCHED_SYMBOL] || typeof fieldResolver !== 'function') { - return fieldResolver!; - } - - function wrappedFieldResolver( - this: GraphQLFieldResolver, - source: TSource, - args: TArgs, - contextValue: TContext & ObjectWithGraphQLData, - info: GraphQLResolveInfo, - ) { - if (!fieldResolver) { - return undefined; - } - const config = getConfig(); - - // follows what graphql is doing to decide if this is a trivial resolver - // for which we don't need to create a resolve span - if ( - config.ignoreTrivialResolveSpans && - isDefaultResolver && - (isObjectLike(source) || typeof source === 'function') - ) { - const property = (source as any)[info.fieldName]; - // a function execution is not trivial and should be recorder. - // property which is not a function is just a value and we don't want a "resolve" span for it - if (typeof property !== 'function') { - return fieldResolver.call(this, source, args, contextValue, info); - } - } - - if (!contextValue[OTEL_GRAPHQL_DATA_SYMBOL]) { - return fieldResolver.call(this, source, args, contextValue, info); - } - const path = pathToArray(info?.path); - - const { field, spanAdded } = createFieldIfNotExists(contextValue, info, path); - const span = field.span; - const shouldEndSpan = spanAdded; - - return withActiveSpan(span, () => { - try { - const res = fieldResolver.call(this, source, args, contextValue, info); - if (isPromise(res)) { - return res.then( - (r: any) => { - handleResolveSpanSuccess(span, shouldEndSpan); - return r; - }, - (err: Error) => { - handleResolveSpanError(span, err, shouldEndSpan); - throw err; - }, - ); - } else { - handleResolveSpanSuccess(span, shouldEndSpan); - return res; - } - } catch (err: any) { - handleResolveSpanError(span, err, shouldEndSpan); - throw err; - } - }); - } - - (wrappedFieldResolver as OtelPatched)[OTEL_PATCHED_SYMBOL] = true; - - return wrappedFieldResolver; -} diff --git a/packages/node/src/integrations/tracing/hapi/index.ts b/packages/node/src/integrations/tracing/hapi/index.ts index 280af9b4dba7..214a70ee74c9 100644 --- a/packages/node/src/integrations/tracing/hapi/index.ts +++ b/packages/node/src/integrations/tracing/hapi/index.ts @@ -1,14 +1,8 @@ -import { HapiInstrumentation } from './vendored/instrumentation'; import { captureException, debug, getDefaultIsolationScope, getIsolationScope, SDK_VERSION } from '@sentry/core'; -import { generateInstrumentOnce } from '../../../otel/instrument'; import { ensureIsWrapped } from '../../../utils/ensureIsWrapped'; import { DEBUG_BUILD } from '../../../debug-build'; import type { Request, RequestEvent, Server } from './types'; -const INTEGRATION_NAME = 'Hapi' as const; - -export const instrumentHapi = generateInstrumentOnce(INTEGRATION_NAME, () => new HapiInstrumentation()); - function isErrorEvent(event: unknown): event is RequestEvent { return !!(event && typeof event === 'object' && 'error' in event && event.error); } diff --git a/packages/node/src/integrations/tracing/hapi/vendored/enums/AttributeNames.ts b/packages/node/src/integrations/tracing/hapi/vendored/enums/AttributeNames.ts deleted file mode 100644 index 9384d73ad08d..000000000000 --- a/packages/node/src/integrations/tracing/hapi/vendored/enums/AttributeNames.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-hapi - * - Upstream version: @opentelemetry/instrumentation-hapi@0.64.0 - */ -/* eslint-disable */ - -export enum AttributeNames { - HAPI_TYPE = 'hapi.type', - PLUGIN_NAME = 'hapi.plugin.name', - EXT_TYPE = 'server.ext.type', -} diff --git a/packages/node/src/integrations/tracing/hapi/vendored/hapi-types.ts b/packages/node/src/integrations/tracing/hapi/vendored/hapi-types.ts deleted file mode 100644 index 1c1419285fd0..000000000000 --- a/packages/node/src/integrations/tracing/hapi/vendored/hapi-types.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Simplified type definitions vendored from @types/hapi__hapi. - * Only includes the types actually accessed by the instrumentation. - */ -/* eslint-disable */ - -export type ServerOptions = Record; - -export declare function server(options?: ServerOptions): Server; -export declare function Server(options?: ServerOptions): Server; - -export type ServerRequestExtType = - | 'onPreAuth' - | 'onCredentials' - | 'onPostAuth' - | 'onPreHandler' - | 'onPostHandler' - | 'onPreResponse' - | 'onRequest'; - -export namespace Lifecycle { - export type Method = (request: any, h: any, err?: Error) => ReturnValue; - export type ReturnValue = any; - export type FailAction = 'error' | 'log' | 'ignore' | Method; -} - -export interface ServerRoute { - path: string; - method: string; - handler?: Lifecycle.Method | T; - options?: ((server: Server) => ServerRouteOptions) | ServerRouteOptions; - [key: string]: any; -} - -interface ServerRouteOptions { - handler?: Lifecycle.Method | any; - [key: string]: any; -} - -export interface Server { - route: (...args: any[]) => any; - ext: (...args: any[]) => any; - register: (...args: any[]) => any; - [key: string]: any; -} - -export interface Plugin { - register: (server: Server, options: T) => void | Promise; - name?: string; - pkg?: { name: string; [key: string]: any }; - [key: string]: any; -} - -export interface PluginNameVersion { - name: string; - [key: string]: any; -} - -export interface PluginPackage { - pkg: { name: string; [key: string]: any }; - [key: string]: any; -} - -export interface ServerRegisterPluginObject { - plugin: Plugin | { plugin: Plugin; [key: string]: any }; - [key: string]: any; -} - -export interface ServerRegisterOptions { - [key: string]: any; -} - -export interface ServerExtEventsObject { - type: string; - [key: string]: any; -} - -export interface ServerExtEventsRequestObject { - type: ServerRequestExtType; - method: Lifecycle.Method; - [key: string]: any; -} - -export interface ServerExtOptions { - [key: string]: any; -} diff --git a/packages/node/src/integrations/tracing/hapi/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/hapi/vendored/instrumentation.ts deleted file mode 100644 index d25083f46910..000000000000 --- a/packages/node/src/integrations/tracing/hapi/vendored/instrumentation.ts +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-hapi - * - Upstream version: @opentelemetry/instrumentation-hapi@0.64.0 - * - Types vendored from @hapi/hapi as simplified interfaces - * - Minor TypeScript strictness adjustments for this repository's compiler settings - * - Span creation migrated to the @sentry/core API; op/origin folded into span creation - */ - -import * as api from '@opentelemetry/api'; -import { setHttpServerSpanRouteAttribute } from '../../../../utils/setHttpServerSpanRouteAttribute'; -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; - -import type * as Hapi from './hapi-types'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; -import { AttributeNames } from './enums/AttributeNames'; -import { - HapiComponentName, - handlerPatched, - type HapiServerRouteInput, - type PatchableServerRoute, - type HapiServerRouteInputMethod, - type HapiPluginInput, - type RegisterFunction, - type PatchableExtMethod, - type ServerExtDirectInput, -} from './internal-types'; -import { - getRouteMetadata, - getPluginName, - isLifecycleExtType, - isLifecycleExtEventObj, - getExtMetadata, - isDirectExtInput, - isPatchableExtMethod, - getPluginFromInput, -} from './utils'; - -const PACKAGE_NAME = '@sentry/instrumentation-hapi'; - -/** Hapi instrumentation for OpenTelemetry */ -export class HapiInstrumentation extends InstrumentationBase { - constructor(config: InstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - protected init() { - return new InstrumentationNodeModuleDefinition( - HapiComponentName, - ['>=17.0.0 <22'], - (module: any) => { - const moduleExports: typeof Hapi = module[Symbol.toStringTag] === 'Module' ? module.default : module; - if (!isWrapped(moduleExports.server)) { - this._wrap(moduleExports, 'server', this._getServerPatch.bind(this)); - } - - if (!isWrapped(moduleExports.Server)) { - this._wrap(moduleExports, 'Server', this._getServerPatch.bind(this)); - } - return moduleExports; - }, - (module: any) => { - const moduleExports: typeof Hapi = module[Symbol.toStringTag] === 'Module' ? module.default : module; - this._massUnwrap([moduleExports], ['server', 'Server']); - }, - ); - } - - /** - * Patches the Hapi.server and Hapi.Server functions in order to instrument - * the server.route, server.ext, and server.register functions via calls to the - * @function _getServerRoutePatch, @function _getServerExtPatch, and - * @function _getServerRegisterPatch functions - * @param original - the original Hapi Server creation function - */ - private _getServerPatch(original: (options?: Hapi.ServerOptions) => Hapi.Server) { - const instrumentation: HapiInstrumentation = this; - const self = this; - return function server(this: Hapi.Server, opts?: Hapi.ServerOptions) { - const newServer: Hapi.Server = original.apply(this, [opts]); - - self._wrap(newServer, 'route', originalRouter => { - return instrumentation._getServerRoutePatch.bind(instrumentation)(originalRouter); - }); - - // Casting as any is necessary here due to multiple overloads on the Hapi.ext - // function, which requires supporting a variety of different parameters - // as extension inputs - self._wrap(newServer, 'ext', originalExtHandler => { - return instrumentation._getServerExtPatch.bind(instrumentation)(originalExtHandler as any); - }); - - // Casting as any is necessary here due to multiple overloads on the Hapi.Server.register - // function, which requires supporting a variety of different types of Plugin inputs - self._wrap(newServer, 'register', instrumentation._getServerRegisterPatch.bind(instrumentation)); - return newServer; - }; - } - - /** - * Patches the plugin register function used by the Hapi Server. This function - * goes through each plugin that is being registered and adds instrumentation - * via a call to the @function _wrapRegisterHandler function. - * @param {RegisterFunction} original - the original register function which - * registers each plugin on the server - */ - private _getServerRegisterPatch(original: RegisterFunction): RegisterFunction { - const instrumentation: HapiInstrumentation = this; - return function register(this: Hapi.Server, pluginInput: HapiPluginInput, options?: Hapi.ServerRegisterOptions) { - if (Array.isArray(pluginInput)) { - for (const pluginObj of pluginInput) { - const plugin = getPluginFromInput(pluginObj); - instrumentation._wrapRegisterHandler(plugin); - } - } else { - const plugin = getPluginFromInput(pluginInput); - instrumentation._wrapRegisterHandler(plugin); - } - return original.apply(this, [pluginInput, options]); - }; - } - - /** - * Patches the Server.ext function which adds extension methods to the specified - * point along the request lifecycle. This function accepts the full range of - * accepted input into the standard Hapi `server.ext` function. For each extension, - * it adds instrumentation to the handler via a call to the @function _wrapExtMethods - * function. - * @param original - the original ext function which adds the extension method to the server - * @param {string} [pluginName] - if present, represents the name of the plugin responsible - * for adding this server extension. Else, signifies that the extension was added directly - */ - private _getServerExtPatch(original: (...args: unknown[]) => unknown, pluginName?: string) { - const instrumentation: HapiInstrumentation = this; - - return function ext(this: ThisParameterType, ...args: Parameters) { - if (Array.isArray(args[0])) { - const eventsList: Hapi.ServerExtEventsObject[] | Hapi.ServerExtEventsRequestObject[] = args[0]; - for (let i = 0; i < eventsList.length; i++) { - const eventObj = eventsList[i]!; - if (isLifecycleExtType(eventObj.type)) { - const lifecycleEventObj = eventObj as Hapi.ServerExtEventsRequestObject; - const handler = instrumentation._wrapExtMethods(lifecycleEventObj.method, eventObj.type, pluginName); - lifecycleEventObj.method = handler; - eventsList[i] = lifecycleEventObj; - } - } - return original.apply(this, args); - } else if (isDirectExtInput(args)) { - const extInput: ServerExtDirectInput = args; - const method: PatchableExtMethod = extInput[1]; - const handler = instrumentation._wrapExtMethods(method, extInput[0], pluginName); - return original.apply(this, [extInput[0], handler, extInput[2]]); - } else if (isLifecycleExtEventObj(args[0])) { - const lifecycleEventObj = args[0]; - const handler = instrumentation._wrapExtMethods(lifecycleEventObj.method, lifecycleEventObj.type, pluginName); - lifecycleEventObj.method = handler; - return original.call(this, lifecycleEventObj); - } - return original.apply(this, args); - }; - } - - /** - * Patches the Server.route function. This function accepts either one or an array - * of Hapi.ServerRoute objects and adds instrumentation on each route via a call to - * the @function _wrapRouteHandler function. - * @param {HapiServerRouteInputMethod} original - the original route function which adds - * the route to the server - * @param {string} [pluginName] - if present, represents the name of the plugin responsible - * for adding this server route. Else, signifies that the route was added directly - */ - private _getServerRoutePatch(original: HapiServerRouteInputMethod, pluginName?: string) { - const instrumentation: HapiInstrumentation = this; - return function route(this: Hapi.Server, route: HapiServerRouteInput): void { - if (Array.isArray(route)) { - for (let i = 0; i < route.length; i++) { - const newRoute = instrumentation._wrapRouteHandler.call(instrumentation, route[i]!, pluginName); - route[i] = newRoute; - } - } else { - // oxlint-disable-next-line no-param-reassign - route = instrumentation._wrapRouteHandler.call(instrumentation, route, pluginName); - } - return original.apply(this, [route]); - }; - } - - /** - * Wraps newly registered plugins to add instrumentation to the plugin's clone of - * the original server. Specifically, wraps the server.route and server.ext functions - * via calls to @function _getServerRoutePatch and @function _getServerExtPatch - * @param {Hapi.Plugin} plugin - the new plugin which is being instrumented - */ - private _wrapRegisterHandler(plugin: Hapi.Plugin): void { - const instrumentation: HapiInstrumentation = this; - const pluginName = getPluginName(plugin); - const oldRegister = plugin.register; - const self = this; - const newRegisterHandler = function (this: typeof plugin, server: Hapi.Server, options: T) { - self._wrap(server, 'route', original => { - return instrumentation._getServerRoutePatch.bind(instrumentation)(original, pluginName); - }); - - // Casting as any is necessary here due to multiple overloads on the Hapi.ext - // function, which requires supporting a variety of different parameters - // as extension inputs - self._wrap(server, 'ext', originalExtHandler => { - return instrumentation._getServerExtPatch.bind(instrumentation)(originalExtHandler as any, pluginName); - }); - return oldRegister.call(this, server, options); - }; - plugin.register = newRegisterHandler; - } - - /** - * Wraps request extension methods to add instrumentation to each new extension handler. - * Patches each individual extension in order to create the - * span and propagate context. It does not create spans when there is no parent span. - * @param {PatchableExtMethod | PatchableExtMethod[]} method - the request extension - * handler which is being instrumented - * @param {Hapi.ServerRequestExtType} extPoint - the point in the Hapi request lifecycle - * which this extension targets - * @param {string} [pluginName] - if present, represents the name of the plugin responsible - * for adding this server route. Else, signifies that the route was added directly - */ - private _wrapExtMethods( - method: T, - extPoint: Hapi.ServerRequestExtType, - pluginName?: string, - ): T { - const instrumentation: HapiInstrumentation = this; - if (method instanceof Array) { - for (let i = 0; i < method.length; i++) { - method[i] = instrumentation._wrapExtMethods(method[i]!, extPoint); - } - return method; - } else if (isPatchableExtMethod(method)) { - if (method[handlerPatched] === true) return method; - method[handlerPatched] = true; - - const newHandler: PatchableExtMethod = function (this: any, ...params: Parameters) { - if (api.trace.getSpan(api.context.active()) === undefined) { - return method.apply(this, params); - } - const metadata = getExtMetadata(extPoint, pluginName, method.name); - return startSpan( - { - name: metadata.name, - op: `${metadata.attributes[AttributeNames.HAPI_TYPE]}.hapi`, - attributes: { - ...metadata.attributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.hapi', - }, - }, - () => method.apply(undefined, params), - ); - }; - return newHandler as T; - } - return method; - } - - /** - * Patches each individual route handler method in order to create the - * span and propagate context. It does not create spans when there is no parent span. - * @param {PatchableServerRoute} route - the route handler which is being instrumented - * @param {string} [pluginName] - if present, represents the name of the plugin responsible - * for adding this server route. Else, signifies that the route was added directly - */ - private _wrapRouteHandler(route: PatchableServerRoute, pluginName?: string): PatchableServerRoute { - if (route[handlerPatched] === true) return route; - route[handlerPatched] = true; - - const wrapHandler: (oldHandler: Hapi.Lifecycle.Method) => Hapi.Lifecycle.Method = oldHandler => { - return function (this: any, ...params: Parameters) { - if (api.trace.getSpan(api.context.active()) === undefined) { - return oldHandler.call(this, ...params); - } - setHttpServerSpanRouteAttribute(route.path); - const metadata = getRouteMetadata(route, pluginName); - return startSpan( - { - name: metadata.name, - op: `${metadata.attributes[AttributeNames.HAPI_TYPE]}.hapi`, - attributes: { - ...metadata.attributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.hapi', - }, - }, - () => oldHandler.call(this, ...params), - ); - }; - }; - - if (typeof route.handler === 'function') { - route.handler = wrapHandler(route.handler as Hapi.Lifecycle.Method); - } else if (typeof route.options === 'function') { - const oldOptions = route.options; - route.options = function (server) { - const options = oldOptions(server); - if (typeof options.handler === 'function') { - options.handler = wrapHandler(options.handler as Hapi.Lifecycle.Method); - } - return options; - }; - } else if (typeof route.options?.handler === 'function') { - route.options.handler = wrapHandler(route.options.handler as Hapi.Lifecycle.Method); - } - return route; - } -} diff --git a/packages/node/src/integrations/tracing/hapi/vendored/internal-types.ts b/packages/node/src/integrations/tracing/hapi/vendored/internal-types.ts deleted file mode 100644 index 4c76706c3e2e..000000000000 --- a/packages/node/src/integrations/tracing/hapi/vendored/internal-types.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-hapi - * - Upstream version: @opentelemetry/instrumentation-hapi@0.64.0 - * - Types vendored from @hapi/hapi as simplified interfaces - */ -/* eslint-disable */ - -import type * as Hapi from './hapi-types'; - -export const HapiComponentName = '@hapi/hapi'; - -/** - * This symbol is used to mark a Hapi route handler or server extension handler as - * already patched, since its possible to use these handlers multiple times - * i.e. when allowing multiple versions of one plugin, or when registering a plugin - * multiple times on different servers. - */ -export const handlerPatched: unique symbol = Symbol('hapi-handler-patched'); - -export type HapiServerRouteInputMethod = (route: HapiServerRouteInput) => void; - -export type HapiServerRouteInput = PatchableServerRoute | PatchableServerRoute[]; - -export type PatchableServerRoute = Hapi.ServerRoute & { - [handlerPatched]?: boolean; -}; - -export type HapiPluginObject = Hapi.ServerRegisterPluginObject; - -export type HapiPluginInput = HapiPluginObject | Array>; - -export type RegisterFunction = (plugin: HapiPluginInput, options?: Hapi.ServerRegisterOptions) => Promise; - -export type PatchableExtMethod = Hapi.Lifecycle.Method & { - [handlerPatched]?: boolean; -}; - -export type ServerExtDirectInput = [ - Hapi.ServerRequestExtType, - Hapi.Lifecycle.Method, - (Hapi.ServerExtOptions | undefined)?, -]; - -export const HapiLayerType = { - ROUTER: 'router', - PLUGIN: 'plugin', - EXT: 'server.ext', -}; - -export const HapiLifecycleMethodNames = new Set([ - 'onPreAuth', - 'onCredentials', - 'onPostAuth', - 'onPreHandler', - 'onPostHandler', - 'onPreResponse', - 'onRequest', -]); diff --git a/packages/node/src/integrations/tracing/hapi/vendored/utils.ts b/packages/node/src/integrations/tracing/hapi/vendored/utils.ts deleted file mode 100644 index 0b35c8bac885..000000000000 --- a/packages/node/src/integrations/tracing/hapi/vendored/utils.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-hapi - * - Upstream version: @opentelemetry/instrumentation-hapi@0.64.0 - * - Types vendored from @hapi/hapi as simplified interfaces - */ - -import type { Attributes } from '@opentelemetry/api'; -import { HTTP_METHOD, HTTP_ROUTE } from '@sentry/conventions/attributes'; -import type * as Hapi from './hapi-types'; -import { - HapiLayerType, - HapiLifecycleMethodNames, - type HapiPluginObject, - type PatchableExtMethod, - type ServerExtDirectInput, -} from './internal-types'; -import { AttributeNames } from './enums/AttributeNames'; - -export function getPluginName(plugin: Hapi.Plugin): string { - if ((plugin as Hapi.PluginNameVersion).name) { - return (plugin as Hapi.PluginNameVersion).name; - } else { - return (plugin as Hapi.PluginPackage).pkg.name; - } -} - -export const isLifecycleExtType = (variableToCheck: unknown): variableToCheck is Hapi.ServerRequestExtType => { - return typeof variableToCheck === 'string' && HapiLifecycleMethodNames.has(variableToCheck); -}; - -export const isLifecycleExtEventObj = ( - variableToCheck: unknown, -): variableToCheck is Hapi.ServerExtEventsRequestObject => { - const event = (variableToCheck as Hapi.ServerExtEventsRequestObject)?.type; - return event !== undefined && isLifecycleExtType(event); -}; - -export const isDirectExtInput = (variableToCheck: unknown): variableToCheck is ServerExtDirectInput => { - return ( - Array.isArray(variableToCheck) && - variableToCheck.length <= 3 && - isLifecycleExtType(variableToCheck[0]) && - typeof variableToCheck[1] === 'function' - ); -}; - -export const isPatchableExtMethod = ( - variableToCheck: PatchableExtMethod | PatchableExtMethod[], -): variableToCheck is PatchableExtMethod => { - return !Array.isArray(variableToCheck); -}; - -export const getRouteMetadata = ( - route: Hapi.ServerRoute, - pluginName?: string, -): { - attributes: Attributes; - name: string; -} => { - const attributes: Attributes = { - [HTTP_ROUTE]: route.path, - // eslint-disable-next-line typescript/no-deprecated - [HTTP_METHOD]: route.method, - }; - - let name; - if (pluginName) { - attributes[AttributeNames.HAPI_TYPE] = HapiLayerType.PLUGIN; - attributes[AttributeNames.PLUGIN_NAME] = pluginName; - name = `${pluginName}: route - ${route.path}`; - } else { - attributes[AttributeNames.HAPI_TYPE] = HapiLayerType.ROUTER; - name = `route - ${route.path}`; - } - - return { attributes, name }; -}; - -export const getExtMetadata = ( - extPoint: Hapi.ServerRequestExtType, - pluginName?: string, - methodName?: string, -): { - attributes: Attributes; - name: string; -} => { - let baseName = `ext - ${extPoint}`; - if (methodName && methodName !== 'method') { - // method is the default name for the extension in the ServerExtEventsObject format. - baseName = `ext - ${extPoint} - ${methodName}`; - } - if (pluginName) { - return { - attributes: { - [AttributeNames.EXT_TYPE]: extPoint, - [AttributeNames.HAPI_TYPE]: HapiLayerType.EXT, - [AttributeNames.PLUGIN_NAME]: pluginName, - }, - name: `${pluginName}: ${baseName}`, - }; - } - return { - attributes: { - [AttributeNames.EXT_TYPE]: extPoint, - [AttributeNames.HAPI_TYPE]: HapiLayerType.EXT, - }, - name: baseName, - }; -}; - -export const getPluginFromInput = (pluginObj: HapiPluginObject): Hapi.Plugin => { - if ('plugin' in pluginObj) { - if ('plugin' in pluginObj.plugin) { - return pluginObj.plugin.plugin; - } - return pluginObj.plugin; - } - return pluginObj; -}; diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index 337df789c1cc..8f87b0e69d9b 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -25,14 +25,12 @@ import { vercelAiIntegration, } from '@sentry/server-utils/orchestrion'; import { instrumentSentryHttp } from '../http'; -import { fastifyIntegration, instrumentFastifyV3 } from './fastify'; +import { fastifyIntegration } from './fastify'; import { redisIntegration } from './redis'; export function getAutoPerformanceIntegrations(): Integration[] { return [ expressIntegration(), - // Fastify keeps the node wrapper: the streamlined integration covers fastify `>=3.21.0 <6`, and - // the wrapper adds `instrumentFastifyV3` for the remaining early-v3 range (`>=3.0.0 <3.21.0`). fastifyIntegration(), graphqlDiagnosticsIntegration(), mongodbIntegration(), @@ -69,10 +67,5 @@ export function getAutoPerformanceIntegrations(): Integration[] { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => void) & { id: string })[] { - return [ - instrumentSentryHttp, - // The streamlined `Fastify` integration covers fastify `>=3.21.0 <6`; `instrumentFastifyV3` - // fills the remaining early-v3 gap (`>=3.0.0 <3.21.0`), so it stays preloaded here. - instrumentFastifyV3, - ]; + return [instrumentSentryHttp]; } diff --git a/packages/node/src/integrations/tracing/kafka/index.ts b/packages/node/src/integrations/tracing/kafka/index.ts deleted file mode 100644 index b56288771904..000000000000 --- a/packages/node/src/integrations/tracing/kafka/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { KafkaJsInstrumentation } from './vendored/instrumentation'; -import { generateInstrumentOnce } from '../../../otel/instrument'; - -const INTEGRATION_NAME = 'Kafka' as const; - -export const instrumentKafka = generateInstrumentOnce(INTEGRATION_NAME, () => new KafkaJsInstrumentation()); diff --git a/packages/node/src/integrations/tracing/kafka/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/kafka/vendored/instrumentation.ts deleted file mode 100644 index d9a5e65237ee..000000000000 --- a/packages/node/src/integrations/tracing/kafka/vendored/instrumentation.ts +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors, Aspecto - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-kafkajs - * - Upstream version: @opentelemetry/instrumentation-kafkajs@0.27.0 - * - Some types vendored from kafkajs with simplifications - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - * - Cross-broker trace propagation uses Sentry's `getTraceData`/`continueTrace` instead of the OTel - * propagator, so the vendored `bufferTextMapGetter` propagator is gone - * - Dropped the OTel metrics (no MeterProvider is wired up) and, with them, the `network.request` - * event listeners they relied on; origin is folded into span creation instead of `index.ts` hooks - */ - -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import { MESSAGING_BATCH_MESSAGE_COUNT } from '@sentry/conventions/attributes'; -import type { Span } from '@sentry/core'; -import { - continueTrace, - SDK_VERSION, - SPAN_STATUS_ERROR, - SPAN_STATUS_OK, - startInactiveSpan, - startNewTrace, - withActiveSpan, -} from '@sentry/core'; -import type { - Consumer, - ConsumerRunConfig, - EachBatchHandler, - EachMessageHandler, - Kafka, - KafkaMessage, - Producer, - RecordMetadata, - Transaction, -} from './kafkajs-types'; -import { - ATTR_MESSAGING_DESTINATION_PARTITION_ID, - MESSAGING_OPERATION_TYPE_VALUE_PROCESS, - MESSAGING_OPERATION_TYPE_VALUE_RECEIVE, -} from './semconv'; -import { - endSpansOnPromise, - getHeaderAsString, - getLinksFromHeaders, - startConsumerSpan, - startProducerSpan, -} from './utils'; - -const PACKAGE_NAME = '@sentry/instrumentation-kafkajs'; - -export class KafkaJsInstrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - protected init(): InstrumentationNodeModuleDefinition { - const unpatch = (moduleExports: any): void => { - if (isWrapped(moduleExports?.Kafka?.prototype.producer)) { - this._unwrap(moduleExports.Kafka.prototype, 'producer'); - } - if (isWrapped(moduleExports?.Kafka?.prototype.consumer)) { - this._unwrap(moduleExports.Kafka.prototype, 'consumer'); - } - }; - - const module = new InstrumentationNodeModuleDefinition( - 'kafkajs', - ['>=0.3.0 <3'], - (moduleExports: any) => { - unpatch(moduleExports); - this._wrap(moduleExports?.Kafka?.prototype, 'producer', this._getProducerPatch()); - this._wrap(moduleExports?.Kafka?.prototype, 'consumer', this._getConsumerPatch()); - - return moduleExports; - }, - unpatch, - ); - return module; - } - - private _getConsumerPatch() { - const instrumentation = this; - return (original: Kafka['consumer']) => { - return function consumer(this: Kafka, ...args: Parameters) { - const newConsumer: Consumer = original.apply(this, args); - - // oxlint-disable-next-line typescript/unbound-method -- property check, the method is never called - if (isWrapped(newConsumer.run)) { - instrumentation._unwrap(newConsumer, 'run'); - } - - instrumentation._wrap(newConsumer, 'run', instrumentation._getConsumerRunPatch()); - - return newConsumer; - }; - }; - } - - private _getProducerPatch() { - const instrumentation = this; - return (original: Kafka['producer']) => { - return function consumer(this: Kafka, ...args: Parameters) { - const newProducer: Producer = original.apply(this, args); - - // oxlint-disable-next-line typescript/unbound-method -- property check, the method is never called - if (isWrapped(newProducer.sendBatch)) { - instrumentation._unwrap(newProducer, 'sendBatch'); - } - instrumentation._wrap(newProducer, 'sendBatch', instrumentation._getSendBatchPatch()); - - // oxlint-disable-next-line typescript/unbound-method -- property check, the method is never called - if (isWrapped(newProducer.send)) { - instrumentation._unwrap(newProducer, 'send'); - } - instrumentation._wrap(newProducer, 'send', instrumentation._getSendPatch()); - - // oxlint-disable-next-line typescript/unbound-method -- property check, the method is never called - if (isWrapped(newProducer.transaction)) { - instrumentation._unwrap(newProducer, 'transaction'); - } - instrumentation._wrap(newProducer, 'transaction', instrumentation._getProducerTransactionPatch()); - - return newProducer; - }; - }; - } - - private _getConsumerRunPatch() { - const instrumentation = this; - return (original: Consumer['run']) => { - return function run(this: Consumer, ...args: Parameters): ReturnType { - const config = args[0]; - if (config?.eachMessage) { - if (isWrapped(config.eachMessage)) { - instrumentation._unwrap(config, 'eachMessage'); - } - instrumentation._wrap(config, 'eachMessage', instrumentation._getConsumerEachMessagePatch()); - } - if (config?.eachBatch) { - if (isWrapped(config.eachBatch)) { - instrumentation._unwrap(config, 'eachBatch'); - } - instrumentation._wrap(config, 'eachBatch', instrumentation._getConsumerEachBatchPatch()); - } - return original.call(this, config); - }; - }; - } - - private _getConsumerEachMessagePatch() { - return (original: ConsumerRunConfig['eachMessage']) => { - return function eachMessage(this: unknown, ...args: Parameters): Promise { - const payload = args[0]; - const sentryTrace = getHeaderAsString(payload.message.headers, 'sentry-trace'); - const baggage = getHeaderAsString(payload.message.headers, 'baggage'); - - // Continue the producer's trace so the consumer span is parented to the message's producer. - return continueTrace({ sentryTrace, baggage }, () => { - const span = startConsumerSpan({ - topic: payload.topic, - message: payload.message, - operationType: MESSAGING_OPERATION_TYPE_VALUE_PROCESS, - attributes: { - [ATTR_MESSAGING_DESTINATION_PARTITION_ID]: String(payload.partition), - }, - }); - - const eachMessagePromise = withActiveSpan(span, () => { - return original!.apply(this, args); - }); - return endSpansOnPromise([span], eachMessagePromise); - }); - }; - }; - } - - private _getConsumerEachBatchPatch() { - return (original: ConsumerRunConfig['eachBatch']) => { - return function eachBatch(this: unknown, ...args: Parameters): Promise { - const payload = args[0]; - // https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/messaging.md#topic-with-multiple-consumers - // A batch pull aggregates messages from many producers, so the receiving span is a fresh root - // trace and each processed message links back to its own producer span. - const receivingSpan = startNewTrace(() => - startConsumerSpan({ - topic: payload.batch.topic, - message: undefined, - operationType: MESSAGING_OPERATION_TYPE_VALUE_RECEIVE, - attributes: { - [MESSAGING_BATCH_MESSAGE_COUNT]: payload.batch.messages.length, - [ATTR_MESSAGING_DESTINATION_PARTITION_ID]: String(payload.batch.partition), - }, - }), - ); - - return withActiveSpan(receivingSpan, () => { - const spans: Span[] = [receivingSpan]; - payload.batch.messages.forEach((message: KafkaMessage) => { - spans.push( - startConsumerSpan({ - topic: payload.batch.topic, - message, - operationType: MESSAGING_OPERATION_TYPE_VALUE_PROCESS, - links: getLinksFromHeaders(message.headers), - attributes: { - [ATTR_MESSAGING_DESTINATION_PARTITION_ID]: String(payload.batch.partition), - }, - }), - ); - }); - const batchMessagePromise: Promise = original!.apply(this, args); - return endSpansOnPromise(spans, batchMessagePromise); - }); - }; - }; - } - - private _getProducerTransactionPatch() { - const instrumentation = this; - return (original: Producer['transaction']) => { - return function transaction( - this: Producer, - ...args: Parameters - ): ReturnType { - const transactionSpan = startInactiveSpan({ name: 'transaction' }); - - const transactionPromise = original.apply(this, args); - - transactionPromise - .then((transaction: Transaction) => { - // oxlint-disable-next-line typescript/unbound-method -- re-bound below via `.apply(this, args)` - const originalSend = transaction.send; - transaction.send = function send(this: Transaction, ...args) { - return withActiveSpan(transactionSpan, () => { - const patched = instrumentation._getSendPatch()(originalSend); - return patched.apply(this, args).catch((err: any) => { - transactionSpan.setStatus({ - code: SPAN_STATUS_ERROR, - message: err?.message, - }); - throw err; - }); - }); - }; - - // oxlint-disable-next-line typescript/unbound-method -- re-bound below via `.apply(this, args)` - const originalSendBatch = transaction.sendBatch; - transaction.sendBatch = function sendBatch(this: Transaction, ...args) { - return withActiveSpan(transactionSpan, () => { - const patched = instrumentation._getSendBatchPatch()(originalSendBatch); - return patched.apply(this, args).catch((err: any) => { - transactionSpan.setStatus({ - code: SPAN_STATUS_ERROR, - message: err?.message, - }); - throw err; - }); - }); - }; - - // oxlint-disable-next-line typescript/unbound-method -- re-bound below via `.apply(this, args)` - const originalCommit = transaction.commit; - transaction.commit = function commit(this: Transaction, ...args) { - const originCommitPromise = originalCommit.apply(this, args).then(() => { - transactionSpan.setStatus({ code: SPAN_STATUS_OK }); - }); - return endSpansOnPromise([transactionSpan], originCommitPromise); - }; - - // oxlint-disable-next-line typescript/unbound-method -- re-bound below via `.apply(this, args)` - const originalAbort = transaction.abort; - transaction.abort = function abort(this: Transaction, ...args) { - const originAbortPromise = originalAbort.apply(this, args); - return endSpansOnPromise([transactionSpan], originAbortPromise); - }; - }) - .catch((err: any) => { - transactionSpan.setStatus({ - code: SPAN_STATUS_ERROR, - message: err?.message, - }); - transactionSpan.end(); - }); - - return transactionPromise; - }; - }; - } - - private _getSendBatchPatch() { - return (original: Producer['sendBatch'] | Transaction['sendBatch']) => { - return function sendBatch( - this: Producer | Transaction, - ...args: Parameters - ): ReturnType { - const batch = args[0]; - const messages = batch.topicMessages || []; - - const spans: Span[] = []; - - messages.forEach((topicMessage: any) => { - topicMessage.messages.forEach((message: any) => { - spans.push(startProducerSpan(topicMessage.topic, message)); - }); - }); - const origSendResult: Promise = original.apply(this, args); - return endSpansOnPromise(spans, origSendResult); - }; - }; - } - - private _getSendPatch() { - return (original: Producer['send'] | Transaction['send']) => { - return function send( - this: Producer | Transaction, - ...args: Parameters - ): ReturnType { - const record = args[0]; - const spans: Span[] = record.messages.map((message: any) => { - return startProducerSpan(record.topic, message); - }); - - const origSendResult: Promise = original.apply(this, args); - return endSpansOnPromise(spans, origSendResult); - }; - }; - } -} diff --git a/packages/node/src/integrations/tracing/kafka/vendored/kafkajs-types.ts b/packages/node/src/integrations/tracing/kafka/vendored/kafkajs-types.ts deleted file mode 100644 index d6db2e7a486e..000000000000 --- a/packages/node/src/integrations/tracing/kafka/vendored/kafkajs-types.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Simplified types inlined from kafkajs/types/index.d.ts. - * Only includes members accessed by this instrumentation. - */ - -type Sender = { - send(record: any): Promise; - sendBatch(batch: any): Promise; -}; - -export type Producer = Sender & { - connect(): Promise; - disconnect(): Promise; - isIdempotent(): boolean; - transaction(): Promise; - [key: string]: any; -}; - -export type Transaction = Sender & { - sendOffsets(offsets: any): Promise; - commit(): Promise; - abort(): Promise; - isActive(): boolean; -}; - -export type Consumer = { - connect(): Promise; - disconnect(): Promise; - subscribe(subscription: any): Promise; - run(config?: any): Promise; - [key: string]: any; -}; - -export declare class Kafka { - consumer(config: any): Consumer; - producer(config?: any): Producer; - [key: string]: any; -} - -export interface Message { - key?: Buffer | string | null; - value: Buffer | string | null; - partition?: number; - headers?: Record; - timestamp?: string; -} - -export type KafkaMessage = { [key: string]: any } & Message; - -export type RecordMetadata = { - topicName: string; - partition: number; - errorCode: number; - offset?: string; - timestamp?: string; - baseOffset?: string; - logAppendTime?: string; - logStartOffset?: string; -}; - -export interface EachMessagePayload { - topic: string; - partition: number; - message: KafkaMessage; - heartbeat(): Promise; - pause(): () => void; -} - -export interface EachBatchPayload { - batch: any; - resolveOffset(offset: string): void; - heartbeat(): Promise; - pause(): () => void; - commitOffsetsIfNecessary(offsets?: any): Promise; - uncommittedOffsets(): any; - isRunning(): boolean; - isStale(): boolean; -} - -export type EachMessageHandler = (payload: EachMessagePayload) => Promise; -export type EachBatchHandler = (payload: EachBatchPayload) => Promise; - -export type ConsumerRunConfig = { - autoCommit?: boolean; - autoCommitInterval?: number | null; - autoCommitThreshold?: number | null; - eachBatchAutoResolve?: boolean; - partitionsConsumedConcurrently?: number; - eachBatch?: EachBatchHandler; - eachMessage?: EachMessageHandler; -}; diff --git a/packages/node/src/integrations/tracing/kafka/vendored/semconv.ts b/packages/node/src/integrations/tracing/kafka/vendored/semconv.ts deleted file mode 100644 index 210867048709..000000000000 --- a/packages/node/src/integrations/tracing/kafka/vendored/semconv.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors, Aspecto - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-kafkajs - * - Upstream version: @opentelemetry/instrumentation-kafkajs@0.27.0 - * - Metric semantic conventions dropped; `error.type` inlined from `@opentelemetry/semantic-conventions` - */ - -/* - * This file contains a copy of unstable semantic convention definitions - * used by this package. - * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv - */ - -/** - * The identifier of the partition messages are sent to or received from, unique within the `messaging.destination.name`. - * - * @example "1" - * - * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - */ -export const ATTR_MESSAGING_DESTINATION_PARTITION_ID = 'messaging.destination.partition.id' as const; - -/** - * Message keys in Kafka are used for grouping alike messages to ensure they're processed on the same partition. They differ from `messaging.message.id` in that they're not unique. If the key is `null`, the attribute **MUST NOT** be set. - * - * @example "myKey" - * - * @note If the key type is not string, it's string representation has to be supplied for the attribute. If the key has no unambiguous, canonical string form, don't include its value. - * - * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - */ -export const ATTR_MESSAGING_KAFKA_MESSAGE_KEY = 'messaging.kafka.message.key' as const; - -/** - * A boolean that is true if the message is a tombstone. - * - * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - */ -export const ATTR_MESSAGING_KAFKA_MESSAGE_TOMBSTONE = 'messaging.kafka.message.tombstone' as const; - -/** - * The offset of a record in the corresponding Kafka partition. - * - * @example 42 - * - * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - */ -export const ATTR_MESSAGING_KAFKA_OFFSET = 'messaging.kafka.offset' as const; - -/** - * Enum value "process" for attribute `messaging.operation.type`. - */ -export const MESSAGING_OPERATION_TYPE_VALUE_PROCESS = 'process' as const; - -/** - * Enum value "receive" for attribute `messaging.operation.type`. - */ -export const MESSAGING_OPERATION_TYPE_VALUE_RECEIVE = 'receive' as const; - -/** - * Enum value "send" for attribute `messaging.operation.type`. - */ -export const MESSAGING_OPERATION_TYPE_VALUE_SEND = 'send' as const; - -/** - * Enum value "kafka" for attribute `messaging.system`. - */ -export const MESSAGING_SYSTEM_VALUE_KAFKA = 'kafka' as const; - -/** - * Enum value "_OTHER" for attribute `error.type`. A fallback error value to be used when - * the instrumentation doesn't define a custom value. - */ -export const ERROR_TYPE_VALUE_OTHER = '_OTHER' as const; diff --git a/packages/node/src/integrations/tracing/kafka/vendored/utils.ts b/packages/node/src/integrations/tracing/kafka/vendored/utils.ts deleted file mode 100644 index d9a2bd88acd8..000000000000 --- a/packages/node/src/integrations/tracing/kafka/vendored/utils.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors, Aspecto - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-kafkajs - * - Upstream version: @opentelemetry/instrumentation-kafkajs@0.27.0 - * - Span creation extracted here and migrated to the @sentry/core API; origin folded into span creation - */ - -import { TraceFlags } from '@opentelemetry/api'; -import { - ERROR_TYPE, - MESSAGING_DESTINATION_NAME, - MESSAGING_OPERATION_NAME, - MESSAGING_OPERATION_TYPE, - MESSAGING_SYSTEM, - SENTRY_KIND, -} from '@sentry/conventions/attributes'; -import type { Span, SpanAttributes, SpanLink } from '@sentry/core'; -import { - getTraceData, - propagationContextFromHeaders, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startInactiveSpan, -} from '@sentry/core'; -import type { KafkaMessage, Message } from './kafkajs-types'; -import { - ATTR_MESSAGING_DESTINATION_PARTITION_ID, - ATTR_MESSAGING_KAFKA_MESSAGE_KEY, - ATTR_MESSAGING_KAFKA_MESSAGE_TOMBSTONE, - ATTR_MESSAGING_KAFKA_OFFSET, - ERROR_TYPE_VALUE_OTHER, - MESSAGING_OPERATION_TYPE_VALUE_RECEIVE, - MESSAGING_OPERATION_TYPE_VALUE_SEND, - MESSAGING_SYSTEM_VALUE_KAFKA, -} from './semconv'; - -const PRODUCER_ORIGIN = 'auto.kafkajs.otel.producer'; -const CONSUMER_ORIGIN = 'auto.kafkajs.otel.consumer'; - -export interface ConsumerSpanOptions { - topic: string; - message: KafkaMessage | undefined; - operationType: string; - attributes: SpanAttributes; - links?: SpanLink[]; -} - -/** - * Reads a header value off a kafkajs message as a string. kafkajs delivers headers as `Buffer`s (or - * arrays of them), so we normalize to a string before handing them to Sentry's trace helpers. - */ -export function getHeaderAsString(headers: KafkaMessage['headers'], key: string): string | undefined { - const value = headers?.[key]; - if (value == null) { - return undefined; - } - return Array.isArray(value) ? value[0]?.toString() : value.toString(); -} - -/** - * Builds a span link to the producer span carried in the message headers, mirroring the upstream - * behavior of linking each batch-processed message to its originating producer span. - */ -export function getLinksFromHeaders(headers: KafkaMessage['headers']): SpanLink[] | undefined { - const sentryTrace = getHeaderAsString(headers, 'sentry-trace'); - if (!sentryTrace) { - return undefined; - } - - const { traceId, parentSpanId, sampled } = propagationContextFromHeaders( - sentryTrace, - getHeaderAsString(headers, 'baggage'), - ); - if (!parentSpanId) { - return undefined; - } - - return [ - { - context: { - traceId, - spanId: parentSpanId, - isRemote: true, - traceFlags: sampled ? TraceFlags.SAMPLED : TraceFlags.NONE, - }, - }, - ]; -} - -/** Starts an inactive consumer (process/receive) span carrying the kafkajs messaging attributes. */ -export function startConsumerSpan({ topic, message, operationType, links, attributes }: ConsumerSpanOptions): Span { - const operationName = - operationType === MESSAGING_OPERATION_TYPE_VALUE_RECEIVE - ? 'poll' // for batch processing spans - : operationType; // for individual message processing spans - - return startInactiveSpan({ - name: `${operationName} ${topic}`, - links, - attributes: { - [SENTRY_KIND]: operationType === MESSAGING_OPERATION_TYPE_VALUE_RECEIVE ? 'client' : 'consumer', - ...attributes, - [MESSAGING_SYSTEM]: MESSAGING_SYSTEM_VALUE_KAFKA, - [MESSAGING_DESTINATION_NAME]: topic, - [MESSAGING_OPERATION_TYPE]: operationType, - [MESSAGING_OPERATION_NAME]: operationName, - [ATTR_MESSAGING_KAFKA_MESSAGE_KEY]: message?.key ? String(message.key) : undefined, - [ATTR_MESSAGING_KAFKA_MESSAGE_TOMBSTONE]: message?.key && message.value === null ? true : undefined, - [ATTR_MESSAGING_KAFKA_OFFSET]: message?.offset, - // Mirror the upstream behavior of only tagging per-message processing spans (not the batch - // receiving span, which carries no message) with the auto origin. - ...(message ? { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: CONSUMER_ORIGIN } : {}), - }, - }); -} - -/** Starts an inactive producer span and propagates its trace into the message headers. */ -export function startProducerSpan(topic: string, message: Message): Span { - const span = startInactiveSpan({ - name: `send ${topic}`, - attributes: { - [SENTRY_KIND]: 'producer', - [MESSAGING_SYSTEM]: MESSAGING_SYSTEM_VALUE_KAFKA, - [MESSAGING_DESTINATION_NAME]: topic, - [ATTR_MESSAGING_KAFKA_MESSAGE_KEY]: message.key ? String(message.key) : undefined, - [ATTR_MESSAGING_KAFKA_MESSAGE_TOMBSTONE]: message.key && message.value === null ? true : undefined, - [ATTR_MESSAGING_DESTINATION_PARTITION_ID]: - message.partition !== undefined ? String(message.partition) : undefined, - [MESSAGING_OPERATION_NAME]: 'send', - [MESSAGING_OPERATION_TYPE]: MESSAGING_OPERATION_TYPE_VALUE_SEND, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: PRODUCER_ORIGIN, - }, - }); - - // Propagate the producer span's trace to consumers via the message headers. - message.headers = message.headers ?? {}; - const traceData = getTraceData({ span }); - if (traceData['sentry-trace']) { - message.headers['sentry-trace'] = traceData['sentry-trace']; - } - if (traceData.baggage) { - message.headers['baggage'] = traceData.baggage; - } - - return span; -} - -/** - * Resolves once `sendPromise` settles, ending all `spans` and, on failure, marking them with the - * error status and `error.type` before re-throwing. - */ -export function endSpansOnPromise(spans: Span[], sendPromise: Promise): Promise { - return Promise.resolve(sendPromise) - .catch(reason => { - let errorMessage: string | undefined; - let errorType: string = ERROR_TYPE_VALUE_OTHER; - if (typeof reason === 'string' || reason === undefined) { - errorMessage = reason; - } else if (typeof reason === 'object' && Object.prototype.hasOwnProperty.call(reason, 'message')) { - errorMessage = reason.message; - errorType = reason.constructor.name; - } - - spans.forEach(span => { - span.setAttribute(ERROR_TYPE, errorType); - span.setStatus({ - code: SPAN_STATUS_ERROR, - message: errorMessage, - }); - }); - - throw reason; - }) - .finally(() => { - spans.forEach(span => span.end()); - }); -} diff --git a/packages/node/src/integrations/tracing/knex/index.ts b/packages/node/src/integrations/tracing/knex/index.ts deleted file mode 100644 index 8c32fe87168c..000000000000 --- a/packages/node/src/integrations/tracing/knex/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { KnexInstrumentation } from './vendored/instrumentation'; -import type { IntegrationFn } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce } from '../../../otel/instrument'; -import { isOrchestrionInjected, knexIntegration as knexChannelIntegration } from '@sentry/server-utils/orchestrion'; - -const INTEGRATION_NAME = 'Knex' as const; - -export const instrumentKnex = generateInstrumentOnce(INTEGRATION_NAME, () => new KnexInstrumentation()); - -const _knexIntegration = (() => { - return { - name: INTEGRATION_NAME, - setupOnce() { - // 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; - -/** - * Knex integration - * - * Capture tracing data for [Knex](https://knexjs.org/). - * - * @example - * ```javascript - * import * as Sentry from '@sentry/node'; - * - * Sentry.init({ - * integrations: [Sentry.knexIntegration()], - * }); - * ``` - */ -export const knexIntegration = defineIntegration(_knexIntegration); diff --git a/packages/node/src/integrations/tracing/knex/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/knex/vendored/instrumentation.ts deleted file mode 100644 index e3b00ed2fcc4..000000000000 --- a/packages/node/src/integrations/tracing/knex/vendored/instrumentation.ts +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-knex - * - Upstream version: @opentelemetry/instrumentation-knex@0.62.0 - * - Minor TypeScript strictness adjustments for this repository's compiler settings - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - */ - -/* oxlint-disable typescript/no-deprecated */ - -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import type { Span, SpanAttributes } from '@sentry/core'; -import { - getActiveSpan, - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startSpan, -} from '@sentry/core'; -import { - DB_NAME, - DB_OPERATION, - DB_STATEMENT, - DB_SYSTEM, - DB_USER, - NET_PEER_NAME, - NET_PEER_PORT, - NET_TRANSPORT, - SENTRY_KIND, -} from '@sentry/conventions/attributes'; -import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile'; -import { ATTR_DB_SQL_TABLE } from './semconv'; -import * as utils from './utils'; - -const PACKAGE_NAME = '@sentry/instrumentation-knex'; -const ORIGIN = 'auto.db.otel.knex'; - -const MODULE_NAME = 'knex'; -const SUPPORTED_VERSIONS = [ - // use "lib/execution" for runner.js, "lib" for client.js as basepath, latest tested 0.95.6 - '>=0.22.0 <4', - // use "lib" as basepath - '>=0.10.0 <0.18.0', - '>=0.19.0 <0.22.0', - // use "src" as basepath - '>=0.18.0 <0.19.0', -]; - -// Max length of the query text captured in the `db.statement` attribute; ".." is appended when truncated. -const MAX_QUERY_LENGTH = 1022; - -const parentSpanSymbol = Symbol('sentry.instrumentation-knex.parent-span'); - -export class KnexInstrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - public init(): InstrumentationNodeModuleDefinition { - const module = new InstrumentationNodeModuleDefinition(MODULE_NAME, SUPPORTED_VERSIONS); - - module.files.push( - this._getClientNodeModuleFileInstrumentation('src'), - this._getClientNodeModuleFileInstrumentation('lib'), - this._getRunnerNodeModuleFileInstrumentation('src'), - this._getRunnerNodeModuleFileInstrumentation('lib'), - this._getRunnerNodeModuleFileInstrumentation('lib/execution'), - ); - - return module; - } - - private _getRunnerNodeModuleFileInstrumentation(basePath: string): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - `knex/${basePath}/runner.js`, - SUPPORTED_VERSIONS, - (Runner: any, moduleVersion?: string) => { - this._ensureWrapped(Runner.prototype, 'query', this._createQueryWrapper(moduleVersion)); - return Runner; - }, - (Runner: any) => { - this._unwrap(Runner.prototype, 'query'); - return Runner; - }, - ); - } - - private _getClientNodeModuleFileInstrumentation(basePath: string): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - `knex/${basePath}/client.js`, - SUPPORTED_VERSIONS, - (Client: any) => { - this._ensureWrapped(Client.prototype, 'queryBuilder', this._storeContext.bind(this)); - this._ensureWrapped(Client.prototype, 'schemaBuilder', this._storeContext.bind(this)); - this._ensureWrapped(Client.prototype, 'raw', this._storeContext.bind(this)); - return Client; - }, - (Client: any) => { - this._unwrap(Client.prototype, 'queryBuilder'); - this._unwrap(Client.prototype, 'schemaBuilder'); - this._unwrap(Client.prototype, 'raw'); - return Client; - }, - ); - } - - private _createQueryWrapper(moduleVersion?: string) { - return function wrapQuery(original: (...args: any[]) => any) { - return function wrapped_logging_method(this: any, query: any) { - const config = this.client.config; - - const table = utils.extractTableName(this.builder); - const operation = query?.method; - const connectionString = config?.connection?.connectionString; - const name = - config?.connection?.filename || - config?.connection?.database || - utils.extractDatabaseFromConnectionString(connectionString); - - const attributes: SpanAttributes = { - [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - 'knex.version': moduleVersion, - [DB_SYSTEM]: utils.mapSystem(this.client.driverName), - [ATTR_DB_SQL_TABLE]: table, - [DB_OPERATION]: operation, - [DB_USER]: config?.connection?.user, - [DB_NAME]: name, - [NET_PEER_NAME]: config?.connection?.host ?? utils.extractHostFromConnectionString(connectionString), - [NET_PEER_PORT]: config?.connection?.port ?? utils.extractPortFromConnectionString(connectionString), - [NET_TRANSPORT]: config?.connection?.filename === ':memory:' ? 'inproc' : undefined, - [DB_STATEMENT]: utils.limitLength(query?.sql, MAX_QUERY_LENGTH), - }; - - // The query builder captures the span active when it was created (see `_storeContext`). - // `onlyIfParent` ensures we only instrument queries that run as part of an existing trace. - const parentSpan: Span | undefined = this.builder[parentSpanSymbol] || getActiveSpan(); - - const args = arguments; - return startSpan( - { - name: utils.getName(name, operation, table), - attributes, - parentSpan, - onlyIfParent: true, - }, - span => - // `Runner.query` returns a real, already-executing Promise, so it is safe to let - // `startSpan` await it and auto-end the span. - original.apply(this, args).catch((err: any) => { - const formatter = utils.getFormatter(this); - const fullQuery = formatter(query.sql, query.bindings || []); - const message = err.message.replace(`${fullQuery} - `, ''); - span.setStatus({ code: SPAN_STATUS_ERROR, message }); - throw err; - }), - ); - }; - }; - } - - private _storeContext(original: (...args: any[]) => any) { - return function wrapped_logging_method(this: any) { - const builder = original.apply(this, arguments); - // Capture the span that is active when the query builder is created. The query often executes - // in a different async context, so we reuse this span as the parent when the query runs. - Object.defineProperty(builder, parentSpanSymbol, { - value: getActiveSpan(), - }); - return builder; - }; - } - - private _ensureWrapped(obj: any, methodName: string, wrapper: (original: any) => any): void { - if (isWrapped(obj[methodName])) { - this._unwrap(obj, methodName); - } - this._wrap(obj, methodName, wrapper); - } -} diff --git a/packages/node/src/integrations/tracing/knex/vendored/semconv.ts b/packages/node/src/integrations/tracing/knex/vendored/semconv.ts deleted file mode 100644 index 3cbb5a94d7be..000000000000 --- a/packages/node/src/integrations/tracing/knex/vendored/semconv.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-knex - * - Upstream version: @opentelemetry/instrumentation-knex@0.62.0 - */ - -/** - * @deprecated Replaced by `db.collection.name`. - */ -export const ATTR_DB_SQL_TABLE = 'db.sql.table' as const; - -export const DB_SYSTEM_NAME_VALUE_SQLITE = 'sqlite' as const; - -export const DB_SYSTEM_NAME_VALUE_POSTGRESQL = 'postgresql' as const; diff --git a/packages/node/src/integrations/tracing/knex/vendored/utils.ts b/packages/node/src/integrations/tracing/knex/vendored/utils.ts deleted file mode 100644 index d60013963856..000000000000 --- a/packages/node/src/integrations/tracing/knex/vendored/utils.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-knex - * - Upstream version: @opentelemetry/instrumentation-knex@0.62.0 - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - */ - -import { DB_SYSTEM_NAME_VALUE_POSTGRESQL, DB_SYSTEM_NAME_VALUE_SQLITE } from './semconv'; - -export const getFormatter = (runner: any) => { - if (runner) { - if (runner.client) { - if (runner.client._formatQuery) { - return runner.client._formatQuery.bind(runner.client); - } else if (runner.client.SqlString) { - return runner.client.SqlString.format.bind(runner.client.SqlString); - } - } - if (runner.builder) { - return runner.builder.toString.bind(runner.builder); - } - } - return () => ''; -}; - -const systemMap = new Map([ - ['sqlite3', DB_SYSTEM_NAME_VALUE_SQLITE], - ['pg', DB_SYSTEM_NAME_VALUE_POSTGRESQL], -]); - -export const mapSystem = (knexSystem: string) => { - return systemMap.get(knexSystem) || knexSystem; -}; - -export const getName = (db: string, operation?: string, table?: string) => { - if (operation) { - if (table) { - return `${operation} ${db}.${table}`; - } - return `${operation} ${db}`; - } - return db; -}; - -export const limitLength = (str: string, maxLength: number) => { - if (typeof str === 'string' && typeof maxLength === 'number' && 0 < maxLength && maxLength < str.length) { - return `${str.substring(0, maxLength)}..`; - } - return str; -}; - -export const extractDatabaseFromConnectionString = (connectionString?: string): string | undefined => { - if (!connectionString) return undefined; - try { - const db = new URL(connectionString).pathname?.replace(/^\//, ''); - return db || undefined; - } catch { - return undefined; - } -}; - -export const extractHostFromConnectionString = (connectionString?: string): string | undefined => { - if (!connectionString) return undefined; - try { - return new URL(connectionString).hostname || undefined; - } catch { - return undefined; - } -}; - -export const extractPortFromConnectionString = (connectionString?: string): number | undefined => { - if (!connectionString) return undefined; - try { - const port = new URL(connectionString).port; - return port ? parseInt(port, 10) : undefined; - } catch { - return undefined; - } -}; - -export const extractTableName = (builder: any): string => { - const table = builder?._single?.table; - if (typeof table === 'object') { - return extractTableName(table); - } - return table; -}; diff --git a/packages/node/src/integrations/tracing/koa/index.ts b/packages/node/src/integrations/tracing/koa/index.ts index 0cb1671bd8c2..7c467df9b6f8 100644 --- a/packages/node/src/integrations/tracing/koa/index.ts +++ b/packages/node/src/integrations/tracing/koa/index.ts @@ -1,28 +1,6 @@ -import type { KoaInstrumentationConfig, KoaLayerType } from './vendored/types'; -import { KoaInstrumentation } from './vendored/instrumentation'; import { captureException } from '@sentry/core'; -import { generateInstrumentOnce } from '../../../otel/instrument'; import { ensureIsWrapped } from '../../../utils/ensureIsWrapped'; -interface KoaOptions { - /** - * Ignore layers of specified types - */ - ignoreLayersType?: Array<'middleware' | 'router'>; -} - -const INTEGRATION_NAME = 'Koa' as const; - -export const instrumentKoa = generateInstrumentOnce( - INTEGRATION_NAME, - KoaInstrumentation, - (options: KoaOptions = {}) => { - return { - ignoreLayersType: options.ignoreLayersType as KoaLayerType[], - } satisfies KoaInstrumentationConfig; - }, -); - /** * Add an Koa error handler to capture errors to Sentry. * diff --git a/packages/node/src/integrations/tracing/koa/vendored/enums/AttributeNames.ts b/packages/node/src/integrations/tracing/koa/vendored/enums/AttributeNames.ts deleted file mode 100644 index eb986fa5aa03..000000000000 --- a/packages/node/src/integrations/tracing/koa/vendored/enums/AttributeNames.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-koa - * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 - */ - -export enum AttributeNames { - KOA_TYPE = 'koa.type', - KOA_NAME = 'koa.name', -} diff --git a/packages/node/src/integrations/tracing/koa/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/koa/vendored/instrumentation.ts deleted file mode 100644 index 012d3ad70599..000000000000 --- a/packages/node/src/integrations/tracing/koa/vendored/instrumentation.ts +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-koa - * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 - * - Minor TypeScript strictness adjustments for this repository's compiler settings - * - Span creation migrated to the @sentry/core API; op/origin/name and transaction name folded into - * span creation (previously set via a Sentry requestHook) - */ - -import * as api from '@opentelemetry/api'; -import { isWrapped, InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; - -import { KoaLayerType, type KoaInstrumentationConfig } from './types'; -import { - debug, - getDefaultIsolationScope, - getIsolationScope, - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startSpan, -} from '@sentry/core'; -import { HTTP_ROUTE } from '@sentry/conventions/attributes'; -import { getMiddlewareMetadata, isLayerIgnored } from './utils'; -import { setHttpServerSpanRouteAttribute } from '../../../../utils/setHttpServerSpanRouteAttribute'; -import { DEBUG_BUILD } from '../../../../debug-build'; -import { AttributeNames } from './enums/AttributeNames'; -import { - kLayerPatched, - type Next, - type KoaContext, - type KoaMiddleware, - type KoaPatchedMiddleware, -} from './internal-types'; - -const PACKAGE_NAME = '@sentry/instrumentation-koa'; - -interface KoaModuleExports { - prototype: { use: KoaMiddleware }; -} - -type KoaModule = KoaModuleExports & { [Symbol.toStringTag]?: string; default?: KoaModuleExports }; - -/** Koa instrumentation for OpenTelemetry */ -export class KoaInstrumentation extends InstrumentationBase { - constructor(config: KoaInstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - protected init() { - return new InstrumentationNodeModuleDefinition( - 'koa', - ['>=2.0.0 <4'], - (module: KoaModule) => { - const moduleExports = - module[Symbol.toStringTag] === 'Module' - ? module.default // ESM - : module; // CommonJS - if (moduleExports == null) { - return moduleExports; - } - if (isWrapped(moduleExports.prototype.use)) { - this._unwrap(moduleExports.prototype, 'use'); - } - this._wrap(moduleExports.prototype, 'use', this._getKoaUsePatch.bind(this)); - return module; - }, - (module: KoaModule) => { - const moduleExports = - module[Symbol.toStringTag] === 'Module' - ? module.default // ESM - : module; // CommonJS - if (moduleExports && isWrapped(moduleExports.prototype.use)) { - this._unwrap(moduleExports.prototype, 'use'); - } - }, - ); - } - - /** - * Patches the Koa.use function in order to instrument each original - * middleware layer which is introduced - * @param {KoaMiddleware} middleware - the original middleware function - */ - private _getKoaUsePatch(original: (middleware: KoaMiddleware) => unknown) { - const patchRouterDispatch = this._patchRouterDispatch.bind(this); - const patchLayer = this._patchLayer.bind(this); - return function use(this: unknown, middlewareFunction: KoaMiddleware) { - const patchedFunction = middlewareFunction.router - ? patchRouterDispatch(middlewareFunction) - : patchLayer(middlewareFunction, false); - return original.apply(this, [patchedFunction]); - }; - } - - /** - * Patches the dispatch function used by @koa/router. This function - * goes through each routed middleware and adds instrumentation via a call - * to the @function _patchLayer function. - * @param {KoaMiddleware} dispatchLayer - the original dispatch function which dispatches - * routed middleware - */ - private _patchRouterDispatch(dispatchLayer: KoaMiddleware): KoaMiddleware { - const router = dispatchLayer.router; - - const routesStack = router?.stack ?? []; - for (const pathLayer of routesStack) { - const path = pathLayer.path; - const pathStack = pathLayer.stack; - for (let j = 0; j < pathStack.length; j++) { - const routedMiddleware: KoaMiddleware = pathStack[j]!; - pathStack[j] = this._patchLayer(routedMiddleware, true, path); - } - } - - return dispatchLayer; - } - - /** - * Patches each individual @param middlewareLayer function in order to create the - * span and propagate context. It does not create spans when there is no parent span. - * @param {KoaMiddleware} middlewareLayer - the original middleware function. - * @param {boolean} isRouter - tracks whether the original middleware function - * was dispatched by the router originally - * @param {string?} layerPath - if present, provides additional data from the - * router about the routed path which the middleware is attached to - */ - private _patchLayer( - middlewareLayer: KoaPatchedMiddleware, - isRouter: boolean, - layerPath?: string | RegExp, - ): KoaMiddleware { - const layerType = isRouter ? KoaLayerType.ROUTER : KoaLayerType.MIDDLEWARE; - // Skip patching layer if its ignored in the config - if (middlewareLayer[kLayerPatched] === true || isLayerIgnored(layerType, this.getConfig())) return middlewareLayer; - - if ( - middlewareLayer.constructor.name === 'GeneratorFunction' || - middlewareLayer.constructor.name === 'AsyncGeneratorFunction' - ) { - return middlewareLayer; - } - - middlewareLayer[kLayerPatched] = true; - - return (context: KoaContext, next: Next) => { - const parent = api.trace.getSpan(api.context.active()); - if (parent === undefined) { - return middlewareLayer(context, next); - } - const metadata = getMiddlewareMetadata(context, middlewareLayer, isRouter, layerPath); - - if (context._matchedRoute) { - setHttpServerSpanRouteAttribute(context._matchedRoute.toString()); - } - - const koaName = metadata.attributes[AttributeNames.KOA_NAME]; - // Somehow, name is sometimes `''` for middleware spans - // See: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2220 - const name = typeof koaName === 'string' ? koaName || '< unknown >' : metadata.name; - - return startSpan( - { - name, - op: `${layerType}.koa`, - attributes: { - ...metadata.attributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.koa', - }, - }, - () => { - const route = metadata.attributes[HTTP_ROUTE]; - if (getIsolationScope() === getDefaultIsolationScope()) { - DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName'); - } else if (route) { - const method = (context.request as { method?: string } | undefined)?.method?.toUpperCase() || 'GET'; - getIsolationScope().setTransactionName(`${method} ${route}`); - } - return middlewareLayer(context, next); - }, - ); - }; - } -} diff --git a/packages/node/src/integrations/tracing/koa/vendored/internal-types.ts b/packages/node/src/integrations/tracing/koa/vendored/internal-types.ts deleted file mode 100644 index 583f5efb8c0f..000000000000 --- a/packages/node/src/integrations/tracing/koa/vendored/internal-types.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-koa - * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 - * - Some types vendored from @types/koa, @types/koa-compose, and @types/koa__router with simplifications - */ - -interface DefaultState {} - -export type Next = () => Promise; - -type ParameterizedContext<_StateT = DefaultState, ContextT = {}, _ResponseBodyT = unknown> = { - [key: string]: unknown; -} & ContextT; - -type Middleware = ( - context: ParameterizedContext, - next: Next, -) => unknown; - -interface RouterParamContext { - params: Record; - router: Router; - _matchedRoute: string | RegExp | undefined; - _matchedRouteName: string | undefined; -} - -interface Layer { - path: string | RegExp; - stack: KoaMiddleware[]; -} - -export interface Router<_StateT = DefaultState, _ContextT = {}> { - stack: Layer[]; -} - -export type KoaContext = ParameterizedContext; -export type KoaMiddleware = Middleware & { - router?: Router; -}; - -/** - * This symbol is used to mark a Koa layer as being already instrumented - * since its possible to use a given layer multiple times (ex: middlewares) - */ -export const kLayerPatched: unique symbol = Symbol('koa-layer-patched'); - -export type KoaPatchedMiddleware = KoaMiddleware & { - [kLayerPatched]?: boolean; -}; diff --git a/packages/node/src/integrations/tracing/koa/vendored/types.ts b/packages/node/src/integrations/tracing/koa/vendored/types.ts deleted file mode 100644 index 0565c1d4ce2c..000000000000 --- a/packages/node/src/integrations/tracing/koa/vendored/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-koa - * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 - */ - -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; - -export enum KoaLayerType { - ROUTER = 'router', - MIDDLEWARE = 'middleware', -} - -/** - * Options available for the Koa Instrumentation (see [documentation](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-Instrumentation-koa#koa-Instrumentation-options)) - */ -export interface KoaInstrumentationConfig extends InstrumentationConfig { - /** Ignore specific layers based on their type */ - ignoreLayersType?: KoaLayerType[]; -} diff --git a/packages/node/src/integrations/tracing/koa/vendored/utils.ts b/packages/node/src/integrations/tracing/koa/vendored/utils.ts deleted file mode 100644 index 070aa7eb965d..000000000000 --- a/packages/node/src/integrations/tracing/koa/vendored/utils.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-koa - * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 - */ - -import { KoaLayerType, type KoaInstrumentationConfig } from './types'; -import type { KoaContext, KoaMiddleware } from './internal-types'; -import { AttributeNames } from './enums/AttributeNames'; -import type { Attributes } from '@opentelemetry/api'; -import { CODE_FUNCTION_NAME, HTTP_ROUTE } from '@sentry/conventions/attributes'; - -export const getMiddlewareMetadata = ( - context: KoaContext, - layer: KoaMiddleware, - isRouter: boolean, - layerPath?: string | RegExp, -): { - attributes: Attributes; - name: string; -} => { - if (isRouter) { - return { - attributes: { - [AttributeNames.KOA_NAME]: layerPath?.toString(), // TODO(v11): remove, replaced by http.route - [AttributeNames.KOA_TYPE]: KoaLayerType.ROUTER, - [HTTP_ROUTE]: layerPath?.toString(), - }, - name: context._matchedRouteName || `router - ${layerPath}`, - }; - } else { - return { - attributes: { - [AttributeNames.KOA_NAME]: layer.name ?? 'middleware', // TODO(v11): remove, replaced by code.function.name - [AttributeNames.KOA_TYPE]: KoaLayerType.MIDDLEWARE, - [CODE_FUNCTION_NAME]: layer.name ?? 'middleware', - }, - name: `middleware - ${layer.name}`, - }; - } -}; - -/** - * Check whether the given request is ignored by configuration - * @param [list] List of ignore patterns - * @param [onException] callback for doing something when an exception has - * occurred - */ -export const isLayerIgnored = (type: KoaLayerType, config?: KoaInstrumentationConfig): boolean => { - return !!(Array.isArray(config?.ignoreLayersType) && config?.ignoreLayersType?.includes(type)); -}; diff --git a/packages/node/src/integrations/tracing/langchain/index.ts b/packages/node/src/integrations/tracing/langchain/index.ts deleted file mode 100644 index e464af1813ea..000000000000 --- a/packages/node/src/integrations/tracing/langchain/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { LangChainOptions } from '@sentry/core'; -import { LANGCHAIN_INTEGRATION_NAME } from '@sentry/core'; -import { generateInstrumentOnce } from '../../../otel/instrument'; -import { SentryLangChainInstrumentation } from './instrumentation'; - -export const instrumentLangChain = generateInstrumentOnce( - LANGCHAIN_INTEGRATION_NAME, - options => new SentryLangChainInstrumentation(options), -); diff --git a/packages/node/src/integrations/tracing/langchain/instrumentation.ts b/packages/node/src/integrations/tracing/langchain/instrumentation.ts deleted file mode 100644 index 4ddbaee00c60..000000000000 --- a/packages/node/src/integrations/tracing/langchain/instrumentation.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { - InstrumentationBase, - type InstrumentationConfig, - type InstrumentationModuleDefinition, - InstrumentationNodeModuleDefinition, -} from '@opentelemetry/instrumentation'; -import { InstrumentationNodeModuleFile } from '../InstrumentationNodeModuleFile'; -import type { LangChainOptions } from '@sentry/core'; -import { - _INTERNAL_mergeLangChainCallbackHandler, - _INTERNAL_skipAiProviderWrapping, - ANTHROPIC_AI_INTEGRATION_NAME, - createLangChainCallbackHandler, - GOOGLE_GENAI_INTEGRATION_NAME, - instrumentLangChainEmbeddings, - OPENAI_INTEGRATION_NAME, - SDK_VERSION, -} from '@sentry/core'; - -const supportedVersions = ['>=0.1.0 <2.0.0']; - -type LangChainInstrumentationOptions = InstrumentationConfig & LangChainOptions; - -/** - * Represents the patched shape of LangChain provider package exports - */ -interface PatchedLangChainExports { - [key: string]: unknown; -} - -/** - * Wraps Runnable methods (invoke, stream, batch) to inject Sentry callbacks at request time - * Uses a Proxy to intercept method calls and augment the options.callbacks - */ -function wrapRunnableMethod( - originalMethod: (...args: unknown[]) => unknown, - sentryHandler: unknown, - _methodName: string, -): (...args: unknown[]) => unknown { - return new Proxy(originalMethod, { - apply(target, thisArg, args: unknown[]): unknown { - // LangChain Runnable method signatures: - // invoke(input, options?) - options contains callbacks - // stream(input, options?) - options contains callbacks - // batch(inputs, options?) - options contains callbacks - - // Options is typically the second argument - const optionsIndex = 1; - let options = args[optionsIndex] as Record | undefined; - - // If options don't exist or aren't an object, create them - if (!options || typeof options !== 'object' || Array.isArray(options)) { - options = {}; - args[optionsIndex] = options; - } - - // Inject our callback handler into options.callbacks (request time callbacks) - options.callbacks = _INTERNAL_mergeLangChainCallbackHandler(options.callbacks, sentryHandler); - - // Call original method with augmented options - return Reflect.apply(target, thisArg, args); - }, - }) as (...args: unknown[]) => unknown; -} - -/** - * Sentry LangChain instrumentation using OpenTelemetry. - */ -export class SentryLangChainInstrumentation extends InstrumentationBase { - public constructor(config: LangChainInstrumentationOptions = {}) { - super('@sentry/instrumentation-langchain', SDK_VERSION, config); - } - - /** - * Initializes the instrumentation by defining the modules to be patched. - * We patch the BaseChatModel class methods to inject callbacks - * - * We hook into provider packages (@langchain/anthropic, @langchain/openai, etc.) - * because @langchain/core is often bundled and not loaded as a separate module - */ - public init(): InstrumentationModuleDefinition | InstrumentationModuleDefinition[] { - const modules: InstrumentationModuleDefinition[] = []; - - // Hook into common LangChain provider packages - const providerPackages = [ - '@langchain/anthropic', - '@langchain/openai', - '@langchain/google-genai', - '@langchain/mistralai', - '@langchain/google-vertexai', - '@langchain/groq', - ]; - - for (const packageName of providerPackages) { - // In CJS, LangChain packages re-export from dist/index.cjs files. - // Patching only the root module sometimes misses the real implementation or - // gets overwritten when that file is loaded. We add a file-level patch so that - // _patch runs again on the concrete implementation - modules.push( - new InstrumentationNodeModuleDefinition( - packageName, - supportedVersions, - this._patch.bind(this), - exports => exports, - [ - new InstrumentationNodeModuleFile( - `${packageName}/dist/index.cjs`, - supportedVersions, - this._patch.bind(this), - exports => exports, - ), - ], - ), - ); - } - - // Hook into main 'langchain' package to catch initChatModel (v1+) - modules.push( - new InstrumentationNodeModuleDefinition( - 'langchain', - supportedVersions, - this._patch.bind(this), - exports => exports, - [ - // To catch the CJS build that contains ConfigurableModel / initChatModel for v1 - new InstrumentationNodeModuleFile( - 'langchain/dist/chat_models/universal.cjs', - supportedVersions, - this._patch.bind(this), - exports => exports, - ), - ], - ), - ); - - return modules; - } - - /** - * Core patch logic - patches chat model and embedding methods - * This is called when a LangChain provider package is loaded - */ - private _patch(exports: PatchedLangChainExports): PatchedLangChainExports | void { - // Skip AI provider wrapping now that LangChain is actually being used - // This prevents duplicate spans from Anthropic/OpenAI/GoogleGenAI standalone integrations - _INTERNAL_skipAiProviderWrapping([ - OPENAI_INTEGRATION_NAME, - ANTHROPIC_AI_INTEGRATION_NAME, - GOOGLE_GENAI_INTEGRATION_NAME, - ]); - - const config = this.getConfig(); - - // Create a shared handler instance for chat model callbacks - const sentryHandler = createLangChainCallbackHandler(config); - - // Patch Runnable methods to inject callbacks at request time - // This directly manipulates options.callbacks that LangChain uses - this._patchRunnableMethods(exports, sentryHandler); - - // Patch embedding methods to create spans directly - // Embeddings don't use the callback system, so we wrap the methods themselves - this._patchEmbeddingsMethods(exports, config); - - return exports; - } - - /** - * Patches chat model methods (invoke, stream, batch) to inject Sentry callbacks - * Finds a chat model class from the provider package exports and patches its prototype methods - */ - private _patchRunnableMethods(exports: PatchedLangChainExports, sentryHandler: unknown): void { - // Known chat model class names for each provider - const knownChatModelNames = [ - 'ChatAnthropic', - 'ChatOpenAI', - 'ChatGoogleGenerativeAI', - 'ChatMistralAI', - 'ChatVertexAI', - 'ChatGroq', - 'ConfigurableModel', - ]; - - const exportsToPatch = (exports.universal_exports ?? exports) as Record; - - const chatModelClass = Object.values(exportsToPatch).find(exp => { - return typeof exp === 'function' && knownChatModelNames.includes(exp.name); - }) as { prototype: unknown; name: string } | undefined; - - if (!chatModelClass) { - return; - } - - // Patch directly on chatModelClass.prototype - const targetProto = chatModelClass.prototype as Record; - - // Skip if already patched (both file-level and module-level hooks resolve to the same prototype) - if (targetProto.__sentry_patched__) { - return; - } - targetProto.__sentry_patched__ = true; - - // Patch the methods (invoke, stream, batch) - // All chat model instances will inherit these patched methods - const methodsToPatch = ['invoke', 'stream', 'batch'] as const; - - for (const methodName of methodsToPatch) { - const method = targetProto[methodName]; - if (typeof method === 'function') { - targetProto[methodName] = wrapRunnableMethod( - method as (...args: unknown[]) => unknown, - sentryHandler, - methodName, - ); - } - } - } - - /** - * Patches embedding class methods (embedQuery, embedDocuments) to create Sentry spans. - * - * Unlike chat models which use LangChain's callback system, the Embeddings base class - * has no callback support. We wrap the methods directly on the prototype. - * - * Instruments any exported class whose prototype has both embedQuery and embedDocuments as functions. - */ - private _patchEmbeddingsMethods(exports: PatchedLangChainExports, options: LangChainOptions): void { - const exportsToPatch = (exports.universal_exports ?? exports) as Record; - - for (const exp of Object.values(exportsToPatch)) { - if (typeof exp !== 'function' || !exp.prototype) { - continue; - } - const proto = exp.prototype as Record; - if (typeof proto.embedQuery !== 'function' || typeof proto.embedDocuments !== 'function') { - continue; - } - if (proto.__sentry_patched__) { - continue; - } - proto.__sentry_patched__ = true; - - instrumentLangChainEmbeddings(proto, options); - } - } -} diff --git a/packages/node/src/integrations/tracing/langgraph/index.ts b/packages/node/src/integrations/tracing/langgraph/index.ts deleted file mode 100644 index 738b11ea80a4..000000000000 --- a/packages/node/src/integrations/tracing/langgraph/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { LangGraphOptions } from '@sentry/core'; -import { LANGGRAPH_INTEGRATION_NAME } from '@sentry/core'; -import { generateInstrumentOnce } from '../../../otel/instrument'; -import { SentryLangGraphInstrumentation } from './instrumentation'; - -export const instrumentLangGraph = generateInstrumentOnce( - LANGGRAPH_INTEGRATION_NAME, - options => new SentryLangGraphInstrumentation(options), -); diff --git a/packages/node/src/integrations/tracing/langgraph/instrumentation.ts b/packages/node/src/integrations/tracing/langgraph/instrumentation.ts deleted file mode 100644 index b41bc4f16b65..000000000000 --- a/packages/node/src/integrations/tracing/langgraph/instrumentation.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { - InstrumentationBase, - type InstrumentationConfig, - type InstrumentationModuleDefinition, - InstrumentationNodeModuleDefinition, -} from '@opentelemetry/instrumentation'; -import { InstrumentationNodeModuleFile } from '../InstrumentationNodeModuleFile'; -import type { CompiledGraph, LangGraphOptions } from '@sentry/core'; -import { getClient, instrumentCreateReactAgent, instrumentStateGraph, SDK_VERSION } from '@sentry/core'; - -const supportedVersions = ['>=0.0.0 <2.0.0']; - -type LangGraphInstrumentationOptions = InstrumentationConfig & LangGraphOptions; - -/** - * Represents the patched shape of the LangGraph module export. - */ -interface PatchedModuleExports { - [key: string]: unknown; - StateGraph?: abstract new (...args: unknown[]) => unknown; - createReactAgent?: (...args: unknown[]) => CompiledGraph; -} - -/** - * Sentry LangGraph instrumentation using OpenTelemetry. - */ -export class SentryLangGraphInstrumentation extends InstrumentationBase { - public constructor(config: LangGraphInstrumentationOptions = {}) { - super('@sentry/instrumentation-langgraph', SDK_VERSION, config); - } - - /** - * Initializes the instrumentation by defining the modules to be patched. - */ - public init(): InstrumentationModuleDefinition[] { - return [ - new InstrumentationNodeModuleDefinition( - '@langchain/langgraph', - supportedVersions, - this._patch.bind(this), - exports => exports, - [ - new InstrumentationNodeModuleFile( - /** - * In CJS, LangGraph packages re-export from dist/index.cjs files. - * Patching only the root module sometimes misses the real implementation or - * gets overwritten when that file is loaded. We add a file-level patch so that - * _patch runs again on the concrete implementation - */ - '@langchain/langgraph/dist/index.cjs', - supportedVersions, - this._patch.bind(this), - exports => exports, - ), - new InstrumentationNodeModuleFile( - /** - * In CJS, the prebuilt submodule re-exports from dist/prebuilt/index.cjs. - * We add a file-level patch under the main module so that CJS require() - * of @langchain/langgraph/prebuilt gets patched. - */ - '@langchain/langgraph/dist/prebuilt/index.cjs', - supportedVersions, - this._patch.bind(this), - exports => exports, - ), - ], - ), - new InstrumentationNodeModuleDefinition( - '@langchain/langgraph/prebuilt', - supportedVersions, - this._patch.bind(this), - exports => exports, - [ - new InstrumentationNodeModuleFile( - /** - * In CJS, the prebuilt submodule re-exports from dist/prebuilt/index.cjs. - * We add file-level patches so _patch runs on the concrete implementation. - */ - '@langchain/langgraph/dist/prebuilt/index.cjs', - supportedVersions, - this._patch.bind(this), - exports => exports, - ), - ], - ), - ]; - } - - /** - * Core patch logic applying instrumentation to the LangGraph module. - */ - private _patch(exports: PatchedModuleExports): PatchedModuleExports | void { - const client = getClient(); - const genAI = client?.getDataCollectionOptions().genAI; - const options = { - ...this.getConfig(), - recordInputs: this.getConfig().recordInputs ?? genAI?.inputs ?? false, - recordOutputs: this.getConfig().recordOutputs ?? genAI?.outputs ?? false, - }; - - // Patch StateGraph.compile to instrument both compile() and invoke() - if (exports.StateGraph && typeof exports.StateGraph === 'function') { - instrumentStateGraph(exports.StateGraph.prototype as { compile: (...args: unknown[]) => unknown }, options); - } - - // Patch createReactAgent to instrument agent creation and invocation - if (exports.createReactAgent && typeof exports.createReactAgent === 'function') { - const originalCreateReactAgent = exports.createReactAgent; - Object.defineProperty(exports, 'createReactAgent', { - value: instrumentCreateReactAgent(originalCreateReactAgent as (...args: unknown[]) => CompiledGraph, options), - writable: true, - enumerable: true, - configurable: true, - }); - } - - return exports; - } -} diff --git a/packages/node/src/integrations/tracing/lrumemoizer/index.ts b/packages/node/src/integrations/tracing/lrumemoizer/index.ts deleted file mode 100644 index a1114e6495c4..000000000000 --- a/packages/node/src/integrations/tracing/lrumemoizer/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { LruMemoizerInstrumentation } from './vendored/instrumentation'; -import { generateInstrumentOnce } from '../../../otel/instrument'; - -const INTEGRATION_NAME = 'LruMemoizer' as const; - -export const instrumentLruMemoizer = generateInstrumentOnce(INTEGRATION_NAME, () => new LruMemoizerInstrumentation()); diff --git a/packages/node/src/integrations/tracing/lrumemoizer/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/lrumemoizer/vendored/instrumentation.ts deleted file mode 100644 index f1e600b7359a..000000000000 --- a/packages/node/src/integrations/tracing/lrumemoizer/vendored/instrumentation.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-lru-memoizer - * - Upstream version: @opentelemetry/instrumentation-lru-memoizer@0.62.0 - */ - -import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; -import { getCurrentScope, SDK_VERSION, withScope } from '@sentry/core'; - -const PACKAGE_NAME = '@sentry/instrumentation-lru-memoizer'; - -type Memoizer = (this: unknown, ...args: unknown[]) => unknown; -type LruMemoizerModule = ((this: unknown, ...args: unknown[]) => Memoizer) & { sync: unknown }; - -export class LruMemoizerInstrumentation extends InstrumentationBase { - constructor() { - super(PACKAGE_NAME, SDK_VERSION, {}); - } - - init(): InstrumentationNodeModuleDefinition[] { - return [ - new InstrumentationNodeModuleDefinition( - 'lru-memoizer', - ['>=1.3 <4'], - (moduleExports: LruMemoizerModule) => { - // moduleExports is a function which receives an options object, - // and returns a "memoizer" function upon invocation. - // We want to patch this "memoizer's" internal function - const asyncMemoizer = function (this: unknown, ...args: unknown[]): unknown { - // This following function is invoked every time the user wants to get a (possible) memoized value - // We replace it with another function in which we bind the current context to the last argument (callback) - const origMemoizer = moduleExports.apply(this, args) as Memoizer; - return function (this: unknown, ...memoizerArgs: unknown[]): unknown { - // last argument is the callback - const origCallback = memoizerArgs.pop(); - const scope = getCurrentScope(); - const callbackWithContext = - typeof origCallback === 'function' - ? function (this: unknown, ...callbackArgs: unknown[]): unknown { - return withScope(scope, () => (origCallback as Memoizer).apply(this, callbackArgs)); - } - : origCallback; - return origMemoizer.apply(this, [...memoizerArgs, callbackWithContext]); - }; - }; - - // sync function preserves context, but we still need to export it - // as the lru-memoizer package does - return Object.assign(asyncMemoizer, { sync: moduleExports.sync }); - }, - undefined, // no need to disable as this instrumentation does not create any spans - ), - ]; - } -} diff --git a/packages/node/src/integrations/tracing/mongo/index.ts b/packages/node/src/integrations/tracing/mongo/index.ts deleted file mode 100644 index 91a1a68ea548..000000000000 --- a/packages/node/src/integrations/tracing/mongo/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { MongoDBInstrumentation } from './vendored/instrumentation'; -import { generateInstrumentOnce } from '../../../otel/instrument'; - -const INTEGRATION_NAME = 'Mongo' as const; - -export const instrumentMongo = generateInstrumentOnce(INTEGRATION_NAME, () => new MongoDBInstrumentation()); diff --git a/packages/node/src/integrations/tracing/mongo/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/mongo/vendored/instrumentation.ts deleted file mode 100644 index 292ff2222425..000000000000 --- a/packages/node/src/integrations/tracing/mongo/vendored/instrumentation.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mongodb - * - Upstream version: @opentelemetry/instrumentation-mongodb@0.71.0 - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - * - Dropped the OTel connection-usage metrics (no Sentry MeterProvider consumes them) and the - * session/connect patches that existed only to feed them - * - Dropped the env-gated stable-semconv dual emission; only the (default) old semantic - * conventions are emitted, matching the previous default span output - */ - -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { SDK_VERSION } from '@sentry/core'; -import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile'; -import type { WireProtocolInternal } from './internal-types'; -import * as patches from './patches'; - -const PACKAGE_NAME = '@sentry/instrumentation-mongodb'; - -/** mongodb instrumentation plugin */ -export class MongoDBInstrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - public init(): InstrumentationNodeModuleDefinition[] { - const { v3PatchConnection, v3UnpatchConnection } = this._getV3ConnectionPatches(); - - const { v4PatchConnectionCallback, v4PatchConnectionPromise, v4UnpatchConnection } = this._getV4ConnectionPatches(); - const { v4PatchConnectionPool, v4UnpatchConnectionPool } = this._getV4ConnectionPoolPatches(); - - return [ - new InstrumentationNodeModuleDefinition('mongodb', ['>=3.3.0 <4'], undefined, undefined, [ - new InstrumentationNodeModuleFile( - 'mongodb/lib/core/wireprotocol/index.js', - ['>=3.3.0 <4'], - v3PatchConnection, - v3UnpatchConnection, - ), - ]), - new InstrumentationNodeModuleDefinition('mongodb', ['>=4.0.0 <8'], undefined, undefined, [ - new InstrumentationNodeModuleFile( - 'mongodb/lib/cmap/connection.js', - ['>=4.0.0 <6.4'], - v4PatchConnectionCallback, - v4UnpatchConnection, - ), - new InstrumentationNodeModuleFile( - 'mongodb/lib/cmap/connection.js', - ['>=6.4.0 <8'], - v4PatchConnectionPromise, - v4UnpatchConnection, - ), - new InstrumentationNodeModuleFile( - 'mongodb/lib/cmap/connection_pool.js', - ['>=4.0.0 <6.4'], - v4PatchConnectionPool, - v4UnpatchConnectionPool, - ), - ]), - ]; - } - - private _getV3ConnectionPatches() { - return { - v3PatchConnection: (moduleExports: T) => { - // patch insert operation - if (isWrapped(moduleExports.insert)) { - this._unwrap(moduleExports, 'insert'); - } - this._wrap(moduleExports, 'insert', patches.getV3PatchOperation('insert')); - // patch remove operation - if (isWrapped(moduleExports.remove)) { - this._unwrap(moduleExports, 'remove'); - } - this._wrap(moduleExports, 'remove', patches.getV3PatchOperation('remove')); - // patch update operation - if (isWrapped(moduleExports.update)) { - this._unwrap(moduleExports, 'update'); - } - this._wrap(moduleExports, 'update', patches.getV3PatchOperation('update')); - // patch other command - if (isWrapped(moduleExports.command)) { - this._unwrap(moduleExports, 'command'); - } - this._wrap(moduleExports, 'command', patches.getV3PatchCommand()); - // patch query - if (isWrapped(moduleExports.query)) { - this._unwrap(moduleExports, 'query'); - } - this._wrap(moduleExports, 'query', patches.getV3PatchFind()); - // patch get more operation on cursor - if (isWrapped(moduleExports.getMore)) { - this._unwrap(moduleExports, 'getMore'); - } - this._wrap(moduleExports, 'getMore', patches.getV3PatchCursor()); - return moduleExports; - }, - v3UnpatchConnection: (moduleExports?: T) => { - if (moduleExports === undefined) return; - this._unwrap(moduleExports, 'insert'); - this._unwrap(moduleExports, 'remove'); - this._unwrap(moduleExports, 'update'); - this._unwrap(moduleExports, 'command'); - this._unwrap(moduleExports, 'query'); - this._unwrap(moduleExports, 'getMore'); - }, - }; - } - - private _getV4ConnectionPoolPatches() { - return { - v4PatchConnectionPool: (moduleExports: any) => { - const poolPrototype = moduleExports.ConnectionPool.prototype; - - if (isWrapped(poolPrototype.checkOut)) { - this._unwrap(poolPrototype, 'checkOut'); - } - - this._wrap(poolPrototype, 'checkOut', patches.getV4ConnectionPoolCheckOut()); - return moduleExports; - }, - v4UnpatchConnectionPool: (moduleExports?: any) => { - if (moduleExports === undefined) return; - - this._unwrap(moduleExports.ConnectionPool.prototype, 'checkOut'); - }, - }; - } - - private _getV4ConnectionPatches() { - return { - v4PatchConnectionCallback: (moduleExports: any) => { - if (isWrapped(moduleExports.Connection.prototype.command)) { - this._unwrap(moduleExports.Connection.prototype, 'command'); - } - - this._wrap(moduleExports.Connection.prototype, 'command', patches.getV4PatchCommandCallback()); - return moduleExports; - }, - v4PatchConnectionPromise: (moduleExports: any) => { - if (isWrapped(moduleExports.Connection.prototype.command)) { - this._unwrap(moduleExports.Connection.prototype, 'command'); - } - - this._wrap(moduleExports.Connection.prototype, 'command', patches.getV4PatchCommandPromise()); - return moduleExports; - }, - v4UnpatchConnection: (moduleExports?: any) => { - if (moduleExports === undefined) return; - this._unwrap(moduleExports.Connection.prototype, 'command'); - }, - }; - } -} diff --git a/packages/node/src/integrations/tracing/mongo/vendored/internal-types.ts b/packages/node/src/integrations/tracing/mongo/vendored/internal-types.ts deleted file mode 100644 index 3a192ffa11d6..000000000000 --- a/packages/node/src/integrations/tracing/mongo/vendored/internal-types.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mongodb - * - Upstream version: @opentelemetry/instrumentation-mongodb@0.71.0 - * - Trimmed to the driver-internal types actually used by the instrumentation - */ - -export type MongoInternalCommand = { - findandmodify: boolean; - createIndexes: boolean; - count: boolean; - aggregate: boolean; - ismaster: boolean; - indexes?: unknown[]; - query?: Record; - limit?: number; - q?: Record; - u?: Record; -}; - -export type CursorState = { cmd: MongoInternalCommand } & Record; - -// https://github.com/mongodb/node-mongodb-native/blob/3.6/lib/core/wireprotocol/index.js -export type WireProtocolInternal = { - insert: ( - server: MongoInternalTopology, - ns: string, - ops: unknown[], - options: unknown | Function, - callback?: Function, - ) => unknown; - update: ( - server: MongoInternalTopology, - ns: string, - ops: unknown[], - options: unknown | Function, - callback?: Function, - ) => unknown; - remove: ( - server: MongoInternalTopology, - ns: string, - ops: unknown[], - options: unknown | Function, - callback?: Function, - ) => unknown; - killCursors: (server: MongoInternalTopology, ns: string, cursorState: CursorState, callback: Function) => unknown; - getMore: ( - server: MongoInternalTopology, - ns: string, - cursorState: CursorState, - batchSize: number, - options: unknown | Function, - callback?: Function, - ) => unknown; - query: ( - server: MongoInternalTopology, - ns: string, - cmd: MongoInternalCommand, - cursorState: CursorState, - options: unknown | Function, - callback?: Function, - ) => unknown; - command: ( - server: MongoInternalTopology, - ns: string, - cmd: MongoInternalCommand, - options: unknown | Function, - callback?: Function, - ) => unknown; -}; - -// https://github.com/mongodb/node-mongodb-native/blob/3.6/lib/topologies/server.js#L172 -// https://github.com/mongodb/node-mongodb-native/blob/2.2/lib/server.js#L174 -export type MongoInternalTopology = { - s?: { - // those are for mongodb@3 - options?: { - host?: string; - port?: number; - servername?: string; - }; - // those are for mongodb@2 - host?: string; - port?: number; - }; - // mongodb@3 with useUnifiedTopology option - description?: { - address?: string; - }; -}; - -export enum MongodbCommandType { - CREATE_INDEXES = 'createIndexes', - FIND_AND_MODIFY = 'findAndModify', - IS_MASTER = 'isMaster', - COUNT = 'count', - AGGREGATE = 'aggregate', - UNKNOWN = 'unknown', -} - -// https://github.com/mongodb/js-bson/blob/main/src/bson.ts -export type Document = { - [key: string]: any; -}; - -// https://github.com/mongodb/node-mongodb-native/blob/v6.4.0/src/utils.ts#L281 -export interface MongodbNamespace { - db: string; - collection?: string; -} - -export type V4Connection = { - command: Function; - // From version 6.4.0 the method does not expect a callback and returns a promise - // https://github.com/mongodb/node-mongodb-native/blob/v6.4.2/src/cmap/connection.ts - commandPromise( - ns: MongodbNamespace, - cmd: Document, - options: undefined | unknown, - // From v6.6.0 we have this new param which is a constructor function - // https://github.com/mongodb/node-mongodb-native/blob/v6.6.0/src/cmap/connection.ts#L588 - responseType: undefined | unknown, - ): Promise; - // Earlier versions expect a callback param and return void - // https://github.com/mongodb/node-mongodb-native/blob/v4.2.2/src/cmap/connection.ts - commandCallback(ns: MongodbNamespace, cmd: Document, options: undefined | unknown, callback: any): void; -}; - -// https://github.com/mongodb/node-mongodb-native/blob/v4.2.2/src/cmap/connection_pool.ts -export type V4ConnectionPool = { - // Instrumentation just cares about carrying the async context so - // types of callback params are not needed - checkOut: (callback: (error: any, connection: any) => void) => void; -}; diff --git a/packages/node/src/integrations/tracing/mongo/vendored/patches.ts b/packages/node/src/integrations/tracing/mongo/vendored/patches.ts deleted file mode 100644 index ea29755c32d5..000000000000 --- a/packages/node/src/integrations/tracing/mongo/vendored/patches.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mongodb - * - Upstream version: @opentelemetry/instrumentation-mongodb@0.71.0 - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - */ - -import { getActiveSpan, withActiveSpan } from '@sentry/core'; -import type { - CursorState, - MongodbNamespace, - MongoInternalCommand, - MongoInternalTopology, - V4Connection, - V4ConnectionPool, - WireProtocolInternal, -} from './internal-types'; -import { - getV3CommandOperation, - getV3SpanAttributes, - getV4SpanAttributes, - patchEnd, - shouldSkipInstrumentation, - startMongoSpan, -} from './utils'; - -/** Creates spans for v3 common operations (insert/update/remove). */ -export function getV3PatchOperation(operationName: 'insert' | 'update' | 'remove') { - return (original: WireProtocolInternal[typeof operationName]) => { - return function patchedServerCommand( - this: unknown, - server: MongoInternalTopology, - ns: string, - ops: unknown[], - options: unknown | Function, - callback?: Function, - ) { - const resultHandler = typeof options === 'function' ? options : callback; - if (shouldSkipInstrumentation() || typeof resultHandler !== 'function' || typeof ops !== 'object') { - if (typeof options === 'function') { - return original.call(this, server, ns, ops, options); - } else { - return original.call(this, server, ns, ops, options, callback); - } - } - - const span = startMongoSpan(getV3SpanAttributes(ns, server, ops[0] as any, operationName)); - - const patchedCallback = patchEnd(span, resultHandler); - // handle when options is the callback to send the correct number of args - if (typeof options === 'function') { - return original.call(this, server, ns, ops, patchedCallback); - } else { - return original.call(this, server, ns, ops, options, patchedCallback); - } - }; - }; -} - -/** Creates spans for the v3 command operation. */ -export function getV3PatchCommand() { - return (original: WireProtocolInternal['command']) => { - return function patchedServerCommand( - this: unknown, - server: MongoInternalTopology, - ns: string, - cmd: MongoInternalCommand, - options: unknown | Function, - callback?: Function, - ) { - const resultHandler = typeof options === 'function' ? options : callback; - - if (shouldSkipInstrumentation() || typeof resultHandler !== 'function' || typeof cmd !== 'object') { - if (typeof options === 'function') { - return original.call(this, server, ns, cmd, options); - } else { - return original.call(this, server, ns, cmd, options, callback); - } - } - - const operationName = getV3CommandOperation(cmd as unknown as Record); - const span = startMongoSpan(getV3SpanAttributes(ns, server, cmd, operationName)); - - const patchedCallback = patchEnd(span, resultHandler); - // handle when options is the callback to send the correct number of args - if (typeof options === 'function') { - return original.call(this, server, ns, cmd, patchedCallback); - } else { - return original.call(this, server, ns, cmd, options, patchedCallback); - } - }; - }; -} - -/** Creates spans for the v4 (<6.4) callback-style command operation. */ -export function getV4PatchCommandCallback() { - return (original: V4Connection['commandCallback']) => { - return function patchedV4ServerCommand( - this: any, - ns: MongodbNamespace, - cmd: any, - options: undefined | unknown, - callback: any, - ) { - const resultHandler = callback; - const commandType = Object.keys(cmd)[0]; - - if (typeof cmd !== 'object' || cmd.ismaster || cmd.hello) { - return original.call(this, ns, cmd, options, callback); - } - - let span = undefined; - if (!shouldSkipInstrumentation()) { - span = startMongoSpan(getV4SpanAttributes(this, ns, cmd, commandType)); - } - const patchedCallback = patchEnd(span, resultHandler); - - return original.call(this, ns, cmd, options, patchedCallback); - }; - }; -} - -/** Creates spans for the v4 (>=6.4) promise-style command operation. */ -export function getV4PatchCommandPromise() { - return (original: V4Connection['commandPromise']) => { - return function patchedV4ServerCommand(this: any, ...args: Parameters) { - const [ns, cmd] = args; - const commandType = Object.keys(cmd)[0]; - const resultHandler = () => undefined; - - if (typeof cmd !== 'object' || cmd.ismaster || cmd.hello) { - return original.apply(this, args); - } - - let span = undefined; - if (!shouldSkipInstrumentation()) { - span = startMongoSpan(getV4SpanAttributes(this, ns, cmd, commandType)); - } - - const patchedCallback = patchEnd(span, resultHandler); - - const result = original.apply(this, args); - result.then( - (res: any) => patchedCallback(null, res), - (err: any) => patchedCallback(err), - ); - - return result; - }; - }; -} - -/** Creates spans for the v3 find operation. */ -export function getV3PatchFind() { - return (original: WireProtocolInternal['query']) => { - return function patchedServerCommand( - this: unknown, - server: MongoInternalTopology, - ns: string, - cmd: MongoInternalCommand, - cursorState: CursorState, - options: unknown | Function, - callback?: Function, - ) { - const resultHandler = typeof options === 'function' ? options : callback; - - if (shouldSkipInstrumentation() || typeof resultHandler !== 'function' || typeof cmd !== 'object') { - if (typeof options === 'function') { - return original.call(this, server, ns, cmd, cursorState, options); - } else { - return original.call(this, server, ns, cmd, cursorState, options, callback); - } - } - - const span = startMongoSpan(getV3SpanAttributes(ns, server, cmd, 'find')); - - const patchedCallback = patchEnd(span, resultHandler); - // handle when options is the callback to send the correct number of args - if (typeof options === 'function') { - return original.call(this, server, ns, cmd, cursorState, patchedCallback); - } else { - return original.call(this, server, ns, cmd, cursorState, options, patchedCallback); - } - }; - }; -} - -/** Creates spans for the v3 getMore (cursor) operation. */ -export function getV3PatchCursor() { - return (original: WireProtocolInternal['getMore']) => { - return function patchedServerCommand( - this: unknown, - server: MongoInternalTopology, - ns: string, - cursorState: CursorState, - batchSize: number, - options: unknown | Function, - callback?: Function, - ) { - const resultHandler = typeof options === 'function' ? options : callback; - - if (shouldSkipInstrumentation() || typeof resultHandler !== 'function') { - if (typeof options === 'function') { - return original.call(this, server, ns, cursorState, batchSize, options); - } else { - return original.call(this, server, ns, cursorState, batchSize, options, callback); - } - } - - const span = startMongoSpan(getV3SpanAttributes(ns, server, cursorState.cmd, 'getMore')); - - const patchedCallback = patchEnd(span, resultHandler); - // handle when options is the callback to send the correct number of args - if (typeof options === 'function') { - return original.call(this, server, ns, cursorState, batchSize, patchedCallback); - } else { - return original.call(this, server, ns, cursorState, batchSize, options, patchedCallback); - } - }; - }; -} - -// This patch will become unnecessary once https://jira.mongodb.org/browse/NODE-5639 is done. -export function getV4ConnectionPoolCheckOut() { - return (original: V4ConnectionPool['checkOut']) => { - return function patchedCheckout(this: unknown, callback: (error: any, connection: any) => void) { - // The pool runs the callback in a detached context, so re-activate the span that was - // active when `checkOut` was called — otherwise the pooled operation finds no parent. - const parentSpan = getActiveSpan(); - return original.call(this, function (this: unknown, ...args: [any, any]) { - return withActiveSpan(parentSpan ?? null, () => callback.apply(this, args)); - }); - }; - }; -} diff --git a/packages/node/src/integrations/tracing/mongo/vendored/utils.ts b/packages/node/src/integrations/tracing/mongo/vendored/utils.ts deleted file mode 100644 index 22876deae063..000000000000 --- a/packages/node/src/integrations/tracing/mongo/vendored/utils.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mongodb - * - Upstream version: @opentelemetry/instrumentation-mongodb@0.71.0 - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - * - The db/net attribute extraction, `db.statement` scrubbing and span - * builder are shared with the orchestrion mongodb integration in - * `@sentry/server-utils` so the two emit an identical span shape. - * Only the OTel-specific callback/context helpers below remain here. - */ - -import type { Span, SpanAttributes } from '@sentry/core'; -import { getActiveSpan, SPAN_STATUS_ERROR, withActiveSpan } from '@sentry/core'; -import { - getV3CommandOperation, - getV3SpanAttributes as sharedGetV3SpanAttributes, - getV4SpanAttributes as sharedGetV4SpanAttributes, - startMongoSpan, -} from '@sentry/server-utils'; - -const ORIGIN = 'auto.db.otel.mongo'; - -export { getV3CommandOperation, startMongoSpan }; - -/** Determine a span's attributes from the v4 connection context (OTel origin). */ -export function getV4SpanAttributes(connectionCtx: any, ns: any, command?: any, operation?: string): SpanAttributes { - return sharedGetV4SpanAttributes(connectionCtx, ns, command, operation, ORIGIN); -} - -/** Determine a span's attributes from the v3 topology (OTel origin). */ -export function getV3SpanAttributes(ns: string, topology: any, command?: any, operation?: string): SpanAttributes { - return sharedGetV3SpanAttributes(ns, topology, command, operation, ORIGIN); -} - -/** - * Wraps the result handler so it ends the span (with error status on failure) and runs the - * original callback re-activated under the parent span — mongodb loses the async context when - * it invokes the callback on a later tick. - */ -export function patchEnd(span: Span | undefined, resultHandler: Function): Function { - const parentSpan = getActiveSpan(); - let spanEnded = false; - - return function patchedEnd(this: {}, ...args: unknown[]) { - if (!spanEnded) { - spanEnded = true; - const error = args[0]; - if (span) { - if (error instanceof Error) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: error.message }); - } - span.end(); - } - } - - return withActiveSpan(parentSpan ?? null, () => resultHandler.apply(this, args)); - }; -} - -// The instrumentation only creates spans when there is an active parent span, to avoid emitting -// orphaned mongodb spans. -export function shouldSkipInstrumentation(): boolean { - return !getActiveSpan(); -} diff --git a/packages/node/src/integrations/tracing/mongoose/index.ts b/packages/node/src/integrations/tracing/mongoose/index.ts deleted file mode 100644 index fe18a73490b4..000000000000 --- a/packages/node/src/integrations/tracing/mongoose/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { MongooseInstrumentation } from './vendored/mongoose'; -import { generateInstrumentOnce } from '../../../otel/instrument'; - -const INTEGRATION_NAME = 'Mongoose' as const; - -export const instrumentMongoose = generateInstrumentOnce(INTEGRATION_NAME, () => new MongooseInstrumentation()); diff --git a/packages/node/src/integrations/tracing/mongoose/vendored/mongoose-types.ts b/packages/node/src/integrations/tracing/mongoose/vendored/mongoose-types.ts deleted file mode 100644 index 0d1753007b70..000000000000 --- a/packages/node/src/integrations/tracing/mongoose/vendored/mongoose-types.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Simplified type definitions vendored from mongoose. - * Only includes the types actually accessed by the instrumentation. - */ - -export interface Collection { - name: string; - conn: Connection; - [key: string]: any; -} - -export interface Connection { - name: string; - host: string; - port: number; - user?: string; - [key: string]: any; -} - -export declare const Model: { - prototype: any; - collection: Collection; - modelName: string; - aggregate: Function; - insertMany: Function; - bulkWrite: Function; - [key: string]: any; -}; - -export declare const Query: { - prototype: any; - [key: string]: any; -}; - -export declare const Aggregate: { - prototype: any; - [key: string]: any; -}; - -export interface MongooseError extends Error { - code?: number; -} diff --git a/packages/node/src/integrations/tracing/mongoose/vendored/mongoose.ts b/packages/node/src/integrations/tracing/mongoose/vendored/mongoose.ts deleted file mode 100644 index 579408acf6c5..000000000000 --- a/packages/node/src/integrations/tracing/mongoose/vendored/mongoose.ts +++ /dev/null @@ -1,333 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mongoose - * - Upstream version: @opentelemetry/instrumentation-mongoose@0.64.0 - * - Types vendored from mongoose as simplified interfaces - * - Minor TypeScript strictness adjustments for this repository's compiler settings - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - */ - -import { - type InstrumentationConfig, - InstrumentationBase, - type InstrumentationModuleDefinition, - InstrumentationNodeModuleDefinition, -} from '@opentelemetry/instrumentation'; -import type { Span } from '@sentry/core'; -import { getActiveSpan, SDK_VERSION, withActiveSpan } from '@sentry/core'; -import { startMongooseLegacySpan } from '@sentry/server-utils'; -import type * as mongoose from './mongoose-types'; -import { handleCallbackResponse, handlePromiseResponse } from './utils'; - -const PACKAGE_NAME = '@sentry/instrumentation-mongoose'; -const ORIGIN = 'auto.db.otel.mongoose'; - -type MongooseModuleExports = typeof mongoose; - -// The raw imported `mongoose` module: either the CJS object itself, or an ESM -// namespace wrapper exposing the same shape under `.default`. -type MongooseModule = MongooseModuleExports & { - default?: MongooseModuleExports; - [Symbol.toStringTag]?: string; -}; - -const contextCaptureFunctionsCommon = [ - 'deleteOne', - 'deleteMany', - 'find', - 'findOne', - 'estimatedDocumentCount', - 'countDocuments', - 'distinct', - 'where', - '$where', - 'findOneAndUpdate', - 'findOneAndDelete', - 'findOneAndReplace', -]; - -const contextCaptureFunctions6 = ['remove', 'count', 'findOneAndRemove', ...contextCaptureFunctionsCommon]; -const contextCaptureFunctions7 = ['count', 'findOneAndRemove', ...contextCaptureFunctionsCommon]; -const contextCaptureFunctions8 = [...contextCaptureFunctionsCommon]; - -function getContextCaptureFunctions(moduleVersion: string | undefined): string[] { - /* istanbul ignore next */ - if (!moduleVersion) { - return contextCaptureFunctionsCommon; - } else if (moduleVersion.startsWith('6.') || moduleVersion.startsWith('5.')) { - return contextCaptureFunctions6; - } else if (moduleVersion.startsWith('7.')) { - return contextCaptureFunctions7; - } else { - return contextCaptureFunctions8; - } -} - -function instrumentRemove(moduleVersion: string | undefined): boolean { - return (moduleVersion && (moduleVersion.startsWith('5.') || moduleVersion.startsWith('6.'))) || false; -} - -/** - * 8.21.0 changed Document.updateOne/deleteOne so that the Query is not fully built when Query.exec() is called. - * @param moduleVersion - */ -function needsDocumentMethodPatch(moduleVersion: string | undefined): boolean { - if (!moduleVersion || !moduleVersion.startsWith('8.')) { - return false; - } - - const minor = parseInt(moduleVersion.split('.')[1]!, 10); - return minor >= 21; -} - -// when mongoose functions are called, we store the original call context -// and then set it as the parent for the spans created by Query/Aggregate exec() -// calls. this bypass the unlinked spans issue on thenables await operations. -export const _STORED_PARENT_SPAN: unique symbol = Symbol('stored-parent-span'); - -// Prevents double-instrumentation when doc.updateOne/deleteOne (Mongoose 8.21.0+) -// creates a span and returns a Query that also calls exec() -export const _ALREADY_INSTRUMENTED: unique symbol = Symbol('already-instrumented'); - -export class MongooseInstrumentation extends InstrumentationBase { - constructor(config: InstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - protected init(): InstrumentationModuleDefinition { - const module = new InstrumentationNodeModuleDefinition( - 'mongoose', - // mongoose >= 9.7.0 publishes via diagnostics_channel and is instrumented by - // `subscribeMongooseDiagnosticChannels` instead, so this IITM patcher must not - // overlap it — otherwise every operation would emit two mongoose spans. - ['>=5.9.7 <9.7.0'], - this.patch.bind(this), - this.unpatch.bind(this), - ); - return module; - } - - private patch(module: MongooseModule, moduleVersion: string | undefined) { - const moduleExports: MongooseModuleExports = - module[Symbol.toStringTag] === 'Module' && module.default ? module.default : module; - - this._wrap(moduleExports.Model.prototype, 'save', this.patchOnModelMethods('save')); - // mongoose applies this code on module require: - // Model.prototype.$save = Model.prototype.save; - // which captures the save function before it is patched. - // so we need to apply the same logic after instrumenting the save function. - moduleExports.Model.prototype.$save = moduleExports.Model.prototype.save; - - if (instrumentRemove(moduleVersion)) { - this._wrap(moduleExports.Model.prototype, 'remove', this.patchOnModelMethods('remove')); - } - - // Mongoose 8.21.0+ changed Document.updateOne()/deleteOne() so that the Query is not fully built when Query.exec() is called. - if (needsDocumentMethodPatch(moduleVersion)) { - this._wrap(moduleExports.Model.prototype, 'updateOne', this._patchDocumentUpdateMethods('updateOne')); - this._wrap(moduleExports.Model.prototype, 'deleteOne', this._patchDocumentUpdateMethods('deleteOne')); - } - - this._wrap(moduleExports.Query.prototype, 'exec', this.patchQueryExec()); - this._wrap(moduleExports.Aggregate.prototype, 'exec', this.patchAggregateExec()); - - const contextCaptureFunctions = getContextCaptureFunctions(moduleVersion); - - contextCaptureFunctions.forEach((funcName: string) => { - this._wrap(moduleExports.Query.prototype, funcName as any, this.patchAndCaptureSpanContext(funcName)); - }); - this._wrap(moduleExports.Model, 'aggregate', this.patchModelAggregate()); - - this._wrap(moduleExports.Model, 'insertMany', this.patchModelStatic('insertMany')); - this._wrap(moduleExports.Model, 'bulkWrite', this.patchModelStatic('bulkWrite')); - - return moduleExports; - } - - private unpatch(module: MongooseModule, moduleVersion: string | undefined): void { - const moduleExports: MongooseModuleExports = - module[Symbol.toStringTag] === 'Module' && module.default ? module.default : module; - - const contextCaptureFunctions = getContextCaptureFunctions(moduleVersion); - - this._unwrap(moduleExports.Model.prototype, 'save'); - // revert the patch for $save which we applied by aliasing it to patched `save` - moduleExports.Model.prototype.$save = moduleExports.Model.prototype.save; - - if (instrumentRemove(moduleVersion)) { - this._unwrap(moduleExports.Model.prototype, 'remove'); - } - - if (needsDocumentMethodPatch(moduleVersion)) { - this._unwrap(moduleExports.Model.prototype, 'updateOne'); - this._unwrap(moduleExports.Model.prototype, 'deleteOne'); - } - - this._unwrap(moduleExports.Query.prototype, 'exec'); - this._unwrap(moduleExports.Aggregate.prototype, 'exec'); - - contextCaptureFunctions.forEach((funcName: string) => { - this._unwrap(moduleExports.Query.prototype, funcName as any); - }); - this._unwrap(moduleExports.Model, 'aggregate'); - - this._unwrap(moduleExports.Model, 'insertMany'); - this._unwrap(moduleExports.Model, 'bulkWrite'); - } - - private patchAggregateExec() { - const self = this; - return (originalAggregate: Function) => { - return function exec(this: any, callback?: Function) { - const parentSpan = this[_STORED_PARENT_SPAN]; - const span = startMongooseLegacySpan({ - collection: this._model.collection, - modelName: this._model?.modelName, - operation: 'aggregate', - origin: ORIGIN, - parentSpan, - }); - - return self._handleResponse(span, originalAggregate, this, arguments, callback); - }; - }; - } - - private patchQueryExec() { - const self = this; - return (originalExec: Function) => { - return function exec(this: any, callback?: Function) { - // Skip if already instrumented by document instance method patch - if (this[_ALREADY_INSTRUMENTED]) { - return originalExec.apply(this, arguments); - } - - const parentSpan = this[_STORED_PARENT_SPAN]; - const span = startMongooseLegacySpan({ - collection: this.mongooseCollection, - modelName: this.model.modelName, - operation: this.op, - origin: ORIGIN, - parentSpan, - }); - - return self._handleResponse(span, originalExec, this, arguments, callback); - }; - }; - } - - private patchOnModelMethods(op: string) { - const self = this; - return (originalOnModelFunction: Function) => { - return function method(this: any, options?: any, callback?: Function) { - const span = startMongooseLegacySpan({ - collection: this.constructor.collection, - modelName: this.constructor.modelName, - operation: op, - origin: ORIGIN, - }); - - if (options instanceof Function) { - // oxlint-disable-next-line no-param-reassign - callback = options; - } - - return self._handleResponse(span, originalOnModelFunction, this, arguments, callback); - }; - }; - } - - // Patch document instance methods (doc.updateOne/deleteOne) for Mongoose 8.21.0+. - private _patchDocumentUpdateMethods(op: string) { - const self = this; - return (originalMethod: Function) => { - return function method(this: any, update?: any, options?: any, callback?: Function) { - // determine actual callback since different argument patterns are allowed - let actualCallback: Function | undefined = callback; - if (typeof update === 'function') { - actualCallback = update; - } else if (typeof options === 'function') { - actualCallback = options; - } - - const span = startMongooseLegacySpan({ - collection: this.constructor.collection, - modelName: this.constructor.modelName, - operation: op, - origin: ORIGIN, - }); - - const result = self._handleResponse(span, originalMethod, this, arguments, actualCallback); - - // Mark returned Query to prevent double-instrumentation when exec() is eventually called - if (result && typeof result === 'object') { - result[_ALREADY_INSTRUMENTED] = true; - } - - return result; - }; - }; - } - - private patchModelStatic(op: string) { - const self = this; - return (original: Function) => { - return function patchedStatic(this: any, docsOrOps: any, options?: any, callback?: Function) { - if (typeof options === 'function') { - // oxlint-disable-next-line no-param-reassign - callback = options; - } - - const span = startMongooseLegacySpan({ - collection: this.collection, - modelName: this.modelName, - operation: op, - origin: ORIGIN, - }); - - return self._handleResponse(span, original, this, arguments, callback); - }; - }; - } - - // we want to capture the otel span on the object which is calling exec. - // in the special case of aggregate, we need have no function to path - // on the Aggregate object to capture the context on, so we patch - // the aggregate of Model, and set the context on the Aggregate object - private patchModelAggregate() { - return (original: Function) => { - return function captureSpanContext(this: any) { - const currentSpan = getActiveSpan(); - const aggregate = original.apply(this, arguments); - if (aggregate) aggregate[_STORED_PARENT_SPAN] = currentSpan; - return aggregate; - }; - }; - } - - private patchAndCaptureSpanContext(_funcName: string) { - return (original: Function) => { - return function captureSpanContext(this: any) { - this[_STORED_PARENT_SPAN] = getActiveSpan(); - return original.apply(this, arguments); - }; - }; - } - - private _handleResponse(span: Span, exec: Function, originalThis: any, args: IArguments, callback?: Function) { - // Activate the span while the underlying operation runs so that nested instrumentation - // (e.g. the mongodb driver spans) is parented to this span. `withActiveSpan` returns the - // callback's result untouched, so lazy mongoose Query thenables are handed back unexecuted. - return withActiveSpan(span, () => { - if (callback instanceof Function) { - return handleCallbackResponse(callback, exec, originalThis, span, args); - } else { - const response = exec.apply(originalThis, args); - return handlePromiseResponse(response, span); - } - }); - } -} diff --git a/packages/node/src/integrations/tracing/mongoose/vendored/utils.ts b/packages/node/src/integrations/tracing/mongoose/vendored/utils.ts deleted file mode 100644 index ea5aeb259a8f..000000000000 --- a/packages/node/src/integrations/tracing/mongoose/vendored/utils.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mongoose - * - Upstream version: @opentelemetry/instrumentation-mongoose@0.64.0 - * - Types vendored from mongoose as simplified interfaces - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - */ - -import type { Span } from '@sentry/core'; -import { SPAN_STATUS_ERROR } from '@sentry/core'; -import type { MongooseError } from './mongoose-types'; - -function setErrorStatus(span: Span, error: MongooseError): void { - span.setStatus({ - code: SPAN_STATUS_ERROR, - message: `${error.message} ${error.code ? `\nMongoose Error Code: ${error.code}` : ''}`, - }); -} - -export function handlePromiseResponse(execResponse: any, span: Span): any { - if (!(execResponse instanceof Promise)) { - span.end(); - return execResponse; - } - - return execResponse - .catch((err: any) => { - setErrorStatus(span, err); - throw err; - }) - .finally(() => span.end()); -} - -export function handleCallbackResponse( - callback: Function, - exec: Function, - originalThis: any, - span: Span, - args: IArguments, -) { - let callbackArgumentIndex = 0; - if (args.length === 2) { - callbackArgumentIndex = 1; - } else if (args.length === 3) { - callbackArgumentIndex = 2; - } - - args[callbackArgumentIndex] = (err: Error, response: any): any => { - if (err) { - setErrorStatus(span, err); - } - - span.end(); - return callback(err, response); - }; - - return exec.apply(originalThis, args); -} diff --git a/packages/node/src/integrations/tracing/mysql/index.ts b/packages/node/src/integrations/tracing/mysql/index.ts deleted file mode 100644 index 1691bfd10ef2..000000000000 --- a/packages/node/src/integrations/tracing/mysql/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { MySQLInstrumentation } from './vendored/instrumentation'; -import { generateInstrumentOnce } from '../../../otel/instrument'; - -const INTEGRATION_NAME = 'Mysql' as const; - -export const instrumentMysql = generateInstrumentOnce(INTEGRATION_NAME, () => new MySQLInstrumentation({})); diff --git a/packages/node/src/integrations/tracing/mysql/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/mysql/vendored/instrumentation.ts deleted file mode 100644 index 56ae9c3030de..000000000000 --- a/packages/node/src/integrations/tracing/mysql/vendored/instrumentation.ts +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mysql - * - Upstream version: @opentelemetry/instrumentation-mysql@0.64.0 - * - Types from the `mysql` package inlined as simplified interfaces - * - Minor TypeScript strictness adjustments for this repository's compiler settings - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs, and the connection-pool - * metrics / `enhancedDatabaseReporting` option were removed (unused by the Sentry SDK) - */ - -import type { Span } from '@opentelemetry/api'; -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import type { Scope, SpanAttributes } from '@sentry/core'; -import { - bindScopeToEmitter, - getCurrentScope, - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startInactiveSpan, - withActiveSpan, - withScope, -} from '@sentry/core'; -import { - DB_NAME, - DB_STATEMENT, - DB_SYSTEM, - DB_USER, - NET_PEER_NAME, - NET_PEER_PORT, - SENTRY_KIND, -} from '@sentry/conventions/attributes'; -import type * as mysqlTypes from './mysql-types'; -import { ATTR_DB_CONNECTION_STRING, DB_SYSTEM_VALUE_MYSQL } from './semconv'; -import { getConfig, getDbQueryText, getJDBCString, getSpanName } from './utils'; - -const PACKAGE_NAME = '@sentry/instrumentation-mysql'; -const ORIGIN = 'auto.db.otel.mysql'; - -type getConnectionCallbackType = (err: mysqlTypes.MysqlError, connection: mysqlTypes.PoolConnection) => void; - -export class MySQLInstrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - protected init() { - return [ - new InstrumentationNodeModuleDefinition( - 'mysql', - ['>=2.0.0 <3'], - (moduleExports: typeof mysqlTypes) => { - if (isWrapped(moduleExports.createConnection)) { - this._unwrap(moduleExports, 'createConnection'); - } - // oxlint-disable-next-line typescript/no-explicit-any - this._wrap(moduleExports, 'createConnection', this._patchCreateConnection() as any); - - if (isWrapped(moduleExports.createPool)) { - this._unwrap(moduleExports, 'createPool'); - } - // oxlint-disable-next-line typescript/no-explicit-any - this._wrap(moduleExports, 'createPool', this._patchCreatePool() as any); - - if (isWrapped(moduleExports.createPoolCluster)) { - this._unwrap(moduleExports, 'createPoolCluster'); - } - // oxlint-disable-next-line typescript/no-explicit-any - this._wrap(moduleExports, 'createPoolCluster', this._patchCreatePoolCluster() as any); - - return moduleExports; - }, - (moduleExports: typeof mysqlTypes) => { - if (moduleExports === undefined) return; - this._unwrap(moduleExports, 'createConnection'); - this._unwrap(moduleExports, 'createPool'); - this._unwrap(moduleExports, 'createPoolCluster'); - }, - ), - ]; - } - - // global export function - private _patchCreateConnection() { - return (originalCreateConnection: Function) => { - // oxlint-disable-next-line typescript/no-this-alias - const thisPlugin = this; - - return function createConnection(_connectionUri: string | mysqlTypes.ConnectionConfig) { - const originalResult = originalCreateConnection(...arguments); - - // This is unwrapped on next call after unpatch - // oxlint-disable-next-line typescript/no-explicit-any - thisPlugin._wrap(originalResult, 'query', thisPlugin._patchQuery(originalResult) as any); - - return originalResult; - }; - }; - } - - // global export function - private _patchCreatePool() { - return (originalCreatePool: Function) => { - // oxlint-disable-next-line typescript/no-this-alias - const thisPlugin = this; - return function createPool(_config: string | mysqlTypes.PoolConfig) { - const pool = originalCreatePool(...arguments); - - thisPlugin._wrap(pool, 'query', thisPlugin._patchQuery(pool)); - thisPlugin._wrap(pool, 'getConnection', thisPlugin._patchGetConnection(pool)); - - return pool; - }; - }; - } - - // global export function - private _patchCreatePoolCluster() { - return (originalCreatePoolCluster: Function) => { - // oxlint-disable-next-line typescript/no-this-alias - const thisPlugin = this; - return function createPool(_config: string | mysqlTypes.PoolConfig) { - const cluster = originalCreatePoolCluster(...arguments); - - // This is unwrapped on next call after unpatch - thisPlugin._wrap(cluster, 'getConnection', thisPlugin._patchGetConnection(cluster)); - - return cluster; - }; - }; - } - - // method on cluster or pool - private _patchGetConnection(pool: mysqlTypes.Pool | mysqlTypes.PoolCluster) { - return (originalGetConnection: Function) => { - // oxlint-disable-next-line typescript/no-this-alias - const thisPlugin = this; - - return function getConnection(arg1?: unknown, arg2?: unknown, arg3?: unknown) { - // Unwrap if unpatch has been called - if (!thisPlugin['_enabled']) { - thisPlugin._unwrap(pool, 'getConnection'); - return originalGetConnection.apply(pool, arguments); - } - - if (arguments.length === 1 && typeof arg1 === 'function') { - const patchFn = thisPlugin._getConnectionCallbackPatchFn(arg1 as getConnectionCallbackType); - return originalGetConnection.call(pool, patchFn); - } - if (arguments.length === 2 && typeof arg2 === 'function') { - const patchFn = thisPlugin._getConnectionCallbackPatchFn(arg2 as getConnectionCallbackType); - return originalGetConnection.call(pool, arg1, patchFn); - } - if (arguments.length === 3 && typeof arg3 === 'function') { - const patchFn = thisPlugin._getConnectionCallbackPatchFn(arg3 as getConnectionCallbackType); - return originalGetConnection.call(pool, arg1, arg2, patchFn); - } - - return originalGetConnection.apply(pool, arguments); - }; - }; - } - - private _getConnectionCallbackPatchFn(cb: getConnectionCallbackType) { - // oxlint-disable-next-line typescript/no-this-alias - const thisPlugin = this; - const scope = getCurrentScope(); - return function (this: unknown, err: mysqlTypes.MysqlError, connection: mysqlTypes.PoolConnection) { - if (connection) { - // this is the callback passed into a query - // no need to unwrap - if (!isWrapped(connection.query)) { - // oxlint-disable-next-line typescript/no-explicit-any - thisPlugin._wrap(connection, 'query', thisPlugin._patchQuery(connection) as any); - } - } - if (typeof cb === 'function') { - withScope(scope, () => cb.call(this, err, connection)); - } - }; - } - - private _patchQuery(connection: mysqlTypes.Connection | mysqlTypes.Pool) { - return (originalQuery: Function) => { - // oxlint-disable-next-line typescript/no-this-alias - const thisPlugin = this; - - return function query( - query: string | mysqlTypes.Query | mysqlTypes.QueryOptions, - _valuesOrCallback?: unknown[] | mysqlTypes.queryCallback, - _callback?: mysqlTypes.queryCallback, - ) { - if (!thisPlugin['_enabled']) { - thisPlugin._unwrap(connection, 'query'); - return originalQuery.apply(connection, arguments); - } - - const { host, port, database, user } = getConfig(connection.config); - const portNumber = parseInt(String(port), 10); - /* eslint-disable @sentry/no-deprecated-attributes */ - const attributes: SpanAttributes = { - [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [DB_SYSTEM]: DB_SYSTEM_VALUE_MYSQL, - [ATTR_DB_CONNECTION_STRING]: getJDBCString(host, port, database), - [DB_NAME]: database, - [DB_USER]: user, - [DB_STATEMENT]: getDbQueryText(query), - [NET_PEER_NAME]: host, - }; - - if (!isNaN(portNumber)) { - attributes[NET_PEER_PORT] = portNumber; - } - /* eslint-enable @sentry/no-deprecated-attributes */ - - const span = startInactiveSpan({ - name: getSpanName(query), - attributes, - }); - - const cbIndex = Array.from(arguments).findIndex(arg => typeof arg === 'function'); - - const scope = getCurrentScope(); - if (cbIndex === -1) { - const streamableQuery: mysqlTypes.Query = withActiveSpan(span, () => { - return originalQuery.apply(connection, arguments); - }); - bindScopeToEmitter(streamableQuery, scope); - - return streamableQuery - .on('error', (err: unknown) => { - span.setStatus({ - code: SPAN_STATUS_ERROR, - message: (err as mysqlTypes.MysqlError).message, - }); - }) - .on('end', () => { - span.end(); - }); - } else { - thisPlugin._wrap(arguments, cbIndex, thisPlugin._patchCallbackQuery(span, scope)); - - return withActiveSpan(span, () => { - return originalQuery.apply(connection, arguments); - }); - } - }; - }; - } - - private _patchCallbackQuery(span: Span, scope: Scope) { - return (originalCallback: Function) => { - return function (err: mysqlTypes.MysqlError | null, _results?: unknown, _fields?: mysqlTypes.FieldInfo[]) { - if (err) { - span.setStatus({ - code: SPAN_STATUS_ERROR, - message: err.message, - }); - } - span.end(); - return withScope(scope, () => originalCallback(...arguments)); - }; - }; - } -} diff --git a/packages/node/src/integrations/tracing/mysql/vendored/mysql-types.ts b/packages/node/src/integrations/tracing/mysql/vendored/mysql-types.ts deleted file mode 100644 index ee72492137e4..000000000000 --- a/packages/node/src/integrations/tracing/mysql/vendored/mysql-types.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mysql - * - Upstream version: @opentelemetry/instrumentation-mysql@0.64.0 - * - Simplified type definitions inlined from the `mysql` package to avoid requiring it as a dependency - */ - -export interface MysqlError extends Error { - code: string; - errno: number; - fatal: boolean; - sql?: string; - sqlState?: string; - sqlMessage?: string; - [key: string]: unknown; -} - -export interface ConnectionConfig { - host?: string; - port?: number; - database?: string; - user?: string; - password?: string; - [key: string]: unknown; -} - -export interface PoolConfig extends ConnectionConfig { - connectionConfig?: ConnectionConfig; - [key: string]: unknown; -} - -export interface QueryOptions { - sql: string; - values?: unknown; - [key: string]: unknown; -} - -export interface FieldInfo { - catalog: string; - db: string; - table: string; - orgTable: string; - name: string; - orgName: string; - charsetNr: number; - length: number; - type: number; - [key: string]: unknown; -} - -export type queryCallback = (err: MysqlError | null, results?: unknown, fields?: FieldInfo[]) => void; - -export interface Query { - sql: string; - values?: unknown; - on(event: string, listener: (...args: unknown[]) => void): this; - [key: string]: unknown; -} - -export type QueryFunction = { - (query: string | QueryOptions, callback?: queryCallback): Query; - (query: string | QueryOptions, values?: unknown, callback?: queryCallback): Query; -}; - -export interface Connection { - config: ConnectionConfig; - query: QueryFunction; - [key: string]: unknown; -} - -export interface PoolConnection extends Connection { - release(): void; - [key: string]: unknown; -} - -export interface Pool { - config: PoolConfig; - query: QueryFunction; - getConnection(callback: (err: MysqlError, connection: PoolConnection) => void): void; - end(callback?: (err?: MysqlError) => void): void; - on(event: string, listener: (...args: unknown[]) => void): this; - [key: string]: unknown; -} - -export interface PoolCluster { - getConnection(callback: (err: MysqlError, connection: PoolConnection) => void): void; - getConnection(pattern: string, callback: (err: MysqlError, connection: PoolConnection) => void): void; - getConnection( - pattern: string, - selector: string, - callback: (err: MysqlError, connection: PoolConnection) => void, - ): void; - add(config: PoolConfig): void; - add(id: string, config: PoolConfig): void; - [key: string]: unknown; -} - -export declare function createConnection(connectionUri: string | ConnectionConfig): Connection; -export declare function createPool(config: string | PoolConfig): Pool; -export declare function createPoolCluster(config?: PoolConfig): PoolCluster; diff --git a/packages/node/src/integrations/tracing/mysql/vendored/semconv.ts b/packages/node/src/integrations/tracing/mysql/vendored/semconv.ts deleted file mode 100644 index 9bbb61510e4b..000000000000 --- a/packages/node/src/integrations/tracing/mysql/vendored/semconv.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mysql - * - Upstream version: @opentelemetry/instrumentation-mysql@0.64.0 - */ - -/* - * This file contains a copy of unstable semantic convention definitions - * used by this package. - * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv - */ - -/** - * Deprecated, use `server.address`, `server.port` attributes instead. - * - * @example "Server=(localdb)\\v11.0;Integrated Security=true;" - * - * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - * - * @deprecated Replaced by `server.address` and `server.port`. - */ -export const ATTR_DB_CONNECTION_STRING = 'db.connection_string' as const; - -/** - * Enum value "mysql" for attribute `db.system`. - * - * MySQL - * - * @experimental This enum value is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - */ -export const DB_SYSTEM_VALUE_MYSQL = 'mysql' as const; diff --git a/packages/node/src/integrations/tracing/mysql/vendored/utils.ts b/packages/node/src/integrations/tracing/mysql/vendored/utils.ts deleted file mode 100644 index 24e91b9ed44a..000000000000 --- a/packages/node/src/integrations/tracing/mysql/vendored/utils.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mysql - * - Upstream version: @opentelemetry/instrumentation-mysql@0.64.0 - * - Types from the `mysql` package inlined as simplified interfaces - */ - -import type { ConnectionConfig, PoolConfig, Query, QueryOptions } from './mysql-types'; - -export function getConfig(config: ConnectionConfig | PoolConfig | undefined) { - const resolved = (config as PoolConfig | undefined)?.connectionConfig || config || {}; - const { host, port, database, user } = resolved; - return { host, port, database, user }; -} - -export function getJDBCString(host: string | undefined, port: number | undefined, database: string | undefined) { - let jdbcString = `jdbc:mysql://${host || 'localhost'}`; - - if (typeof port === 'number') { - jdbcString += `:${port}`; - } - - if (typeof database === 'string') { - jdbcString += `/${database}`; - } - - return jdbcString; -} - -/** - * @returns the database query being executed. - */ -export function getDbQueryText(query: string | Query | QueryOptions): string { - if (typeof query === 'string') { - return query; - } else { - return query.sql; - } -} - -/** - * The span name SHOULD be set to a low cardinality value - * representing the statement executed on the database. - * - * TODO: revisit span name based on https://github.com/open-telemetry/semantic-conventions/blob/v1.33.0/docs/database/database-spans.md#name - * - * @returns SQL statement without variable arguments or SQL verb - */ -export function getSpanName(query: string | Query | QueryOptions): string { - const rawQuery = typeof query === 'object' ? query.sql : query; - // Extract the SQL verb - const firstSpace = rawQuery?.indexOf(' '); - if (typeof firstSpace === 'number' && firstSpace !== -1) { - return rawQuery?.substring(0, firstSpace); - } - return rawQuery; -} diff --git a/packages/node/src/integrations/tracing/mysql2/index.ts b/packages/node/src/integrations/tracing/mysql2/index.ts deleted file mode 100644 index f2c4489e2230..000000000000 --- a/packages/node/src/integrations/tracing/mysql2/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { MySQL2Instrumentation } from './vendored/instrumentation'; -import { generateInstrumentOnce } from '../../../otel/instrument'; - -const INTEGRATION_NAME = 'Mysql2' as const; - -export const instrumentMysql2 = generateInstrumentOnce(INTEGRATION_NAME, () => new MySQL2Instrumentation()); diff --git a/packages/node/src/integrations/tracing/mysql2/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/mysql2/vendored/instrumentation.ts deleted file mode 100644 index c7c0e7e73746..000000000000 --- a/packages/node/src/integrations/tracing/mysql2/vendored/instrumentation.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mysql2 - * - Upstream version: @opentelemetry/instrumentation-mysql2@0.64.0 - * - Types from 'mysql2' inlined as simplified interfaces - * - Minor TypeScript strictness adjustments for this repository's compiler settings - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - */ - -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import { DB_STATEMENT, DB_SYSTEM, SENTRY_KIND } from '@sentry/conventions/attributes'; -import type { SpanAttributes } from '@sentry/core'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan } from '@sentry/core'; -import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile'; -import type { Connection, FormatFunction, Query, QueryError, QueryOptions } from './mysql2-types'; -import { DB_SYSTEM_VALUE_MYSQL } from './semconv'; -import { getConnectionAttributes, getConnectionPrototypeToInstrument, getQueryText, getSpanName, once } from './utils'; - -const PACKAGE_NAME = '@sentry/instrumentation-mysql2'; -const ORIGIN = 'auto.db.otel.mysql2'; - -// mysql2 >= 3.20.0 publishes via diagnostics_channel and is instrumented by -// `subscribeMysql2DiagnosticChannels` instead, so this IITM patcher must not -// overlap it — otherwise every query would emit two mysql2 spans. -const supportedVersions = ['>=1.4.2 <3.20.0']; - -// The raw imported `mysql2` module exposes the `format` helper used to render -// parameterized queries. Typed shallowly since it is only read internally. -type MySQL2Module = { format?: FormatFunction; [key: string]: unknown }; - -export class MySQL2Instrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - protected init(): InstrumentationNodeModuleDefinition[] { - let format: FormatFunction | undefined; - function setFormatFunction(moduleExports: MySQL2Module): void { - if (!format && moduleExports.format) { - format = moduleExports.format; - } - } - const patch = (ConnectionPrototype: Connection): void => { - if (isWrapped(ConnectionPrototype.query)) { - this._unwrap(ConnectionPrototype, 'query'); - } - this._wrap(ConnectionPrototype, 'query', this._patchQuery(format) as any); - if (isWrapped(ConnectionPrototype.execute)) { - this._unwrap(ConnectionPrototype, 'execute'); - } - this._wrap(ConnectionPrototype, 'execute', this._patchQuery(format) as any); - }; - const unpatch = (ConnectionPrototype: Connection): void => { - this._unwrap(ConnectionPrototype, 'query'); - this._unwrap(ConnectionPrototype, 'execute'); - }; - return [ - new InstrumentationNodeModuleDefinition( - 'mysql2', - supportedVersions, - (moduleExports: MySQL2Module) => { - setFormatFunction(moduleExports); - return moduleExports; - }, - () => {}, - [ - new InstrumentationNodeModuleFile( - 'mysql2/promise.js', - supportedVersions, - (moduleExports: MySQL2Module) => { - setFormatFunction(moduleExports); - return moduleExports; - }, - () => {}, - ), - new InstrumentationNodeModuleFile( - 'mysql2/lib/connection.js', - supportedVersions, - (moduleExports: any) => { - const ConnectionPrototype: Connection = getConnectionPrototypeToInstrument(moduleExports); - patch(ConnectionPrototype); - return moduleExports; - }, - (moduleExports: any) => { - if (moduleExports === undefined) return; - const ConnectionPrototype: Connection = getConnectionPrototypeToInstrument(moduleExports); - unpatch(ConnectionPrototype); - }, - ), - ], - ), - ]; - } - - private _patchQuery(format: FormatFunction | undefined) { - const thisPlugin = this; - return (originalQuery: Function): Function => { - return function query( - this: Connection, - query: string | Query | QueryOptions, - _valuesOrCallback?: unknown[] | Function, - _callback?: Function, - ) { - let values; - if (Array.isArray(_valuesOrCallback)) { - values = _valuesOrCallback; - } else if (arguments[2]) { - values = [_valuesOrCallback]; - } - - const attributes: SpanAttributes = { - [SENTRY_KIND]: 'client', - ...getConnectionAttributes(this.config), - // oxlint-disable-next-line typescript/no-deprecated - [DB_SYSTEM]: DB_SYSTEM_VALUE_MYSQL, - // oxlint-disable-next-line typescript/no-deprecated - [DB_STATEMENT]: getQueryText(query, format, values), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - }; - - const span = startInactiveSpan({ - name: getSpanName(query), - attributes, - }); - - const endSpan = once((err?: QueryError | null) => { - if (err) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: err.message }); - } - span.end(); - }); - - if (arguments.length === 1) { - if (typeof (query as any).onResult === 'function') { - thisPlugin._wrap(query as any, 'onResult', thisPlugin._patchCallbackQuery(endSpan)); - } - - const streamableQuery: Query = originalQuery.apply(this, arguments); - - streamableQuery - .once('error', (err: any) => { - endSpan(err); - }) - .once('result', () => { - endSpan(); - }); - - return streamableQuery; - } - - if (typeof arguments[1] === 'function') { - thisPlugin._wrap(arguments, 1, thisPlugin._patchCallbackQuery(endSpan)); - } else if (typeof arguments[2] === 'function') { - thisPlugin._wrap(arguments, 2, thisPlugin._patchCallbackQuery(endSpan)); - } - - return originalQuery.apply(this, arguments); - }; - }; - } - - private _patchCallbackQuery(endSpan: (err?: QueryError | null) => void) { - return (originalCallback: Function) => { - return function (...args: [err: QueryError | null, ...rest: unknown[]]) { - endSpan(args[0]); - return originalCallback(...args); - }; - }; - } -} diff --git a/packages/node/src/integrations/tracing/mysql2/vendored/mysql2-types.ts b/packages/node/src/integrations/tracing/mysql2/vendored/mysql2-types.ts deleted file mode 100644 index 5b3f105eb997..000000000000 --- a/packages/node/src/integrations/tracing/mysql2/vendored/mysql2-types.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Simplified type definitions from the 'mysql2' package. - * Only the members actually used by the instrumentation are included. - */ - -export interface Connection { - config: ConnectionConfig; - query: (...args: any[]) => Query; - execute: (...args: any[]) => Query; - [key: string]: any; -} - -export interface ConnectionConfig { - host?: string; - port?: number; - database?: string; - user?: string; - connectionConfig?: ConnectionConfig; - [key: string]: any; -} - -export interface Query { - sql: string; - onResult?: (...args: any[]) => any; - once(event: string, callback: (...args: any[]) => void): Query; - [key: string]: any; -} - -export interface QueryOptions { - sql: string; - values?: any | any[] | { [param: string]: any }; - [key: string]: any; -} - -export interface QueryError extends Error { - code?: string; - errno?: number; - sqlState?: string; - sqlMessage?: string; - [key: string]: any; -} - -export interface FieldPacket { - [key: string]: any; -} - -export type FormatFunction = (sql: string, values?: any[], stringifyObjects?: boolean, timeZone?: string) => string; diff --git a/packages/node/src/integrations/tracing/mysql2/vendored/semconv.ts b/packages/node/src/integrations/tracing/mysql2/vendored/semconv.ts deleted file mode 100644 index 2dbf2793b106..000000000000 --- a/packages/node/src/integrations/tracing/mysql2/vendored/semconv.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mysql2 - * - Upstream version: @opentelemetry/instrumentation-mysql2@0.64.0 - */ - -export const ATTR_DB_CONNECTION_STRING = 'db.connection_string' as const; -export const DB_SYSTEM_VALUE_MYSQL = 'mysql' as const; diff --git a/packages/node/src/integrations/tracing/mysql2/vendored/utils.ts b/packages/node/src/integrations/tracing/mysql2/vendored/utils.ts deleted file mode 100644 index 28636a9fee4c..000000000000 --- a/packages/node/src/integrations/tracing/mysql2/vendored/utils.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-mysql2 - * - Upstream version: @opentelemetry/instrumentation-mysql2@0.64.0 - * - Types from 'mysql2' inlined as simplified interfaces - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - */ - -import { DB_NAME, DB_USER, NET_PEER_NAME, NET_PEER_PORT } from '@sentry/conventions/attributes'; -import type { SpanAttributes } from '@sentry/core'; -import type { FormatFunction } from './mysql2-types'; -import { ATTR_DB_CONNECTION_STRING } from './semconv'; - -interface QueryOptions { - sql: string; - values?: any | any[] | { [param: string]: any }; -} - -interface Query { - sql: string; -} - -interface Config { - host?: string; - port?: number; - database?: string; - user?: string; - connectionConfig?: Config; -} - -export function getConnectionAttributes(config: Config): SpanAttributes { - const { host, port, database, user } = getConfig(config); - - const attrs: SpanAttributes = { - [ATTR_DB_CONNECTION_STRING]: getJDBCString(host, port, database), - // oxlint-disable-next-line typescript/no-deprecated - [DB_NAME]: database, - [DB_USER]: user, - // oxlint-disable-next-line typescript/no-deprecated - [NET_PEER_NAME]: host, - }; - - const portNumber = parseInt(port, 10); - if (!isNaN(portNumber)) { - // oxlint-disable-next-line typescript/no-deprecated - attrs[NET_PEER_PORT] = portNumber; - } - - return attrs; -} - -function getConfig(config: any) { - const { host, port, database, user } = config?.connectionConfig || config || {}; - return { host, port, database, user }; -} - -function getJDBCString(host: string | undefined, port: number | undefined, database: string | undefined) { - let jdbcString = `jdbc:mysql://${host || 'localhost'}`; - - if (typeof port === 'number') { - jdbcString += `:${port}`; - } - - if (typeof database === 'string') { - jdbcString += `/${database}`; - } - - return jdbcString; -} - -export function getQueryText(query: string | Query | QueryOptions, format?: FormatFunction, values?: any[]): string { - const [querySql, queryValues] = - typeof query === 'string' ? [query, values] : [query.sql, hasValues(query) ? values || query.values : values]; - try { - if (format && queryValues) { - return format(querySql, queryValues); - } else { - return querySql; - } - } catch { - return 'Could not determine the query due to an error in formatting'; - } -} - -function hasValues(obj: Query | QueryOptions): obj is QueryOptions { - return 'values' in obj; -} - -export function getSpanName(query: string | Query | QueryOptions): string { - const rawQuery = typeof query === 'object' ? query.sql : query; - const firstSpace = rawQuery?.indexOf(' '); - if (typeof firstSpace === 'number' && firstSpace !== -1) { - return rawQuery?.substring(0, firstSpace); - } - return rawQuery; -} - -export const once = (fn: Function) => { - let called = false; - return (...args: unknown[]) => { - if (called) return; - called = true; - return fn(...args); - }; -}; - -export function getConnectionPrototypeToInstrument(connection: any) { - const connectionPrototype = connection.prototype; - const basePrototype = Object.getPrototypeOf(connectionPrototype); - - if (typeof basePrototype?.query === 'function' && typeof basePrototype?.execute === 'function') { - return basePrototype; - } - - return connectionPrototype; -} diff --git a/packages/node/src/integrations/tracing/openai/index.ts b/packages/node/src/integrations/tracing/openai/index.ts deleted file mode 100644 index 613f3b7ae053..000000000000 --- a/packages/node/src/integrations/tracing/openai/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { OpenAiOptions } from '@sentry/core'; -import { OPENAI_INTEGRATION_NAME } from '@sentry/core'; -import { generateInstrumentOnce } from '../../../otel/instrument'; -import { SentryOpenAiInstrumentation } from './instrumentation'; - -export const instrumentOpenAi = generateInstrumentOnce( - OPENAI_INTEGRATION_NAME, - options => new SentryOpenAiInstrumentation(options), -); diff --git a/packages/node/src/integrations/tracing/openai/instrumentation.ts b/packages/node/src/integrations/tracing/openai/instrumentation.ts deleted file mode 100644 index 0d44a056838a..000000000000 --- a/packages/node/src/integrations/tracing/openai/instrumentation.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { - InstrumentationBase, - type InstrumentationConfig, - type InstrumentationModuleDefinition, - InstrumentationNodeModuleDefinition, -} from '@opentelemetry/instrumentation'; -import type { Integration, OpenAiClient, OpenAiOptions } from '@sentry/core'; -import { - _INTERNAL_shouldSkipAiProviderWrapping, - instrumentOpenAiClient, - OPENAI_INTEGRATION_NAME, - SDK_VERSION, -} from '@sentry/core'; - -const supportedVersions = ['>=4.0.0 <7']; - -export interface OpenAiIntegration extends Integration { - options: OpenAiOptions; -} - -type OpenAiInstrumentationOptions = InstrumentationConfig & OpenAiOptions; - -/** - * Represents the patched shape of the OpenAI module export. - */ -interface PatchedModuleExports { - [key: string]: unknown; - OpenAI: abstract new (...args: unknown[]) => OpenAiClient; - AzureOpenAI?: abstract new (...args: unknown[]) => OpenAiClient; -} - -/** - * Sentry OpenAI instrumentation using OpenTelemetry. - */ -export class SentryOpenAiInstrumentation extends InstrumentationBase { - public constructor(config: OpenAiInstrumentationOptions = {}) { - super('@sentry/instrumentation-openai', SDK_VERSION, config); - } - - /** - * Initializes the instrumentation by defining the modules to be patched. - */ - public init(): InstrumentationModuleDefinition { - const module = new InstrumentationNodeModuleDefinition('openai', supportedVersions, this._patch.bind(this)); - return module; - } - - /** - * Core patch logic applying instrumentation to the OpenAI and AzureOpenAI client constructors. - */ - private _patch(exports: PatchedModuleExports): PatchedModuleExports | void { - let result = exports; - result = this._patchClient(result, 'OpenAI'); - result = this._patchClient(result, 'AzureOpenAI'); - return result; - } - - /** - * Patch logic applying instrumentation to the specified client constructor. - */ - private _patchClient(exports: PatchedModuleExports, exportKey: 'OpenAI' | 'AzureOpenAI'): PatchedModuleExports { - const Original = exports[exportKey]; - if (!Original) { - return exports; - } - - const config = this.getConfig(); - - const WrappedOpenAI = function (this: unknown, ...args: unknown[]) { - // Check if wrapping should be skipped (e.g., when LangChain is handling instrumentation) - if (_INTERNAL_shouldSkipAiProviderWrapping(OPENAI_INTEGRATION_NAME)) { - return Reflect.construct(Original, args) as OpenAiClient; - } - - const instance = Reflect.construct(Original, args); - - return instrumentOpenAiClient(instance as OpenAiClient, config); - } as unknown as abstract new (...args: unknown[]) => OpenAiClient; - - // Preserve static and prototype chains - Object.setPrototypeOf(WrappedOpenAI, Original); - Object.setPrototypeOf(WrappedOpenAI.prototype, Original.prototype); - - for (const key of Object.getOwnPropertyNames(Original)) { - if (!['length', 'name', 'prototype'].includes(key)) { - const descriptor = Object.getOwnPropertyDescriptor(Original, key); - if (descriptor) { - Object.defineProperty(WrappedOpenAI, key, descriptor); - } - } - } - - // Constructor replacement - handle read-only properties - // The OpenAI property might have only a getter, so use defineProperty - try { - exports[exportKey] = WrappedOpenAI; - } catch { - // If direct assignment fails, override the property descriptor - Object.defineProperty(exports, exportKey, { - value: WrappedOpenAI, - writable: true, - configurable: true, - enumerable: true, - }); - } - - // Wrap the default export if it points to the original constructor - // Constructor replacement - handle read-only properties - // The OpenAI property might have only a getter, so use defineProperty - if (exports.default === Original) { - try { - exports.default = WrappedOpenAI; - } catch { - // If direct assignment fails, override the property descriptor - Object.defineProperty(exports, 'default', { - value: WrappedOpenAI, - writable: true, - configurable: true, - enumerable: true, - }); - } - } - return exports; - } -} diff --git a/packages/node/src/integrations/tracing/postgres/index.ts b/packages/node/src/integrations/tracing/postgres/index.ts deleted file mode 100644 index 362effccc554..000000000000 --- a/packages/node/src/integrations/tracing/postgres/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { PgInstrumentation } from './vendored/instrumentation'; -import { generateInstrumentOnce } from '../../../otel/instrument'; - -interface PostgresIntegrationOptions { - ignoreConnectSpans?: boolean; -} - -const INTEGRATION_NAME = 'Postgres' as const; - -export const instrumentPostgres = generateInstrumentOnce( - INTEGRATION_NAME, - PgInstrumentation, - (options?: PostgresIntegrationOptions) => ({ - ignoreConnectSpans: options?.ignoreConnectSpans ?? false, - }), -); diff --git a/packages/node/src/integrations/tracing/postgres/vendored/enums/AttributeNames.ts b/packages/node/src/integrations/tracing/postgres/vendored/enums/AttributeNames.ts deleted file mode 100644 index 15a8689c4971..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/enums/AttributeNames.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-pg - * - Upstream version: @opentelemetry/instrumentation-pg@0.70.0 - */ - -// Postgresql specific attributes not covered by semantic conventions -export enum AttributeNames { - PG_PLAN = 'db.postgresql.plan', - IDLE_TIMEOUT_MILLIS = 'db.postgresql.idle.timeout.millis', - MAX_CLIENT = 'db.postgresql.max.client', -} diff --git a/packages/node/src/integrations/tracing/postgres/vendored/enums/SpanNames.ts b/packages/node/src/integrations/tracing/postgres/vendored/enums/SpanNames.ts deleted file mode 100644 index cffc9100d4f3..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/enums/SpanNames.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-pg - * - Upstream version: @opentelemetry/instrumentation-pg@0.70.0 - */ - -// Contains span names produced by instrumentation -export enum SpanNames { - QUERY_PREFIX = 'pg.query', - CONNECT = 'pg.connect', - POOL_CONNECT = 'pg-pool.connect', -} diff --git a/packages/node/src/integrations/tracing/postgres/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/postgres/vendored/instrumentation.ts deleted file mode 100644 index 9aae196ed98d..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/instrumentation.ts +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-pg - * - Upstream version: @opentelemetry/instrumentation-pg@0.70.0 - * - Types from `pg` and `pg-pool` packages inlined as simplified interfaces - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - * - Dropped the OTel metrics (no MeterProvider is wired up), the `SemconvStability` - * dual-emission, and config the SDK never passes (request/response hooks, - * enhancedDatabaseReporting, addSqlCommenterCommentToQueries) - */ - -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import type { Span } from '@sentry/core'; -import { getActiveSpan, SDK_VERSION, SPAN_STATUS_ERROR, startInactiveSpan, withActiveSpan } from '@sentry/core'; -import { SENTRY_KIND } from '@sentry/conventions/attributes'; -import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile'; -import { SpanNames } from './enums/SpanNames'; -import type { - PgClientConnect, - PgClientExtended, - PgPoolCallback, - PgPoolExtended, - PostgresCallback, -} from './internal-types'; -import type { PgInstrumentationConfig } from './types'; -import * as utils from './utils'; - -const PACKAGE_NAME = '@sentry/instrumentation-pg'; - -function extractModuleExports(module: any): any { - return module[Symbol.toStringTag] === 'Module' - ? module.default // ESM - : module; // CommonJS -} - -/** - * Binds `callback` to `parentSpan`, so that the span is active again whenever the - * (deferred) callback runs. This mirrors the upstream `context.bind(context.active(), callback)`: - * `pg` invokes callbacks outside of the original async scope (e.g. for pooled connections), so we - * re-establish the trace context to keep spans created inside the callback correctly parented. - */ -function bindCallbackToSpan any>(parentSpan: Span, callback: T): T { - return function (this: unknown, ...args: any[]): unknown { - return withActiveSpan(parentSpan, () => callback.apply(this, args)); - } as T; -} - -export class PgInstrumentation extends InstrumentationBase { - public constructor(config: PgInstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - protected init(): InstrumentationNodeModuleDefinition[] { - const SUPPORTED_PG_VERSIONS = ['>=8.0.3 <9']; - const SUPPORTED_PG_POOL_VERSIONS = ['>=2.0.0 <4']; - - const modulePgNativeClient = new InstrumentationNodeModuleFile( - 'pg/lib/native/client.js', - SUPPORTED_PG_VERSIONS, - this._patchPgClient.bind(this), - this._unpatchPgClient.bind(this), - ); - - const modulePgClient = new InstrumentationNodeModuleFile( - 'pg/lib/client.js', - SUPPORTED_PG_VERSIONS, - this._patchPgClient.bind(this), - this._unpatchPgClient.bind(this), - ); - - const modulePG = new InstrumentationNodeModuleDefinition( - 'pg', - SUPPORTED_PG_VERSIONS, - (module: any) => { - const moduleExports = extractModuleExports(module); - - this._patchPgClient(moduleExports.Client); - return module; - }, - (module: any) => { - const moduleExports = extractModuleExports(module); - - this._unpatchPgClient(moduleExports.Client); - return module; - }, - [modulePgClient, modulePgNativeClient], - ); - - const modulePGPool = new InstrumentationNodeModuleDefinition( - 'pg-pool', - SUPPORTED_PG_POOL_VERSIONS, - (module: any) => { - const moduleExports = extractModuleExports(module); - if (isWrapped(moduleExports.prototype.connect)) { - this._unwrap(moduleExports.prototype, 'connect'); - } - this._wrap(moduleExports.prototype, 'connect', this._getPoolConnectPatch()); - return moduleExports; - }, - (module: any) => { - const moduleExports = extractModuleExports(module); - if (isWrapped(moduleExports.prototype.connect)) { - this._unwrap(moduleExports.prototype, 'connect'); - } - }, - ); - - return [modulePG, modulePGPool]; - } - - private _patchPgClient(module: any): any { - if (!module) { - return; - } - - const moduleExports = extractModuleExports(module); - - if (isWrapped(moduleExports.prototype.query)) { - this._unwrap(moduleExports.prototype, 'query'); - } - - if (isWrapped(moduleExports.prototype.connect)) { - this._unwrap(moduleExports.prototype, 'connect'); - } - - this._wrap(moduleExports.prototype, 'query', this._getClientQueryPatch()); - - this._wrap(moduleExports.prototype, 'connect', this._getClientConnectPatch()); - - return module; - } - - private _unpatchPgClient(module: any): any { - const moduleExports = extractModuleExports(module); - - if (isWrapped(moduleExports.prototype.query)) { - this._unwrap(moduleExports.prototype, 'query'); - } - - if (isWrapped(moduleExports.prototype.connect)) { - this._unwrap(moduleExports.prototype, 'connect'); - } - - return module; - } - - private _getClientConnectPatch() { - const plugin = this; - return (original: PgClientConnect) => { - return function connect(this: PgClientExtended, callback?: (...args: unknown[]) => void) { - if (utils.shouldSkipInstrumentation() || plugin.getConfig().ignoreConnectSpans) { - return original.call(this, callback); - } - - const span = startInactiveSpan({ - name: SpanNames.CONNECT, - attributes: { - [SENTRY_KIND]: 'client', - ...utils.getSemanticAttributesFromConnection(this), - }, - }); - - let cb = callback; - if (cb) { - const parentSpan = getActiveSpan(); - cb = utils.patchClientConnectCallback(span, cb); - if (parentSpan) { - cb = bindCallbackToSpan(parentSpan, cb); - } - } - - const connectResult: unknown = withActiveSpan(span, () => { - return original.call(this, cb); - }); - - return handleConnectResult(span, connectResult); - }; - }; - } - - private _getClientQueryPatch() { - return (original: (...args: any[]) => any) => { - this._diag.debug('Patching pg.Client.prototype.query'); - return function query(this: PgClientExtended, ...args: unknown[]) { - if (utils.shouldSkipInstrumentation()) { - return original.apply(this, args as never); - } - - // client.query(text, cb?), client.query(text, values, cb?), and - // client.query(configObj, cb?) are all valid signatures. We construct - // a queryConfig obj from all (valid) signatures to build the span in a - // unified way. We verify that we at least have query text, and code - // defensively when dealing with `queryConfig` after that (to handle all - // the other invalid cases, like a non-array for values being provided). - // The type casts here reflect only what we've actually validated. - const arg0 = args[0]; - const firstArgIsString = typeof arg0 === 'string'; - const firstArgIsQueryObjectWithText = utils.isObjectWithTextString(arg0); - - const queryConfig = firstArgIsString - ? { - text: arg0, - values: Array.isArray(args[1]) ? args[1] : undefined, - } - : firstArgIsQueryObjectWithText - ? { - ...(arg0 as any), - name: arg0.name, - text: arg0.text, - values: (arg0 as any).values ?? (Array.isArray(args[1]) ? args[1] : undefined), - } - : undefined; - - const span = utils.handleConfigQuery.call(this, queryConfig); - - // Bind callback (if any) to parent span (if any) - if (args.length > 0) { - const parentSpan = getActiveSpan(); - if (typeof args[args.length - 1] === 'function') { - // Patch ParameterQuery callback - args[args.length - 1] = utils.patchCallback(span, args[args.length - 1] as PostgresCallback); - - // If a parent span exists, bind the callback - if (parentSpan) { - args[args.length - 1] = bindCallbackToSpan(parentSpan, args[args.length - 1] as PostgresCallback); - } - } else if (typeof queryConfig?.callback === 'function') { - // Patch ConfigQuery callback - let callback = utils.patchCallback(span, queryConfig.callback as PostgresCallback); - - // If a parent span existed, bind the callback - if (parentSpan) { - callback = bindCallbackToSpan(parentSpan, callback); - } - - (args[0] as { callback?: PostgresCallback }).callback = callback; - } - } - - let result: unknown; - try { - result = original.apply(this, args as never); - } catch (e: unknown) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: utils.getErrorMessage(e) }); - span.end(); - throw e; - } - - // Bind promise to parent span and end the span - if (result instanceof Promise) { - return result - .then((result: unknown) => { - span.end(); - return result; - }) - .catch((error: unknown) => { - span.setStatus({ code: SPAN_STATUS_ERROR, message: utils.getErrorMessage(error) }); - span.end(); - return Promise.reject(error); - }); - } - - // else returns void - return result; // void - }; - }; - } - - private _getPoolConnectPatch() { - const plugin = this; - return (originalConnect: (...args: any[]) => any) => { - return function connect(this: PgPoolExtended, callback?: PgPoolCallback) { - if (utils.shouldSkipInstrumentation() || plugin.getConfig().ignoreConnectSpans) { - return originalConnect.call(this, callback); - } - - const span = startInactiveSpan({ - name: SpanNames.POOL_CONNECT, - attributes: { - [SENTRY_KIND]: 'client', - ...utils.getSemanticAttributesFromPoolConnection(this.options), - }, - }); - - let cb = callback; - if (cb) { - const parentSpan = getActiveSpan(); - cb = utils.patchCallbackPGPool(span, cb); - // If a parent span exists, bind the callback - if (parentSpan) { - cb = bindCallbackToSpan(parentSpan, cb); - } - } - - const connectResult: unknown = withActiveSpan(span, () => { - return originalConnect.call(this, cb); - }); - - return handleConnectResult(span, connectResult); - }; - }; - } -} - -function handleConnectResult(span: Span, connectResult: unknown): unknown { - if (!(connectResult instanceof Promise)) { - return connectResult; - } - - const connectResultPromise = connectResult as Promise; - // The caller's continuation after `await client.connect()` keeps its trace context via the - // SDK's async context propagation, so we don't need to re-bind the returned promise. - return connectResultPromise - .then(result => { - span.end(); - return result; - }) - .catch((error: unknown) => { - span.setStatus({ code: SPAN_STATUS_ERROR, message: utils.getErrorMessage(error) }); - span.end(); - return Promise.reject(error); - }); -} diff --git a/packages/node/src/integrations/tracing/postgres/vendored/internal-types.ts b/packages/node/src/integrations/tracing/postgres/vendored/internal-types.ts deleted file mode 100644 index b62df8c77234..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/internal-types.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-pg - * - Upstream version: @opentelemetry/instrumentation-pg@0.70.0 - * - Types from `pg` and `pg-pool` packages inlined as simplified interfaces - */ - -import type { PgClient } from './pg-types'; -import type { PgPool } from './pg-pool-types'; - -export type PostgresCallback = (err: Error, res: object) => unknown; - -// NB: this type describes the shape of a parsed, normalized form of the -// connection information that's stored inside each pg.Client instance. It's -// _not_ the same as the ConnectionConfig type exported from `@types/pg`. That -// type defines how data must be _passed in_ when creating a new `pg.Client`, -// which doesn't necessarily match the normalized internal form. E.g., a user -// can call `new Client({ connectionString: '...' }), but `connectionString` -// will never show up in the type below, because only the extracted host, port, -// etc. are recorded in this normalized config. The keys listed below are also -// incomplete, which is fine because the type is internal and these keys are the -// only ones our code is reading. See https://github.com/brianc/node-postgres/blob/fde5ec586e49258dfc4a2fcd861fcdecb4794fc3/lib/client.js#L25 -export interface PgParsedConnectionParams { - database?: string; - host?: string; - namespace?: string; - port?: number; - user?: string; -} - -export interface PgClientExtended extends PgClient { - connectionParameters: PgParsedConnectionParams; -} - -export type PgPoolCallback = (err: Error, client: any, done: (release?: any) => void) => void; - -export interface PgPoolOptionsParams { - allowExitOnIdle: boolean; - connectionString?: string; // connection string if provided directly - database: string; - host: string; - idleTimeoutMillis: number; // the minimum amount of time that an object may sit idle in the pool before it is eligible for eviction due to idle time - max: number; - maxClient: number; // maximum size of the pool - maxLifetimeSeconds: number; - maxUses: number; - namespace: string; - port: number; - user: string; -} - -export interface PgPoolExtended extends PgPool { - options: PgPoolOptionsParams; -} - -export type PgClientConnect = (callback?: Function) => Promise | void; diff --git a/packages/node/src/integrations/tracing/postgres/vendored/pg-pool-types.ts b/packages/node/src/integrations/tracing/postgres/vendored/pg-pool-types.ts deleted file mode 100644 index 4bf1ea6a112a..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/pg-pool-types.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Simplified type definitions inlined from the `pg-pool` package. - * PoolClient and PoolConfig are replaced with structural equivalents. - */ - -import type { PgPoolClient } from './pg-types'; - -export interface PgPool { - readonly totalCount: number; - readonly idleCount: number; - readonly waitingCount: number; - readonly expiredCount: number; - - readonly ending: boolean; - readonly ended: boolean; - - options: any; - - connect(): Promise; - connect( - callback: (err: Error | undefined, client: PgPoolClient | undefined, done: (release?: any) => void) => void, - ): void; - - end(): Promise; - end(callback: () => void): void; - - query(...args: any[]): any; - - on(event: 'release' | 'error', listener: (err: Error, client: PgPoolClient) => void): this; - on(event: 'connect' | 'acquire' | 'remove', listener: (client: PgPoolClient) => void): this; - on(event: string, listener: (...args: any[]) => void): this; - - [key: string]: any; -} diff --git a/packages/node/src/integrations/tracing/postgres/vendored/pg-types.ts b/packages/node/src/integrations/tracing/postgres/vendored/pg-types.ts deleted file mode 100644 index 181449405947..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/pg-types.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Simplified type definitions inlined from the `pg` package. - * Deep type trees (Connection, Submittable, QueryConfig generics, FieldDef, - * NoticeMessage, ClientConfig) are replaced with structural equivalents. - */ - -export interface FieldDef { - name: string; - tableID: number; - columnID: number; - dataTypeID: number; - dataTypeSize: number; - dataTypeModifier: number; - format: string; -} - -export interface QueryResultBase { - command: string; - rowCount: number | null; - oid: number; - fields: FieldDef[]; -} - -export interface QueryResult extends QueryResultBase { - rows: R[]; -} - -export interface QueryArrayResult extends QueryResultBase { - rows: R[]; -} - -export interface PgClientBase { - connect(): Promise; - connect(callback: (err: Error) => void): void; - - query(...args: any[]): any; - - copyFrom(queryText: string): any; - copyTo(queryText: string): any; - - pauseDrain(): void; - resumeDrain(): void; - - escapeIdentifier(str: string): string; - escapeLiteral(str: string): string; - - on(event: 'drain', listener: () => void): this; - on(event: 'error', listener: (err: Error) => void): this; - on(event: 'notice', listener: (notice: any) => void): this; - on(event: 'notification', listener: (message: any) => void): this; - on(event: 'end', listener: () => void): this; - on(event: string, listener: (...args: any[]) => void): this; - - [key: string]: any; -} - -export interface PgClient extends PgClientBase { - user?: string | undefined; - database?: string | undefined; - port: number; - host: string; - password?: string | undefined; - ssl: boolean; - readonly connection: any; - - end(): Promise; - end(callback: (err: Error) => void): void; -} - -export interface PgPoolClient extends PgClientBase { - release(err?: Error | boolean): void; -} diff --git a/packages/node/src/integrations/tracing/postgres/vendored/semconv.ts b/packages/node/src/integrations/tracing/postgres/vendored/semconv.ts deleted file mode 100644 index ae3306d29c7a..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/semconv.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-pg - * - Upstream version: @opentelemetry/instrumentation-pg@0.70.0 - * - Trimmed to the (old) semantic conventions the SDK actually emits - */ - -/* - * This file contains a copy of unstable semantic convention definitions - * used by this package. - * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv - */ - -/** - * Deprecated, use `server.address`, `server.port` attributes instead. - * - * @example "Server=(localdb)\\v11.0;Integrated Security=true;" - * - * @deprecated Replaced by `server.address` and `server.port`. - */ -export const ATTR_DB_CONNECTION_STRING = 'db.connection_string' as const; - -/** - * Enum value "postgresql" for attribute `db.system`. - */ -export const DB_SYSTEM_VALUE_POSTGRESQL = 'postgresql' as const; diff --git a/packages/node/src/integrations/tracing/postgres/vendored/types.ts b/packages/node/src/integrations/tracing/postgres/vendored/types.ts deleted file mode 100644 index 82487d8acb53..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-pg - * - Upstream version: @opentelemetry/instrumentation-pg@0.70.0 - * - Trimmed to the config the SDK actually passes - */ - -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; - -export interface PgInstrumentationConfig extends InstrumentationConfig { - /** - * If true, `pg.connect` and `pg-pool.connect` spans will not be created. - * Query spans are still recorded. - * - * @default false - */ - ignoreConnectSpans?: boolean; -} diff --git a/packages/node/src/integrations/tracing/postgres/vendored/utils.ts b/packages/node/src/integrations/tracing/postgres/vendored/utils.ts deleted file mode 100644 index 2af01ae35c9d..000000000000 --- a/packages/node/src/integrations/tracing/postgres/vendored/utils.ts +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-pg - * - Upstream version: @opentelemetry/instrumentation-pg@0.70.0 - * - Types from `pg` package inlined as simplified interfaces - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - */ - -import type { Span, SpanAttributes } from '@sentry/core'; -import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan } from '@sentry/core'; -import { AttributeNames } from './enums/AttributeNames'; -import { SpanNames } from './enums/SpanNames'; -import type { - PgClientExtended, - PgParsedConnectionParams, - PgPoolCallback, - PgPoolExtended, - PgPoolOptionsParams, - PostgresCallback, -} from './internal-types'; -import type { PgClient } from './pg-types'; -import { ATTR_DB_CONNECTION_STRING, DB_SYSTEM_VALUE_POSTGRESQL } from './semconv'; -import { - DB_NAME, - DB_STATEMENT, - DB_SYSTEM, - DB_USER, - NET_PEER_NAME, - NET_PEER_PORT, - SENTRY_KIND, -} from '@sentry/conventions/attributes'; - -/* oxlint-disable typescript/no-deprecated */ - -export const ORIGIN = 'auto.db.otel.postgres'; - -/** - * Helper function to get a low cardinality span name from whatever info we have - * about the query. - * - * This is tricky, because we don't have most of the information (table name, - * operation name, etc) the spec recommends using to build a low-cardinality - * value w/o parsing. So, we use db.name and assume that, if the query's a named - * prepared statement, those `name` values will be low cardinality. If we don't - * have a named prepared statement, we try to parse an operation (despite the - * spec's warnings). - * - * @params dbName The name of the db against which this query is being issued, - * which could be missing if no db name was given at the time that the - * connection was established. - * @params queryConfig Information we have about the query being issued, typed - * to reflect only the validation we've actually done on the args to - * `client.query()`. This will be undefined if `client.query()` was called - * with invalid arguments. - */ -export function getQuerySpanName(dbName: string | undefined, queryConfig?: { text: string; name?: unknown }): string { - // NB: when the query config is invalid, we omit the dbName too, so that - // someone (or some tool) reading the span name doesn't misinterpret the - // dbName as being a prepared statement or sql commit name. - if (!queryConfig) return SpanNames.QUERY_PREFIX; - - // Either the name of a prepared statement; or an attempted parse - // of the SQL command, normalized to uppercase; or unknown. - const command = - typeof queryConfig.name === 'string' && queryConfig.name - ? queryConfig.name - : parseNormalizedOperationName(queryConfig.text); - - return `${SpanNames.QUERY_PREFIX}:${command}${dbName ? ` ${dbName}` : ''}`; -} - -export function parseNormalizedOperationName(queryText: string): string { - // Trim the query text to handle leading/trailing whitespace - const trimmedQuery = queryText.trim(); - const indexOfFirstSpace = trimmedQuery.indexOf(' '); - let sqlCommand = indexOfFirstSpace === -1 ? trimmedQuery : trimmedQuery.slice(0, indexOfFirstSpace); - sqlCommand = sqlCommand.toUpperCase(); - - // Handle query text being "COMMIT;", which has an extra semicolon before the space. - return sqlCommand.endsWith(';') ? sqlCommand.slice(0, -1) : sqlCommand; -} - -export function parseAndMaskConnectionString(connectionString: string): string { - try { - // Parse the connection string - const url = new URL(connectionString); - - // Remove all auth information (username and password) - url.username = ''; - url.password = ''; - - return url.toString(); - } catch { - // If parsing fails, return a generic connection string - return 'postgresql://localhost:5432/'; - } -} - -export function getConnectionString(params: PgParsedConnectionParams | PgPoolOptionsParams): string { - if ('connectionString' in params && params.connectionString) { - return parseAndMaskConnectionString(params.connectionString); - } - const host = params.host || 'localhost'; - const port = params.port || 5432; - const database = params.database || ''; - return `postgresql://${host}:${port}/${database}`; -} - -function getPort(port: number | undefined): number | undefined { - // Port may be NaN as parseInt() is used on the value, passing null will result in NaN being parsed. - // https://github.com/brianc/node-postgres/blob/2a8efbee09a284be12748ed3962bc9b816965e36/packages/pg/lib/connection-parameters.js#L66 - if (Number.isInteger(port)) { - return port; - } - - // Unable to find the default used in pg code, so falling back to 'undefined'. - return undefined; -} - -export function getSemanticAttributesFromConnection(params: PgParsedConnectionParams): SpanAttributes { - return { - [DB_SYSTEM]: DB_SYSTEM_VALUE_POSTGRESQL, - [DB_NAME]: params.database, - [ATTR_DB_CONNECTION_STRING]: getConnectionString(params), - [DB_USER]: params.user, - [NET_PEER_NAME]: params.host, // required - [NET_PEER_PORT]: getPort(params.port), - }; -} - -export function getSemanticAttributesFromPoolConnection(params: PgPoolOptionsParams): SpanAttributes { - let url: URL | undefined; - try { - url = params.connectionString ? new URL(params.connectionString) : undefined; - } catch { - url = undefined; - } - - return { - [AttributeNames.IDLE_TIMEOUT_MILLIS]: params.idleTimeoutMillis, - [AttributeNames.MAX_CLIENT]: params.maxClient, - [DB_SYSTEM]: DB_SYSTEM_VALUE_POSTGRESQL, - [DB_NAME]: url?.pathname.slice(1) ?? params.database, - [ATTR_DB_CONNECTION_STRING]: getConnectionString(params), - [NET_PEER_NAME]: url?.hostname ?? params.host, - [NET_PEER_PORT]: Number(url?.port) || getPort(params.port), - [DB_USER]: url?.username ?? params.user, - }; -} - -/** - * The SDK always requires a parent span (it sets `requireParentSpan: true`), so - * we only instrument when there is an active span to parent the new span under. - */ -export function shouldSkipInstrumentation(): boolean { - return getActiveSpan() === undefined; -} - -// Create an (inactive) span from our normalized queryConfig object, -// or return a basic span if no queryConfig was given/could be created. -export function handleConfigQuery( - this: PgClientExtended, - queryConfig?: { text: string; values?: unknown; name?: unknown }, -): Span { - // Create child span. - const { connectionParameters } = this; - const dbName = connectionParameters.database; - - const spanName = getQuerySpanName(dbName, queryConfig); - const span = startInactiveSpan({ - name: spanName, - attributes: { - [SENTRY_KIND]: 'client', - ...getSemanticAttributesFromConnection(connectionParameters), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - }, - }); - - if (!queryConfig) { - return span; - } - - // Set attributes - if (queryConfig.text) { - span.setAttribute(DB_STATEMENT, queryConfig.text); - } - - // Set plan name attribute, if present - if (typeof queryConfig.name === 'string') { - span.setAttribute(AttributeNames.PG_PLAN, queryConfig.name); - } - - return span; -} - -export function patchCallback(span: Span, cb: PostgresCallback): PostgresCallback { - return function patchedCallback(this: PgClientExtended, err: Error, res: object) { - if (err) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: err.message }); - } - span.end(); - cb.call(this, err, res); - }; -} - -export function patchCallbackPGPool(span: Span, cb: PgPoolCallback): PgPoolCallback { - return function patchedCallback(this: PgPoolExtended, err: Error, res: object, done: any) { - if (err) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: err.message }); - } - span.end(); - cb.call(this, err, res, done); - }; -} - -export function patchClientConnectCallback(span: Span, cb: (...args: unknown[]) => void): (...args: unknown[]) => void { - return function patchedClientConnectCallback(this: PgClient, ...args: unknown[]) { - const err = args[0]; - if (err instanceof Error) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: err.message }); - } - span.end(); - cb.apply(this, args); - }; -} - -/** - * Attempt to get a message string from a thrown value, while being quite - * defensive, to recognize the fact that, in JS, any kind of value (even - * primitives) can be thrown. - */ -export function getErrorMessage(e: unknown): string | undefined { - return typeof e === 'object' && e !== null && 'message' in e - ? String((e as { message?: unknown }).message) - : undefined; -} - -export function isObjectWithTextString(it: unknown): it is ObjectWithText { - return typeof it === 'object' && typeof (it as null | { text?: unknown })?.text === 'string'; -} - -export type ObjectWithText = { - text: string; - [k: string]: unknown; -}; diff --git a/packages/node/src/integrations/tracing/postgresjs.ts b/packages/node/src/integrations/tracing/postgresjs.ts deleted file mode 100644 index 4de3617d60ec..000000000000 --- a/packages/node/src/integrations/tracing/postgresjs.ts +++ /dev/null @@ -1,401 +0,0 @@ -// Instrumentation for https://github.com/porsager/postgres - -import { context, trace } from '@opentelemetry/api'; -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { - InstrumentationBase, - InstrumentationNodeModuleDefinition, - safeExecuteInTheMiddle, -} from '@opentelemetry/instrumentation'; -import { InstrumentationNodeModuleFile } from './InstrumentationNodeModuleFile'; -import { DB_OPERATION_NAME, DB_QUERY_TEXT, DB_SYSTEM_NAME, ERROR_TYPE } from '@sentry/conventions/attributes'; -import type { Span } from '@sentry/core'; -import { - debug, - instrumentPostgresJsSql, - replaceExports, - SDK_VERSION, - SPAN_STATUS_ERROR, - startSpanManual, -} from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import { addOriginToSpan } from '../../utils/addOriginToSpan'; -import { DEBUG_BUILD } from '../../debug-build'; - -const INTEGRATION_NAME = 'PostgresJs' as const; -const SUPPORTED_VERSIONS = ['>=3.0.0 <4']; -// Not part of `@sentry/conventions`, so we keep it inline. -const ATTR_DB_RESPONSE_STATUS_CODE = 'db.response.status_code'; -const SQL_OPERATION_REGEX = /^(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER)/i; - -type PostgresConnectionContext = { - ATTR_DB_NAMESPACE?: string; // Database name - ATTR_SERVER_ADDRESS?: string; // Hostname or IP address of the database server - ATTR_SERVER_PORT?: string; // Port number of the database server -}; - -// Marker to track if a query was created from an instrumented sql instance -// This prevents double-spanning when both wrapper and prototype patches are active -const QUERY_FROM_INSTRUMENTED_SQL = Symbol.for('sentry.query.from.instrumented.sql'); - -type PostgresJsInstrumentationConfig = InstrumentationConfig & { - /** - * Whether to require a parent span for the instrumentation. - * If set to true, the instrumentation will only create spans if there is a parent span - * available in the current scope. - * @default true - */ - requireParentSpan?: boolean; - /** - * Hook to modify the span before it is started. - * This can be used to set additional attributes or modify the span in any way. - */ - requestHook?: (span: Span, sanitizedSqlQuery: string, postgresConnectionContext?: PostgresConnectionContext) => void; -}; - -export const instrumentPostgresJs = generateInstrumentOnce( - INTEGRATION_NAME, - (options?: PostgresJsInstrumentationConfig) => - new PostgresJsInstrumentation({ - requireParentSpan: options?.requireParentSpan ?? true, - requestHook: options?.requestHook, - }), -); - -/** - * Instrumentation for the [postgres](https://www.npmjs.com/package/postgres) library. - * This instrumentation captures postgresjs queries and their attributes. - * - * Uses internal Sentry patching patterns to support both CommonJS and ESM environments. - */ -export class PostgresJsInstrumentation extends InstrumentationBase { - public constructor(config: PostgresJsInstrumentationConfig) { - super('sentry-postgres-js', SDK_VERSION, config); - } - - /** - * Initializes the instrumentation by patching the postgres module. - * Uses two complementary approaches: - * 1. Main function wrapper: instruments sql instances created AFTER instrumentation is set up (CJS + ESM) - * 2. Query.prototype patch: fallback for sql instances created BEFORE instrumentation (CJS only) - */ - public init(): InstrumentationNodeModuleDefinition { - const module = new InstrumentationNodeModuleDefinition( - 'postgres', - SUPPORTED_VERSIONS, - exports => { - try { - return this._patchPostgres(exports); - } catch (e) { - DEBUG_BUILD && debug.error('Failed to patch postgres module:', e); - return exports; - } - }, - exports => exports, - ); - - // Add fallback Query.prototype patching for pre-existing sql instances (CJS only) - // This catches queries from sql instances created before Sentry was initialized - ['src', 'cf/src', 'cjs/src'].forEach(path => { - module.files.push( - new InstrumentationNodeModuleFile( - `postgres/${path}/query.js`, - SUPPORTED_VERSIONS, - this._patchQueryPrototype.bind(this), - this._unpatchQueryPrototype.bind(this), - ), - ); - }); - - return module; - } - - /** - * Patches the postgres module by wrapping the main export function. - * This intercepts the creation of sql instances and instruments them. - */ - private _patchPostgres(exports: { [key: string]: unknown }): { [key: string]: unknown } { - // In CJS: exports is the function itself - // In ESM: exports.default is the function - const isFunction = typeof exports === 'function'; - const Original = isFunction ? exports : exports.default; - - if (typeof Original !== 'function') { - DEBUG_BUILD && debug.warn('postgres module does not export a function. Skipping instrumentation.'); - return exports; - } - - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - - const WrappedPostgres = function (this: unknown, ...args: unknown[]): unknown { - const sql = Reflect.construct(Original as (...args: unknown[]) => unknown, args); - - // Validate that construction succeeded and returned a valid function object - if (!sql || typeof sql !== 'function') { - DEBUG_BUILD && debug.warn('postgres() did not return a valid instance'); - return sql; - } - - // Delegate to the portable instrumentation from @sentry/core - const config = self.getConfig(); - return instrumentPostgresJsSql(sql, { - requireParentSpan: config.requireParentSpan, - requestHook: config.requestHook, - }); - }; - - Object.setPrototypeOf(WrappedPostgres, Original); - Object.setPrototypeOf(WrappedPostgres.prototype, (Original as { prototype: object }).prototype); - - for (const key of Object.getOwnPropertyNames(Original)) { - if (!['length', 'name', 'prototype'].includes(key)) { - const descriptor = Object.getOwnPropertyDescriptor(Original, key); - if (descriptor) { - Object.defineProperty(WrappedPostgres, key, descriptor); - } - } - } - - // For CJS: the exports object IS the function, so return the wrapped function - // For ESM: replace the default export - if (isFunction) { - return WrappedPostgres as unknown as { [key: string]: unknown }; - } else { - replaceExports(exports, 'default', WrappedPostgres); - return exports; - } - } - - /** - * Determines whether a span should be created based on the current context. - * If `requireParentSpan` is set to true in the configuration, a span will - * only be created if there is a parent span available. - */ - private _shouldCreateSpans(): boolean { - const config = this.getConfig(); - const hasParentSpan = trace.getSpan(context.active()) !== undefined; - return hasParentSpan || !config.requireParentSpan; - } - - /** - * Extracts DB operation name from SQL query and sets it on the span. - */ - private _setOperationName(span: Span, sanitizedQuery: string | undefined, command?: string): void { - if (command) { - span.setAttribute(DB_OPERATION_NAME, command); - return; - } - // Fallback: extract operation from the SQL query - const operationMatch = sanitizedQuery?.match(SQL_OPERATION_REGEX); - if (operationMatch?.[1]) { - span.setAttribute(DB_OPERATION_NAME, operationMatch[1].toUpperCase()); - } - } - - /** - * Reconstructs the full SQL query from template strings with PostgreSQL placeholders. - * - * For sql`SELECT * FROM users WHERE id = ${123} AND name = ${'foo'}`: - * strings = ["SELECT * FROM users WHERE id = ", " AND name = ", ""] - * returns: "SELECT * FROM users WHERE id = $1 AND name = $2" - */ - private _reconstructQuery(strings: string[] | undefined): string | undefined { - if (!strings?.length) { - return undefined; - } - if (strings.length === 1) { - return strings[0] || undefined; - } - // Join template parts with PostgreSQL placeholders ($1, $2, etc.) - return strings.reduce((acc, str, i) => (i === 0 ? str : `${acc}$${i}${str}`), ''); - } - - /** - * Sanitize SQL query as per the OTEL semantic conventions - * https://opentelemetry.io/docs/specs/semconv/database/database-spans/#sanitization-of-dbquerytext - * - * PostgreSQL $n placeholders are preserved per OTEL spec - they're parameterized queries, - * not sensitive literals. Only actual values (strings, numbers, booleans) are sanitized. - */ - private _sanitizeSqlQuery(sqlQuery: string | undefined): string { - if (!sqlQuery) { - return 'Unknown SQL Query'; - } - - return ( - sqlQuery - // Remove comments first (they may contain newlines and extra spaces) - .replace(/--.*$/gm, '') // Single line comments (multiline mode) - .replace(/\/\*[\s\S]*?\*\//g, '') // Multi-line comments - .replace(/;\s*$/, '') // Remove trailing semicolons - // Collapse whitespace to a single space (after removing comments) - .replace(/\s+/g, ' ') - .trim() // Remove extra spaces and trim - // Sanitize hex/binary literals before string literals - .replace(/\bX'[0-9A-Fa-f]*'/gi, '?') // Hex string literals - .replace(/\bB'[01]*'/gi, '?') // Binary string literals - // Sanitize string literals (handles escaped quotes) - .replace(/'(?:[^']|'')*'/g, '?') - // Sanitize hex numbers - .replace(/\b0x[0-9A-Fa-f]+/gi, '?') - // Sanitize boolean literals - .replace(/\b(?:TRUE|FALSE)\b/gi, '?') - // Sanitize numeric literals (preserve $n placeholders via negative lookbehind) - .replace(/-?\b\d+\.?\d*[eE][+-]?\d+\b/g, '?') // Scientific notation - .replace(/-?\b\d+\.\d+\b/g, '?') // Decimals - .replace(/-?\.\d+\b/g, '?') // Decimals starting with dot - .replace(/(? Promise) & { - __sentry_original__?: (...args: unknown[]) => Promise; - }; - }; - }; - }): typeof moduleExports { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - const originalHandle = moduleExports.Query.prototype.handle; - - moduleExports.Query.prototype.handle = async function ( - this: { - resolve: unknown; - reject: unknown; - strings?: string[]; - executed?: boolean; - }, - ...args: unknown[] - ): Promise { - // Skip if this query came from an instrumented sql instance (already handled by wrapper), - // or if handle() was already called (postgres.js calls handle() from then/catch/finally — - // only the first call executes SQL, subsequent calls are no-ops). - if (this.executed || (this as Record)[QUERY_FROM_INSTRUMENTED_SQL]) { - return originalHandle.apply(this, args); - } - - // Skip if we shouldn't create spans - if (!self._shouldCreateSpans()) { - return originalHandle.apply(this, args); - } - - const fullQuery = self._reconstructQuery(this.strings); - const sanitizedSqlQuery = self._sanitizeSqlQuery(fullQuery); - - return startSpanManual( - { - name: sanitizedSqlQuery || 'postgresjs.query', - op: 'db', - }, - (span: Span) => { - addOriginToSpan(span, 'auto.db.postgresjs'); - - span.setAttributes({ - [DB_SYSTEM_NAME]: 'postgres', - [DB_QUERY_TEXT]: sanitizedSqlQuery, - }); - - // Note: No connection context available for pre-existing instances - // because the sql instance wasn't created through our instrumented wrapper - - const config = self.getConfig(); - const { requestHook } = config; - if (requestHook) { - safeExecuteInTheMiddle( - () => requestHook(span, sanitizedSqlQuery, undefined), - e => { - if (e) { - span.setAttribute('sentry.hook.error', 'requestHook failed'); - DEBUG_BUILD && debug.error(`Error in requestHook for ${INTEGRATION_NAME} integration:`, e); - } - }, - true, - ); - } - - // Wrap resolve to end span on success - const originalResolve = this.resolve; - this.resolve = new Proxy(originalResolve as (...args: unknown[]) => unknown, { - apply: (resolveTarget, resolveThisArg, resolveArgs: [{ command?: string }]) => { - try { - self._setOperationName(span, sanitizedSqlQuery, resolveArgs?.[0]?.command); - span.end(); - } catch (e) { - DEBUG_BUILD && debug.error('Error ending span in resolve callback:', e); - } - return Reflect.apply(resolveTarget, resolveThisArg, resolveArgs); - }, - }); - - // Wrap reject to end span on error - const originalReject = this.reject; - this.reject = new Proxy(originalReject as (...args: unknown[]) => unknown, { - apply: (rejectTarget, rejectThisArg, rejectArgs: { message?: string; code?: string; name?: string }[]) => { - try { - span.setStatus({ - code: SPAN_STATUS_ERROR, - message: rejectArgs?.[0]?.message || 'unknown_error', - }); - span.setAttribute(ATTR_DB_RESPONSE_STATUS_CODE, rejectArgs?.[0]?.code || 'unknown'); - span.setAttribute(ERROR_TYPE, rejectArgs?.[0]?.name || 'unknown'); - self._setOperationName(span, sanitizedSqlQuery); - span.end(); - } catch (e) { - DEBUG_BUILD && debug.error('Error ending span in reject callback:', e); - } - return Reflect.apply(rejectTarget, rejectThisArg, rejectArgs); - }, - }); - - try { - return originalHandle.apply(this, args); - } catch (e) { - span.setStatus({ - code: SPAN_STATUS_ERROR, - message: e instanceof Error ? e.message : 'unknown_error', - }); - span.end(); - throw e; - } - }, - ); - }; - - // Store original for unpatch - must be set on the NEW patched function - moduleExports.Query.prototype.handle.__sentry_original__ = originalHandle; - - return moduleExports; - } - - /** - * Restores the original Query.prototype.handle method. - */ - private _unpatchQueryPrototype(moduleExports: { - Query: { - prototype: { - handle: ((...args: unknown[]) => Promise) & { - __sentry_original__?: (...args: unknown[]) => Promise; - }; - }; - }; - }): typeof moduleExports { - if (moduleExports.Query.prototype.handle.__sentry_original__) { - moduleExports.Query.prototype.handle = moduleExports.Query.prototype.handle.__sentry_original__; - } - return moduleExports; - } -} diff --git a/packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts b/packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts deleted file mode 100644 index a3db6ff01edd..000000000000 --- a/packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/instrumentation-ioredis-v0.62.0/packages/instrumentation-ioredis - * - Upstream version: @opentelemetry/instrumentation-ioredis@0.62.0 - * - Minor TypeScript adjustments for this repository's compiler settings - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - */ - -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import type { Span, SpanAttributes } from '@sentry/core'; -import { - getActiveSpan, - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startInactiveSpan, -} from '@sentry/core'; -import { DB_STATEMENT, DB_SYSTEM, NET_PEER_NAME, NET_PEER_PORT, SENTRY_KIND } from '@sentry/conventions/attributes'; -import { defaultDbStatementSerializer } from '@sentry/server-utils'; -import { ATTR_DB_CONNECTION_STRING, DB_SYSTEM_VALUE_REDIS } from './semconv'; -import type { IORedisInstrumentationConfig } from './types'; - -const PACKAGE_NAME = '@sentry/instrumentation-ioredis'; -const ORIGIN = 'auto.db.otel.redis'; - -// ioredis >= 5.11.0 publishes via diagnostics_channel, which Sentry subscribes -// to separately, so this monkey-patching instrumentation only covers < 5.11.0. -const SUPPORTED_VERSIONS = ['>=2.0.0 <5.11.0']; - -// The raw imported `ioredis` module is either the CommonJS export or an ESM -// namespace wrapping it on `.default`. Typed shallowly since it is only used -// internally to reach the `Redis` prototype that holds the methods we patch. -type IORedisModule = { - default?: { prototype: RedisPrototype }; - prototype: RedisPrototype; - [Symbol.toStringTag]?: string; -}; - -interface RedisPrototype { - sendCommand: (...args: unknown[]) => unknown; - connect: (...args: unknown[]) => unknown; -} - -// The `this` of the patched methods is a Redis client instance exposing its -// connection options. -interface RedisClient { - options: { host?: string; port?: number }; -} - -// The in-flight command object ioredis passes to `sendCommand`. We swap its -// `resolve`/`reject` so the span ends when the command settles. -interface RedisCommand { - name: string; - args: Array; - resolve: (result: unknown) => void; - reject: (err: Error) => void; -} - -function endSpan(span: Span, err: Error | null | undefined): void { - if (err) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: err.message }); - } - span.end(); -} - -export class IORedisInstrumentation extends InstrumentationBase { - public constructor(config: IORedisInstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - protected init(): InstrumentationNodeModuleDefinition[] { - return [ - new InstrumentationNodeModuleDefinition( - 'ioredis', - SUPPORTED_VERSIONS, - (module: IORedisModule) => { - const moduleExports = module[Symbol.toStringTag] === 'Module' && module.default ? module.default : module; - if (isWrapped(moduleExports.prototype.sendCommand)) { - this._unwrap(moduleExports.prototype, 'sendCommand'); - } - this._wrap(moduleExports.prototype, 'sendCommand', this._patchSendCommand()); - if (isWrapped(moduleExports.prototype.connect)) { - this._unwrap(moduleExports.prototype, 'connect'); - } - this._wrap(moduleExports.prototype, 'connect', this._patchConnection()); - return module; - }, - (module: IORedisModule | undefined) => { - if (module === undefined) return; - const moduleExports = module[Symbol.toStringTag] === 'Module' && module.default ? module.default : module; - this._unwrap(moduleExports.prototype, 'sendCommand'); - this._unwrap(moduleExports.prototype, 'connect'); - }, - ), - ]; - } - - private _patchSendCommand() { - const instrumentation = this; - return (original: (...args: unknown[]) => unknown) => { - return function (this: RedisClient, ...args: unknown[]): unknown { - const cmd = args[0] as RedisCommand | undefined; - // ioredis only creates a span when there is an active parent span - // (the upstream `requireParentSpan` default, which Sentry never overrides). - if (args.length < 1 || typeof cmd !== 'object' || !getActiveSpan()) { - return original.apply(this, args); - } - - const { host, port } = this.options; - const attributes: SpanAttributes = { - [SENTRY_KIND]: 'client', - // oxlint-disable-next-line typescript/no-deprecated - [DB_SYSTEM]: DB_SYSTEM_VALUE_REDIS, - // oxlint-disable-next-line typescript/no-deprecated - [DB_STATEMENT]: defaultDbStatementSerializer(cmd.name, cmd.args), - [ATTR_DB_CONNECTION_STRING]: `redis://${host}:${port}`, - // oxlint-disable-next-line typescript/no-deprecated - [NET_PEER_NAME]: host, - // oxlint-disable-next-line typescript/no-deprecated - [NET_PEER_PORT]: port, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - }; - - const span = startInactiveSpan({ name: cmd.name, attributes }); - - try { - const result = original.apply(this, args); - const origResolve = cmd.resolve; - cmd.resolve = function (response: unknown): void { - instrumentation._callResponseHook(span, cmd, response); - endSpan(span, null); - origResolve(response); - }; - const origReject = cmd.reject; - cmd.reject = function (err: Error): void { - endSpan(span, err); - origReject(err); - }; - return result; - } catch (error) { - endSpan(span, error as Error); - throw error; - } - }; - }; - } - - private _patchConnection() { - return (original: (...args: unknown[]) => unknown) => { - return function (this: RedisClient, ...args: unknown[]): unknown { - if (!getActiveSpan()) { - return original.apply(this, args); - } - - const { host, port } = this.options; - const attributes: SpanAttributes = { - [SENTRY_KIND]: 'client', - // oxlint-disable-next-line typescript/no-deprecated - [DB_SYSTEM]: DB_SYSTEM_VALUE_REDIS, - // oxlint-disable-next-line typescript/no-deprecated - [DB_STATEMENT]: 'connect', - [ATTR_DB_CONNECTION_STRING]: `redis://${host}:${port}`, - // oxlint-disable-next-line typescript/no-deprecated - [NET_PEER_NAME]: host, - // oxlint-disable-next-line typescript/no-deprecated - [NET_PEER_PORT]: port, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - }; - - const span = startInactiveSpan({ name: 'connect', attributes }); - - try { - const result = original.apply(this, args) as Promise | undefined; - if (result instanceof Promise) { - return result.then( - (value: unknown) => { - endSpan(span, null); - return value; - }, - (error: Error) => { - endSpan(span, error); - return Promise.reject(error); - }, - ); - } - endSpan(span, null); - return result; - } catch (error) { - endSpan(span, error as Error); - throw error; - } - }; - }; - } - - private _callResponseHook(span: Span, cmd: RedisCommand, response: unknown): void { - const { responseHook } = this.getConfig(); - if (!responseHook) { - return; - } - try { - responseHook(span, cmd.name, cmd.args, response); - } catch { - // ignore errors thrown from the user-provided response hook - } - } -} diff --git a/packages/node/src/integrations/tracing/redis/vendored/redis-instrumentation.ts b/packages/node/src/integrations/tracing/redis/vendored/redis-instrumentation.ts deleted file mode 100644 index 0f474564c136..000000000000 --- a/packages/node/src/integrations/tracing/redis/vendored/redis-instrumentation.ts +++ /dev/null @@ -1,562 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/instrumentation-redis-v0.62.0/packages/instrumentation-redis - * - Upstream version: @opentelemetry/instrumentation-redis@0.62.0 - * - Minor TypeScript adjustments for this repository's compiler settings - * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs - */ - -import type { TracerProvider } from '@opentelemetry/api'; -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import type { Span, SpanAttributes } from '@sentry/core'; -import { - debug, - getActiveSpan, - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startInactiveSpan, - withActiveSpan, -} from '@sentry/core'; -import { DB_STATEMENT, DB_SYSTEM, NET_PEER_NAME, NET_PEER_PORT, SENTRY_KIND } from '@sentry/conventions/attributes'; -import { defaultDbStatementSerializer } from '@sentry/server-utils'; -import { DEBUG_BUILD } from '../../../../debug-build'; -import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile'; -import { ATTR_DB_CONNECTION_STRING, DB_SYSTEM_VALUE_REDIS } from './semconv'; -import type { RedisInstrumentationConfig, RedisResponseCustomAttributeFunction } from './types'; - -const PACKAGE_NAME = '@sentry/instrumentation-redis'; -const ORIGIN = 'auto.db.otel.redis'; - -// node-redis >= 5.12.0 publishes via diagnostics_channel, which Sentry subscribes -// to separately, so this monkey-patching instrumentation only covers the older -// `redis` (v2-v3), `@redis/client`/`@node-redis/client` (v1, i.e. node-redis v4) -// and node-redis 5.0-5.11 releases. - -// ---- Internal types ---- - -interface RedisPluginClientTypes { - connection_options?: { - port?: string | number; - host?: string; - }; - address?: string; -} - -interface RedisCommand { - command: string; - args: string[]; - buffer_args: boolean; - callback: (err: Error | null, reply: unknown) => void; - call_on_write: boolean; -} - -interface MultiErrorReply extends Error { - replies: unknown[]; - errorIndexes: Array; -} - -interface OpenSpanInfo { - span: Span; - commandName: string; - commandArgs: Array; -} - -const OTEL_OPEN_SPANS = Symbol('opentelemetry.instrumentation.redis.open_spans'); -const MULTI_COMMAND_OPTIONS = Symbol('opentelemetry.instrumentation.redis.multi_command_options'); - -// ---- shared utils ---- - -function endSpan(span: Span, err: Error | null | undefined): void { - if (err) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: err.message }); - } - span.end(); -} - -function runResponseHook( - responseHook: RedisResponseCustomAttributeFunction | undefined, - span: Span, - commandName: string, - commandArgs: Array, - response: unknown, -): void { - if (!responseHook) { - return; - } - try { - responseHook(span, commandName, commandArgs, response); - } catch { - // never let a user-provided response hook break instrumentation - } -} - -// ---- v4-v5 utils ---- - -function removeCredentialsFromDBConnectionStringAttribute(url: string | undefined): string | undefined { - if (typeof url !== 'string' || !url) { - return undefined; - } - try { - const u = new URL(url); - u.searchParams.delete('user_pwd'); - u.username = ''; - u.password = ''; - return u.href; - } catch (err) { - DEBUG_BUILD && debug.error('failed to sanitize redis connection url', err); - } - return undefined; -} - -function getClientAttributes(options: any): SpanAttributes { - return { - [SENTRY_KIND]: 'client', - // oxlint-disable-next-line typescript/no-deprecated - [DB_SYSTEM]: DB_SYSTEM_VALUE_REDIS, - // oxlint-disable-next-line typescript/no-deprecated - [NET_PEER_NAME]: options?.socket?.host, - // oxlint-disable-next-line typescript/no-deprecated - [NET_PEER_PORT]: options?.socket?.port, - [ATTR_DB_CONNECTION_STRING]: removeCredentialsFromDBConnectionStringAttribute(options?.url), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - }; -} - -// ---- RedisInstrumentationV2_V3 ---- - -class RedisInstrumentationV2_V3 extends InstrumentationBase { - static COMPONENT = 'redis'; - - constructor(config: RedisInstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - init() { - return [ - new InstrumentationNodeModuleDefinition( - 'redis', - ['>=2.6.0 <4'], - (moduleExports: any) => { - if (isWrapped(moduleExports.RedisClient.prototype['internal_send_command'])) { - this._unwrap(moduleExports.RedisClient.prototype, 'internal_send_command'); - } - this._wrap(moduleExports.RedisClient.prototype, 'internal_send_command', this._getPatchInternalSendCommand()); - return moduleExports; - }, - (moduleExports: any) => { - if (moduleExports === undefined) return; - this._unwrap(moduleExports.RedisClient.prototype, 'internal_send_command'); - }, - ), - ]; - } - - private _getPatchInternalSendCommand() { - const instrumentation = this; - return function internal_send_command(original: Function) { - return function internal_send_command_trace(this: RedisPluginClientTypes, cmd: RedisCommand) { - if (arguments.length !== 1 || typeof cmd !== 'object') { - return original.apply(this, arguments); - } - const attributes: SpanAttributes = { - [SENTRY_KIND]: 'client', - // oxlint-disable-next-line typescript/no-deprecated - [DB_SYSTEM]: DB_SYSTEM_VALUE_REDIS, - // oxlint-disable-next-line typescript/no-deprecated - [DB_STATEMENT]: defaultDbStatementSerializer(cmd.command, cmd.args), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - }; - if (this.connection_options) { - // oxlint-disable-next-line typescript/no-deprecated - attributes[NET_PEER_NAME] = this.connection_options.host; - // oxlint-disable-next-line typescript/no-deprecated - attributes[NET_PEER_PORT] = this.connection_options.port; - } - if (this.address) { - attributes[ATTR_DB_CONNECTION_STRING] = `redis://${this.address}`; - } - const span = startInactiveSpan({ - name: `${RedisInstrumentationV2_V3.COMPONENT}-${cmd.command}`, - attributes, - }); - const originalCallback = arguments[0].callback; - if (originalCallback) { - const parentSpan = getActiveSpan(); - arguments[0].callback = function callback(this: any, err: Error | null, reply: unknown) { - runResponseHook(instrumentation.getConfig().responseHook, span, cmd.command, cmd.args, reply); - endSpan(span, err); - return withActiveSpan(parentSpan ?? null, () => originalCallback.apply(this, arguments)); - }; - } - try { - return original.apply(this, arguments); - } catch (rethrow) { - endSpan(span, rethrow as Error); - throw rethrow; - } - }; - }; - } -} - -// ---- RedisInstrumentationV4_V5 ---- - -class RedisInstrumentationV4_V5 extends InstrumentationBase { - static COMPONENT = 'redis'; - - constructor(config: RedisInstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - init() { - return [ - this._getInstrumentationNodeModuleDefinition('@redis/client'), - this._getInstrumentationNodeModuleDefinition('@node-redis/client'), - ]; - } - - private _getInstrumentationNodeModuleDefinition(basePackageName: string) { - const commanderModuleFile = new InstrumentationNodeModuleFile( - `${basePackageName}/dist/lib/commander.js`, - ['^1.0.0'], - (moduleExports: any, moduleVersion?: string) => { - const transformCommandArguments = moduleExports.transformCommandArguments; - if (!transformCommandArguments) { - DEBUG_BUILD && debug.error('internal instrumentation error, missing transformCommandArguments function'); - return moduleExports; - } - const functionToPatch = moduleVersion?.startsWith('1.0.') ? 'extendWithCommands' : 'attachCommands'; - if (isWrapped(moduleExports?.[functionToPatch])) { - this._unwrap(moduleExports, functionToPatch); - } - this._wrap(moduleExports, functionToPatch, this._getPatchExtendWithCommands(transformCommandArguments)); - return moduleExports; - }, - (moduleExports: any) => { - if (isWrapped(moduleExports?.extendWithCommands)) { - this._unwrap(moduleExports, 'extendWithCommands'); - } - if (isWrapped(moduleExports?.attachCommands)) { - this._unwrap(moduleExports, 'attachCommands'); - } - }, - ); - - const multiCommanderModule = new InstrumentationNodeModuleFile( - `${basePackageName}/dist/lib/client/multi-command.js`, - ['^1.0.0', '>=5.0.0 <5.12.0'], - (moduleExports: any) => { - const redisClientMultiCommandPrototype = moduleExports?.default?.prototype; - if (isWrapped(redisClientMultiCommandPrototype?.exec)) { - this._unwrap(redisClientMultiCommandPrototype, 'exec'); - } - this._wrap(redisClientMultiCommandPrototype, 'exec', this._getPatchMultiCommandsExec()); - if (isWrapped(redisClientMultiCommandPrototype?.execAsPipeline)) { - this._unwrap(redisClientMultiCommandPrototype, 'execAsPipeline'); - } - this._wrap(redisClientMultiCommandPrototype, 'execAsPipeline', this._getPatchMultiCommandsExec()); - if (isWrapped(redisClientMultiCommandPrototype?.addCommand)) { - this._unwrap(redisClientMultiCommandPrototype, 'addCommand'); - } - this._wrap(redisClientMultiCommandPrototype, 'addCommand', this._getPatchMultiCommandsAddCommand()); - return moduleExports; - }, - (moduleExports: any) => { - const redisClientMultiCommandPrototype = moduleExports?.default?.prototype; - if (isWrapped(redisClientMultiCommandPrototype?.exec)) { - this._unwrap(redisClientMultiCommandPrototype, 'exec'); - } - if (isWrapped(redisClientMultiCommandPrototype?.execAsPipeline)) { - this._unwrap(redisClientMultiCommandPrototype, 'execAsPipeline'); - } - if (isWrapped(redisClientMultiCommandPrototype?.addCommand)) { - this._unwrap(redisClientMultiCommandPrototype, 'addCommand'); - } - }, - ); - - const clientIndexModule = new InstrumentationNodeModuleFile( - `${basePackageName}/dist/lib/client/index.js`, - ['^1.0.0', '>=5.0.0 <5.12.0'], - (moduleExports: any) => { - const redisClientPrototype = moduleExports?.default?.prototype; - if (redisClientPrototype?.multi) { - if (isWrapped(redisClientPrototype?.multi)) { - this._unwrap(redisClientPrototype, 'multi'); - } - this._wrap(redisClientPrototype, 'multi', this._getPatchRedisClientMulti()); - } - if (redisClientPrototype?.MULTI) { - if (isWrapped(redisClientPrototype?.MULTI)) { - this._unwrap(redisClientPrototype, 'MULTI'); - } - this._wrap(redisClientPrototype, 'MULTI', this._getPatchRedisClientMulti()); - } - if (isWrapped(redisClientPrototype?.sendCommand)) { - this._unwrap(redisClientPrototype, 'sendCommand'); - } - this._wrap(redisClientPrototype, 'sendCommand', this._getPatchRedisClientSendCommand()); - if (isWrapped(redisClientPrototype?.connect)) { - this._unwrap(redisClientPrototype, 'connect'); - } - this._wrap(redisClientPrototype, 'connect', this._getPatchedClientConnect()); - return moduleExports; - }, - (moduleExports: any) => { - const redisClientPrototype = moduleExports?.default?.prototype; - if (isWrapped(redisClientPrototype?.multi)) { - this._unwrap(redisClientPrototype, 'multi'); - } - if (isWrapped(redisClientPrototype?.MULTI)) { - this._unwrap(redisClientPrototype, 'MULTI'); - } - if (isWrapped(redisClientPrototype?.sendCommand)) { - this._unwrap(redisClientPrototype, 'sendCommand'); - } - if (isWrapped(redisClientPrototype?.connect)) { - this._unwrap(redisClientPrototype, 'connect'); - } - }, - ); - - return new InstrumentationNodeModuleDefinition( - basePackageName, - ['^1.0.0', '>=5.0.0 <5.12.0'], - (moduleExports: any) => moduleExports, - () => {}, - [commanderModuleFile, multiCommanderModule, clientIndexModule], - ); - } - - private _getPatchExtendWithCommands(transformCommandArguments: Function) { - const plugin = this; - return function extendWithCommandsPatchWrapper(original: Function) { - return function extendWithCommandsPatch(this: any, config: any) { - if (config?.BaseClass?.name !== 'RedisClient') { - return original.apply(this, arguments); - } - const origExecutor = config.executor; - config.executor = function (this: any, command: any, args: any) { - const redisCommandArguments = transformCommandArguments(command, args).args; - return plugin._traceClientCommand(origExecutor, this, arguments, redisCommandArguments); - }; - return original.apply(this, arguments); - }; - }; - } - - private _getPatchMultiCommandsExec() { - const plugin = this; - return function execPatchWrapper(original: Function) { - return function execPatch(this: any) { - const execRes = original.apply(this, arguments); - if (typeof execRes?.then !== 'function') { - DEBUG_BUILD && debug.error('non-promise result when patching exec/execAsPipeline'); - return execRes; - } - return execRes - .then((redisRes: unknown[]) => { - const openSpans: OpenSpanInfo[] = this[OTEL_OPEN_SPANS]; - plugin._endSpansWithRedisReplies(openSpans, redisRes); - return redisRes; - }) - .catch((err: any) => { - const openSpans: OpenSpanInfo[] = this[OTEL_OPEN_SPANS]; - if (!openSpans) { - DEBUG_BUILD && debug.error('cannot find open spans to end for multi/pipeline'); - } else { - const replies = - err.constructor.name === 'MultiErrorReply' - ? (err as MultiErrorReply).replies - : new Array(openSpans.length).fill(err); - plugin._endSpansWithRedisReplies(openSpans, replies); - } - return Promise.reject(err); - }); - }; - }; - } - - private _getPatchMultiCommandsAddCommand() { - const plugin = this; - return function addCommandWrapper(original: Function) { - return function addCommandPatch(this: any, args: any) { - return plugin._traceClientCommand(original, this, arguments, args); - }; - }; - } - - private _getPatchRedisClientMulti() { - return function multiPatchWrapper(original: Function) { - return function multiPatch(this: any) { - const multiRes: any = original.apply(this, arguments); - multiRes[MULTI_COMMAND_OPTIONS] = this.options; - return multiRes; - }; - }; - } - - private _getPatchRedisClientSendCommand() { - const plugin = this; - return function sendCommandWrapper(original: Function) { - return function sendCommandPatch(this: any, args: any) { - return plugin._traceClientCommand(original, this, arguments, args); - }; - }; - } - - private _getPatchedClientConnect() { - return function connectWrapper(original: Function) { - return function patchedConnect(this: any) { - const attributes = getClientAttributes(this.options); - const span = startInactiveSpan({ - name: `${RedisInstrumentationV4_V5.COMPONENT}-connect`, - attributes, - }); - const res = withActiveSpan(span, () => original.apply(this)); - return res.then( - (result: any) => { - span.end(); - return result; - }, - (error: Error) => { - endSpan(span, error); - return Promise.reject(error); - }, - ); - }; - }; - } - - _traceClientCommand( - origFunction: Function, - origThis: any, - origArguments: IArguments, - redisCommandArguments: Array, - ): any { - const clientOptions = origThis.options || origThis[MULTI_COMMAND_OPTIONS]; - const commandName = redisCommandArguments[0] as string; - const commandArgs = redisCommandArguments.slice(1); - const attributes = getClientAttributes(clientOptions); - const dbStatement = defaultDbStatementSerializer(commandName, commandArgs); - if (dbStatement != null) { - // oxlint-disable-next-line typescript/no-deprecated - attributes[DB_STATEMENT] = dbStatement; - } - const span = startInactiveSpan({ - name: `${RedisInstrumentationV4_V5.COMPONENT}-${commandName}`, - attributes, - }); - const res = withActiveSpan(span, () => origFunction.apply(origThis, origArguments)); - if (res instanceof Promise) { - res.then( - (redisRes: unknown) => { - this._endSpanWithResponse(span, commandName, commandArgs, redisRes, undefined); - }, - (err: Error) => { - this._endSpanWithResponse(span, commandName, commandArgs, null, err); - }, - ); - } else { - const redisClientMultiCommand: any = res; - redisClientMultiCommand[OTEL_OPEN_SPANS] = redisClientMultiCommand[OTEL_OPEN_SPANS] || []; - redisClientMultiCommand[OTEL_OPEN_SPANS].push({ - span, - commandName, - commandArgs, - }); - } - return res; - } - - _endSpansWithRedisReplies(openSpans: OpenSpanInfo[] | undefined, replies: unknown[]): void { - if (!openSpans) { - DEBUG_BUILD && debug.error('cannot find open spans to end for redis multi/pipeline'); - return; - } - if (replies.length !== openSpans.length) { - DEBUG_BUILD && debug.error('number of multi command spans does not match response from redis'); - return; - } - for (let i = 0; i < openSpans.length; i++) { - const { span, commandName, commandArgs } = openSpans[i]!; - const currCommandRes = replies[i]; - const [res, err] = currCommandRes instanceof Error ? [null, currCommandRes] : [currCommandRes, undefined]; - this._endSpanWithResponse(span, commandName, commandArgs, res, err); - } - } - - _endSpanWithResponse( - span: Span, - commandName: string, - commandArgs: Array, - response: unknown, - error: Error | null | undefined, - ): void { - if (!error) { - runResponseHook(this.getConfig().responseHook, span, commandName, commandArgs, response); - } - endSpan(span, error); - } -} - -// ---- RedisInstrumentation (wrapper) ---- - -export class RedisInstrumentation extends InstrumentationBase { - private instrumentationV2_V3: RedisInstrumentationV2_V3; - private instrumentationV4_V5: RedisInstrumentationV4_V5; - private initialized = false; - - constructor(config: RedisInstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - this.instrumentationV2_V3 = new RedisInstrumentationV2_V3(this.getConfig()); - this.instrumentationV4_V5 = new RedisInstrumentationV4_V5(this.getConfig()); - this.initialized = true; - } - - override setConfig(config: RedisInstrumentationConfig = {}): void { - super.setConfig(config); - if (!this.initialized) { - return; - } - this.instrumentationV2_V3.setConfig(config); - this.instrumentationV4_V5.setConfig(config); - } - - init() {} - - override getModuleDefinitions() { - return [...this.instrumentationV2_V3.getModuleDefinitions(), ...this.instrumentationV4_V5.getModuleDefinitions()]; - } - - override setTracerProvider(tracerProvider: TracerProvider): void { - super.setTracerProvider(tracerProvider); - if (!this.initialized) { - return; - } - this.instrumentationV2_V3.setTracerProvider(tracerProvider); - this.instrumentationV4_V5.setTracerProvider(tracerProvider); - } - - override enable(): void { - super.enable(); - if (!this.initialized) { - return; - } - this.instrumentationV2_V3.enable(); - this.instrumentationV4_V5.enable(); - } - - override disable(): void { - super.disable(); - if (!this.initialized) { - return; - } - this.instrumentationV2_V3.disable(); - this.instrumentationV4_V5.disable(); - } -} diff --git a/packages/node/src/integrations/tracing/redis/vendored/semconv.ts b/packages/node/src/integrations/tracing/redis/vendored/semconv.ts deleted file mode 100644 index 45dfb56ecfa6..000000000000 --- a/packages/node/src/integrations/tracing/redis/vendored/semconv.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/instrumentation-redis-v0.62.0/packages/instrumentation-redis - * - Upstream version: @opentelemetry/instrumentation-redis@0.62.0 - * - Minor TypeScript adjustments for this repository's compiler settings - */ -/* eslint-disable -- vendored @opentelemetry/instrumentation-redis */ - -/* - * This file contains a copy of unstable semantic convention definitions - * used by the vendored redis/ioredis instrumentations. - * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv - */ - -// Deprecated constants kept for backwards compatibility with older semconv -export const ATTR_DB_CONNECTION_STRING = 'db.connection_string'; -export const DB_SYSTEM_NAME_VALUE_REDIS = 'redis'; -export const DB_SYSTEM_VALUE_REDIS = 'redis'; diff --git a/packages/node/src/integrations/tracing/tedious/index.ts b/packages/node/src/integrations/tracing/tedious/index.ts deleted file mode 100644 index 521faf0cd9a8..000000000000 --- a/packages/node/src/integrations/tracing/tedious/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TediousInstrumentation } from './vendored/instrumentation'; -import { generateInstrumentOnce } from '../../../otel/instrument'; - -const INTEGRATION_NAME = 'Tedious' as const; - -export const instrumentTedious = generateInstrumentOnce(INTEGRATION_NAME, () => new TediousInstrumentation({})); diff --git a/packages/node/src/integrations/tracing/tedious/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/tedious/vendored/instrumentation.ts deleted file mode 100644 index a4ae168e849d..000000000000 --- a/packages/node/src/integrations/tracing/tedious/vendored/instrumentation.ts +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-tedious - * - Upstream version: @opentelemetry/instrumentation-tedious@0.37.0 - * - Minor TypeScript strictness adjustments - * - Span creation migrated to the @sentry/core API; origin folded into span creation - */ - -import { EventEmitter } from 'events'; -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; -import { - DB_NAME, - DB_STATEMENT, - DB_SYSTEM, - DB_USER, - NET_PEER_NAME, - NET_PEER_PORT, - SENTRY_KIND, -} from '@sentry/conventions/attributes'; -import { DB_SYSTEM_VALUE_MSSQL, ATTR_DB_SQL_TABLE } from './semconv'; -import type * as tedious from './tedious-types'; -import { getSpanName, once } from './utils'; -import type { SpanAttributes } from '@sentry/core'; -import { - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startInactiveSpan, - withActiveSpan, -} from '@sentry/core'; - -const PACKAGE_NAME = '@sentry/instrumentation-tedious'; - -const CURRENT_DATABASE = Symbol('opentelemetry.instrumentation-tedious.current-database'); - -const PATCHED_METHODS = ['callProcedure', 'execSql', 'execSqlBatch', 'execBulkLoad', 'prepare', 'execute']; - -type UnknownFunction = (...args: any[]) => any; -type ApproxConnection = EventEmitter & { - [CURRENT_DATABASE]: string; - config: any; -}; -type ApproxRequest = EventEmitter & { - sqlTextOrProcedure: string | undefined; - callback: any; - table: string | undefined; - parametersByName: any; -}; - -function setDatabase(this: ApproxConnection, databaseName: string) { - Object.defineProperty(this, CURRENT_DATABASE, { - value: databaseName, - writable: true, - }); -} - -export class TediousInstrumentation extends InstrumentationBase { - static readonly COMPONENT = 'tedious'; - - constructor(config: InstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, config); - } - - protected init() { - return [ - new InstrumentationNodeModuleDefinition( - TediousInstrumentation.COMPONENT, - ['>=1.11.0 <20'], - (moduleExports: typeof tedious) => { - const ConnectionPrototype: any = moduleExports.Connection.prototype; - for (const method of PATCHED_METHODS) { - if (isWrapped(ConnectionPrototype[method])) { - this._unwrap(ConnectionPrototype, method); - } - this._wrap(ConnectionPrototype, method, this._patchQuery(method) as any); - } - - if (isWrapped(ConnectionPrototype.connect)) { - this._unwrap(ConnectionPrototype, 'connect'); - } - // oxlint-disable-next-line typescript/unbound-method - this._wrap(ConnectionPrototype, 'connect', this._patchConnect); - - return moduleExports; - }, - (moduleExports: typeof tedious) => { - if (moduleExports === undefined) return; - const ConnectionPrototype: any = moduleExports.Connection.prototype; - for (const method of PATCHED_METHODS) { - this._unwrap(ConnectionPrototype, method); - } - this._unwrap(ConnectionPrototype, 'connect'); - }, - ), - ]; - } - - private _patchConnect(original: UnknownFunction): UnknownFunction { - return function patchedConnect(this: ApproxConnection) { - setDatabase.call(this, this.config?.options?.database); - - // remove the listener first in case it's already added - this.removeListener('databaseChange', setDatabase); - this.on('databaseChange', setDatabase); - - this.once('end', () => { - this.removeListener('databaseChange', setDatabase); - }); - return original.apply(this, arguments as unknown as any[]); - }; - } - - private _patchQuery(operation: string) { - return (originalMethod: UnknownFunction): UnknownFunction => { - const thisPlugin = this; - - function patchedMethod(this: ApproxConnection, request: ApproxRequest) { - if (!(request instanceof EventEmitter)) { - thisPlugin._diag.warn(`Unexpected invocation of patched ${operation} method. Span not recorded`); - return originalMethod.apply(this, arguments as unknown as any[]); - } - let procCount = 0; - let statementCount = 0; - const incrementStatementCount = () => statementCount++; - const incrementProcCount = () => procCount++; - const databaseName = this[CURRENT_DATABASE]; - const sql = (request => { - // Required for <11.0.9 - if (request.sqlTextOrProcedure === 'sp_prepare' && request.parametersByName?.stmt?.value) { - return request.parametersByName.stmt.value; - } - return request.sqlTextOrProcedure; - })(request); - - const attributes: SpanAttributes = { - [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.otel.tedious', - // eslint-disable-next-line typescript/no-deprecated - [DB_SYSTEM]: DB_SYSTEM_VALUE_MSSQL, - // eslint-disable-next-line typescript/no-deprecated - [DB_NAME]: databaseName, - // >=4 uses `authentication` object; older versions just userName and password pair - // eslint-disable-next-line typescript/no-deprecated - [DB_USER]: this.config?.userName ?? this.config?.authentication?.options?.userName, - // eslint-disable-next-line typescript/no-deprecated - [DB_STATEMENT]: sql, - // eslint-disable-next-line typescript/no-deprecated - [ATTR_DB_SQL_TABLE]: request.table, - // eslint-disable-next-line typescript/no-deprecated - [NET_PEER_NAME]: this.config?.server, - // eslint-disable-next-line typescript/no-deprecated - [NET_PEER_PORT]: this.config?.options?.port, - }; - const span = startInactiveSpan({ - name: getSpanName(operation, databaseName, sql, request.table), - attributes, - }); - - const endSpan = once((err?: any) => { - request.removeListener('done', incrementStatementCount); - request.removeListener('doneInProc', incrementStatementCount); - request.removeListener('doneProc', incrementProcCount); - request.removeListener('error', endSpan); - this.removeListener('end', endSpan); - - span.setAttribute('tedious.procedure_count', procCount); - span.setAttribute('tedious.statement_count', statementCount); - if (err) { - span.setStatus({ - code: SPAN_STATUS_ERROR, - message: err.message, - }); - // TODO(3290): set `error.type` attribute? - } - span.end(); - }); - - request.on('done', incrementStatementCount); - request.on('doneInProc', incrementStatementCount); - request.on('doneProc', incrementProcCount); - request.once('error', endSpan); - this.on('end', endSpan); - - if (typeof request.callback === 'function') { - thisPlugin._wrap(request, 'callback', thisPlugin._patchCallbackQuery(endSpan)); - } else { - thisPlugin._diag.error('Expected request.callback to be a function'); - } - - return withActiveSpan(span, () => originalMethod.apply(this, arguments as unknown as any[])); - } - - Object.defineProperty(patchedMethod, 'length', { - value: originalMethod.length, - writable: false, - }); - - return patchedMethod; - }; - } - - private _patchCallbackQuery(endSpan: Function) { - return (originalCallback: Function) => { - return function (this: any, err: Error | undefined | null, _rowCount?: number, _rows?: any) { - endSpan(err); - return originalCallback.apply(this, arguments); - }; - }; - } -} diff --git a/packages/node/src/integrations/tracing/tedious/vendored/semconv.ts b/packages/node/src/integrations/tracing/tedious/vendored/semconv.ts deleted file mode 100644 index da4fe8c816b4..000000000000 --- a/packages/node/src/integrations/tracing/tedious/vendored/semconv.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-tedious - * - Upstream version: @opentelemetry/instrumentation-tedious@0.37.0 - */ - -/* - * This file contains a copy of unstable semantic convention definitions - * used by this package. - * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv - */ - -/** - * Deprecated, use `db.collection.name` instead. - * - * @example "mytable" - * - * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - * - * @deprecated Replaced by `db.collection.name`, but only if not extracting the value from `db.query.text`. - */ -export const ATTR_DB_SQL_TABLE = 'db.sql.table' as const; - -/** - * Enum value "mssql" for attribute `db.system`. - * - * Microsoft SQL Server - * - * @experimental This enum value is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. - */ -export const DB_SYSTEM_VALUE_MSSQL = 'mssql' as const; diff --git a/packages/node/src/integrations/tracing/tedious/vendored/tedious-types.ts b/packages/node/src/integrations/tracing/tedious/vendored/tedious-types.ts deleted file mode 100644 index f51ca322e363..000000000000 --- a/packages/node/src/integrations/tracing/tedious/vendored/tedious-types.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Simplified types inlined from tedious. - */ - -import { EventEmitter } from 'events'; - -export declare class Connection extends EventEmitter { - config: any; - connect(connectListener?: (err?: Error) => void): void; - execSql(request: Request): void; - [key: string]: any; -} - -export declare class Request extends EventEmitter { - sqlTextOrProcedure: string | undefined; - callback: any; - table: string | undefined; - parametersByName: any; - constructor( - sqlTextOrProcedure: string | undefined, - callback: (error: Error | null | undefined, rowCount?: number, rows?: any) => void, - ); - addParameter(name: string, type: any, value?: unknown, options?: any): void; - [key: string]: any; -} - -export declare const TYPES: { - VarBinary: any; - [key: string]: any; -}; diff --git a/packages/node/src/integrations/tracing/tedious/vendored/utils.ts b/packages/node/src/integrations/tracing/tedious/vendored/utils.ts deleted file mode 100644 index 0eb092e5fcff..000000000000 --- a/packages/node/src/integrations/tracing/tedious/vendored/utils.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-tedious - * - Upstream version: @opentelemetry/instrumentation-tedious@0.37.0 - */ -/* eslint-disable */ - -/** - * The span name SHOULD be set to a low cardinality value - * representing the statement executed on the database. - * - * @returns Operation executed on Tedious Connection. Does not map to SQL statement in any way. - */ -export function getSpanName( - operation: string, - db: string | undefined, - sql: string | undefined, - bulkLoadTable: string | undefined, -): string { - if (operation === 'execBulkLoad' && bulkLoadTable && db) { - return `${operation} ${bulkLoadTable} ${db}`; - } - if (operation === 'callProcedure') { - // `sql` refers to procedure name with `callProcedure` - if (db) { - return `${operation} ${sql} ${db}`; - } - return `${operation} ${sql}`; - } - // do not use `sql` in general case because of high-cardinality - if (db) { - return `${operation} ${db}`; - } - return `${operation}`; -} - -export const once = (fn: Function) => { - let called = false; - return (...args: unknown[]) => { - if (called) return; - called = true; - return fn(...args); - }; -}; diff --git a/packages/node/src/integrations/tracing/vercelai/constants.ts b/packages/node/src/integrations/tracing/vercelai/constants.ts deleted file mode 100644 index ff19038a8cb6..000000000000 --- a/packages/node/src/integrations/tracing/vercelai/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const INTEGRATION_NAME = 'VercelAI' as const; diff --git a/packages/node/src/integrations/tracing/vercelai/index.ts b/packages/node/src/integrations/tracing/vercelai/index.ts deleted file mode 100644 index b1a923fec8cf..000000000000 --- a/packages/node/src/integrations/tracing/vercelai/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { generateInstrumentOnce } from '../../../otel/instrument'; -import { INTEGRATION_NAME } from './constants'; -import { SentryVercelAiInstrumentation } from './instrumentation'; - -export const instrumentVercelAi = generateInstrumentOnce(INTEGRATION_NAME, () => new SentryVercelAiInstrumentation({})); diff --git a/packages/node/src/integrations/tracing/vercelai/instrumentation.ts b/packages/node/src/integrations/tracing/vercelai/instrumentation.ts deleted file mode 100644 index 7b3deb2b820f..000000000000 --- a/packages/node/src/integrations/tracing/vercelai/instrumentation.ts +++ /dev/null @@ -1,306 +0,0 @@ -import type { InstrumentationConfig, InstrumentationModuleDefinition } from '@opentelemetry/instrumentation'; -import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; -import { - _INTERNAL_cleanupToolCallSpanContext, - _INTERNAL_getSpanContextForToolCallId, - addNonEnumerableProperty, - captureException, - getActiveSpan, - getClient, - handleCallbackErrors, - SDK_VERSION, - withScope, -} from '@sentry/core'; -import { INTEGRATION_NAME } from './constants'; -import type { TelemetrySettings, VercelAiIntegration } from './types'; - -const SUPPORTED_VERSIONS = ['>=3.0.0 <7']; - -// List of patched methods -// From: https://sdk.vercel.ai/docs/ai-sdk-core/telemetry#collected-data -const INSTRUMENTED_METHODS = [ - 'generateText', - 'streamText', - 'generateObject', - 'streamObject', - 'embed', - 'embedMany', - 'rerank', -] as const; - -interface MethodFirstArg extends Record { - experimental_telemetry?: TelemetrySettings; -} - -type MethodArgs = [MethodFirstArg, ...unknown[]]; - -type PatchedModuleExports = Record<(typeof INSTRUMENTED_METHODS)[number], (...args: MethodArgs) => unknown> & - Record; - -interface RecordingOptions { - recordInputs?: boolean; - recordOutputs?: boolean; -} - -interface ToolError { - type: 'tool-error' | 'tool-result' | 'tool-call'; - toolCallId: string; - toolName: string; - input?: { - [key: string]: unknown; - }; - error: Error; - dynamic?: boolean; -} - -function isToolError(obj: unknown): obj is ToolError { - if (typeof obj !== 'object' || obj === null) { - return false; - } - - const candidate = obj as Record; - return ( - 'type' in candidate && - 'error' in candidate && - 'toolName' in candidate && - 'toolCallId' in candidate && - candidate.type === 'tool-error' && - candidate.error instanceof Error - ); -} - -/** - * Process tool call results: capture tool errors and clean up span context mappings. - * - * Error checking runs first (needs span context for linking), then cleanup removes all entries. - * Tool errors are not rejected in Vercel AI V5 — they appear as metadata in the result content. - */ -export function processToolCallResults(result: unknown): void { - if (typeof result !== 'object' || result === null || !('content' in result)) { - return; - } - - const resultObj = result as { content: Array }; - if (!Array.isArray(resultObj.content)) { - return; - } - - captureToolErrors(resultObj.content); - cleanupToolCallSpanContexts(resultObj.content); -} - -function captureToolErrors(content: Array): void { - for (const item of content) { - if (!isToolError(item)) { - continue; - } - - // Try to get the span context associated with this tool call ID - const spanContext = _INTERNAL_getSpanContextForToolCallId(item.toolCallId); - - if (spanContext) { - // We have the span context, so link the error using span and trace IDs - withScope(scope => { - scope.setContext('trace', { - trace_id: spanContext.traceId, - span_id: spanContext.spanId, - }); - - scope.setTag('vercel.ai.tool.name', item.toolName); - scope.setTag('vercel.ai.tool.callId', item.toolCallId); - scope.setLevel('error'); - - captureException(item.error, { - mechanism: { - type: 'auto.vercelai.otel', - handled: false, - }, - }); - }); - } else { - // Fallback: capture without span linking - withScope(scope => { - scope.setTag('vercel.ai.tool.name', item.toolName); - scope.setTag('vercel.ai.tool.callId', item.toolCallId); - scope.setLevel('error'); - - captureException(item.error, { - mechanism: { - type: 'auto.vercelai.otel', - handled: false, - }, - }); - }); - } - } -} - -/** - * Remove span context entries for all completed tool calls in the content array. - */ -export function cleanupToolCallSpanContexts(content: Array): void { - for (const item of content) { - if ( - typeof item === 'object' && - item !== null && - 'toolCallId' in item && - typeof (item as Record).toolCallId === 'string' - ) { - _INTERNAL_cleanupToolCallSpanContext((item as Record).toolCallId as string); - } - } -} - -/** - * Determines whether to record inputs and outputs for Vercel AI telemetry based on the configuration hierarchy. - * - * The order of precedence is: - * 1. The vercel ai integration options - * 2. The experimental_telemetry options in the vercel ai method calls - * 3. When telemetry is explicitly enabled (isEnabled: true), default to recording - * 4. Otherwise, use the dataCollection.genAI settings from client options - */ -export function determineRecordingSettings( - integrationRecordingOptions: RecordingOptions | undefined, - methodTelemetryOptions: RecordingOptions, - telemetryExplicitlyEnabled: boolean | undefined, - defaultInputsEnabled: boolean, - defaultOutputsEnabled: boolean, -): { recordInputs: boolean; recordOutputs: boolean } { - const recordInputs = - integrationRecordingOptions?.recordInputs !== undefined - ? integrationRecordingOptions.recordInputs - : methodTelemetryOptions.recordInputs !== undefined - ? methodTelemetryOptions.recordInputs - : telemetryExplicitlyEnabled === true - ? true // When telemetry is explicitly enabled, default to recording inputs - : defaultInputsEnabled; - - const recordOutputs = - integrationRecordingOptions?.recordOutputs !== undefined - ? integrationRecordingOptions.recordOutputs - : methodTelemetryOptions.recordOutputs !== undefined - ? methodTelemetryOptions.recordOutputs - : telemetryExplicitlyEnabled === true - ? true // When telemetry is explicitly enabled, default to recording outputs - : defaultOutputsEnabled; - - return { recordInputs, recordOutputs }; -} - -/** - * This detects is added by the Sentry Vercel AI Integration to detect if the integration should - * be enabled. - * - * It also patches the `ai` module to enable Vercel AI telemetry automatically for all methods. - */ -export class SentryVercelAiInstrumentation extends InstrumentationBase { - private _isPatched = false; - private _callbacks: (() => void)[] = []; - - public constructor(config: InstrumentationConfig = {}) { - super('@sentry/instrumentation-vercel-ai', SDK_VERSION, config); - } - - /** - * Initializes the instrumentation by defining the modules to be patched. - */ - public init(): InstrumentationModuleDefinition { - const module = new InstrumentationNodeModuleDefinition('ai', SUPPORTED_VERSIONS, this._patch.bind(this)); - return module; - } - - /** - * Call the provided callback when the module is patched. - * If it has already been patched, the callback will be called immediately. - */ - public callWhenPatched(callback: () => void): void { - if (this._isPatched) { - callback(); - } else { - this._callbacks.push(callback); - } - } - - /** - * Patches module exports to enable Vercel AI telemetry. - */ - private _patch(moduleExports: PatchedModuleExports): unknown { - this._isPatched = true; - - this._callbacks.forEach(callback => callback()); - this._callbacks = []; - - const generatePatch = unknown>(originalMethod: T): T => { - return new Proxy(originalMethod, { - apply: (target, thisArg, args: MethodArgs) => { - const existingExperimentalTelemetry = args[0].experimental_telemetry || {}; - const isEnabled = existingExperimentalTelemetry.isEnabled; - - const client = getClient(); - const integration = client?.getIntegrationByName(INTEGRATION_NAME); - const integrationOptions = integration?.options; - const genAI = integration ? client?.getDataCollectionOptions().genAI : undefined; - - const { recordInputs, recordOutputs } = determineRecordingSettings( - integrationOptions, - existingExperimentalTelemetry, - isEnabled, - Boolean(genAI?.inputs), - Boolean(genAI?.outputs), - ); - - args[0].experimental_telemetry = { - ...existingExperimentalTelemetry, - isEnabled: isEnabled !== undefined ? isEnabled : true, - recordInputs, - recordOutputs, - }; - - return handleCallbackErrors( - () => Reflect.apply(target, thisArg, args), - error => { - // This error bubbles up to unhandledrejection handler (if not handled before), - // where we do not know the active span anymore - // So to circumvent this, we set the active span on the error object - // which is picked up by the unhandledrejection handler - if (error && typeof error === 'object') { - addNonEnumerableProperty(error, '_sentry_active_span', getActiveSpan()); - } - }, - () => {}, - result => { - processToolCallResults(result); - }, - ); - }, - }); - }; - - // Is this an ESM module? - // https://tc39.es/ecma262/#sec-module-namespace-objects - if (Object.prototype.toString.call(moduleExports) === '[object Module]') { - // In ESM we take the usual route and just replace the exports we want to instrument - for (const method of INSTRUMENTED_METHODS) { - // Skip methods that don't exist in this version of the AI SDK (e.g., rerank was added in v6) - if (moduleExports[method] != null) { - moduleExports[method] = generatePatch(moduleExports[method]); - } - } - - return moduleExports; - } else { - // In CJS we can't replace the exports in the original module because they - // don't have setters, so we create a new object with the same properties - const patchedModuleExports = INSTRUMENTED_METHODS.reduce((acc, curr) => { - // Skip methods that don't exist in this version of the AI SDK (e.g., rerank was added in v6) - if (moduleExports[curr] != null) { - acc[curr] = generatePatch(moduleExports[curr]); - } - return acc; - }, {} as PatchedModuleExports); - - return { ...moduleExports, ...patchedModuleExports }; - } - } -} diff --git a/packages/node/src/integrations/tracing/vercelai/types.ts b/packages/node/src/integrations/tracing/vercelai/types.ts deleted file mode 100644 index cdf8a515867a..000000000000 --- a/packages/node/src/integrations/tracing/vercelai/types.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { Integration } from '@sentry/core'; -import type { VercelAiOptions as VercelAiBaseOptions } from '@sentry/server-utils'; - -/** - * Telemetry configuration. - */ -export type TelemetrySettings = { - /** - * Enable or disable telemetry. Disabled by default while experimental. - */ - isEnabled?: boolean; - /** - * Enable or disable input recording. Enabled by default. - * - * You might want to disable input recording to avoid recording sensitive - * information, to reduce data transfers, or to increase performance. - */ - recordInputs?: boolean; - /** - * Enable or disable output recording. Enabled by default. - * - * You might want to disable output recording to avoid recording sensitive - * information, to reduce data transfers, or to increase performance. - */ - recordOutputs?: boolean; - /** - * Identifier for this function. Used to group telemetry data by function. - */ - functionId?: string; - /** - * Additional information to include in the telemetry data. - */ - metadata?: Record; -}; - -/** - * Attribute values may be any non-nullish primitive value except an object. - * - * null or undefined attribute values are invalid and will result in undefined behavior. - */ -export declare type AttributeValue = - | string - | number - | boolean - | Array - | Array - | Array; - -export interface VercelAiOptions extends VercelAiBaseOptions { - /** - * By default, the instrumentation will register span processors only when the ai package is used. - * If you want to register the span processors even when the ai package usage cannot be detected, you can set `force` to `true`. - */ - force?: boolean; -} - -export interface VercelAiIntegration extends Integration { - options: VercelAiOptions; -} diff --git a/packages/node/src/sdk/esmLoader.ts b/packages/node/src/sdk/esmLoader.ts deleted file mode 100644 index e0bc0a73964a..000000000000 --- a/packages/node/src/sdk/esmLoader.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { debug, GLOBAL_OBJ } from '@sentry/core'; -import { createAddHookMessageChannel } from 'import-in-the-middle'; -import * as moduleModule from 'module'; -import { supportsEsmLoaderHooks } from '../utils/detection'; - -/** - * Initialize the ESM loader - This method is private and not part of the public - * API. - * - * @ignore - */ -export function initializeEsmLoader(): void { - if (!supportsEsmLoaderHooks()) { - return; - } - - if (!GLOBAL_OBJ._sentryEsmLoaderHookRegistered) { - GLOBAL_OBJ._sentryEsmLoaderHookRegistered = true; - - try { - const { addHookMessagePort } = createAddHookMessageChannel(); - moduleModule.register('import-in-the-middle/hook.mjs', import.meta.url, { - data: { addHookMessagePort, include: [] }, - transferList: [addHookMessagePort], - }); - } catch (error) { - debug.warn("Failed to register 'import-in-the-middle' hook", error); - } - } -} diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index bdcdd4c8702f..4c39ede26ac3 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -44,7 +44,6 @@ import { getEntryPointType } from '../utils/entry-point'; import { getSpotlightConfig } from '../utils/spotlight'; import { defaultStackParser, getSentryRelease } from './api'; import { NodeClient } from './client'; -import { initializeEsmLoader } from './esmLoader'; import { initOpenTelemetry } from './initOtel'; /** @@ -176,10 +175,6 @@ function _init( const clientOptions = getClientOptions({ ...options, defaultIntegrations }, getDefaultIntegrationsImpl); - if (clientOptions.registerEsmLoaderHooks !== false) { - initializeEsmLoader(); - } - setOpenTelemetryContextAsyncContextStrategy(clientOptions); const scope = getCurrentScope(); diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index 42c3e6fb0c40..ae4ade78350e 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -4,7 +4,6 @@ import { debug as coreDebug, hasSpanStreamingEnabled } from '@sentry/core'; import { SentryContextManager } from '../otel/contextManager'; import { setupOpenTelemetryLogger } from '../otel/logger'; import type { NodeClient } from './client'; -import { initializeEsmLoader } from './esmLoader'; import { applyOtelSpanData, type AsyncLocalStorageLookup, @@ -101,8 +100,6 @@ export function preloadOpenTelemetry(options: NodePreloadOptions = {}): void { coreDebug.enable(); } - initializeEsmLoader(); - // These are all integrations that we need to pre-load to ensure they are set up before any other code runs getPreloadMethods(options.integrations).forEach(fn => { fn(); diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index d1fddafdfc15..839db1b7547a 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -75,16 +75,6 @@ export interface BaseNodeOptions extends OpenTelemetryServerRuntimeOptions { * Requires the `LocalVariables` integration. */ includeLocalVariables?: boolean; - - /** - * Whether to register ESM loader hooks to automatically instrument libraries. - * This is necessary to auto instrument libraries that are loaded via ESM imports, but it can cause issues - * with certain libraries. If you run into problems running your app with this enabled, - * please raise an issue in https://github.com/getsentry/sentry-javascript. - * - * Defaults to `true`. - */ - registerEsmLoaderHooks?: boolean; } /** diff --git a/packages/node/src/utils/detection.ts b/packages/node/src/utils/detection.ts deleted file mode 100644 index 98fcf3975609..000000000000 --- a/packages/node/src/utils/detection.ts +++ /dev/null @@ -1,16 +0,0 @@ -function isCjs(): boolean { - /*! rollup-include-cjs-only */ - return true; - /*! rollup-include-cjs-only-end */ - - /*! rollup-include-esm-only */ - return false; - /*! rollup-include-esm-only-end */ -} - -/** - * Check if the current Node.js version supports module.register - */ -export function supportsEsmLoaderHooks(): boolean { - return !isCjs(); -} diff --git a/packages/node/test/integrations/tracing/firebase.test.ts b/packages/node/test/integrations/tracing/firebase.test.ts deleted file mode 100644 index 0fe0309f4449..000000000000 --- a/packages/node/test/integrations/tracing/firebase.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { getPortAndAddress } from '../../../src/integrations/tracing/firebase/otel/patches/firestore'; - -describe('setPortAndAddress', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('IPv6 addresses', () => { - it('should correctly parse IPv6 address without port', () => { - const { address, port } = getPortAndAddress({ host: '[2001:db8::1]' }); - - expect(address).toBe('2001:db8::1'); - expect(port).toBeUndefined(); - }); - - it('should correctly parse IPv6 address with port', () => { - const { address, port } = getPortAndAddress({ host: '[2001:db8::1]:8080' }); - expect(address).toBe('2001:db8::1'); - expect(port).toBe(8080); - }); - - it('should handle IPv6 localhost without port', () => { - const { address, port } = getPortAndAddress({ host: '[::1]' }); - - expect(address).toBe('::1'); - expect(port).toBeUndefined(); - }); - - it('should handle IPv6 localhost with port', () => { - const { address, port } = getPortAndAddress({ host: '[::1]:3000' }); - - expect(address).toBe('::1'); - expect(port).toBe(3000); - }); - }); - - describe('IPv4 and hostname addresses', () => { - it('should correctly parse IPv4 address with port', () => { - const { address, port } = getPortAndAddress({ host: '192.168.1.1:8080' }); - - expect(address).toBe('192.168.1.1'); - expect(port).toBe(8080); - }); - - it('should correctly parse hostname with port', () => { - const { address, port } = getPortAndAddress({ host: 'localhost:3000' }); - - expect(address).toBe('localhost'); - expect(port).toBe(3000); - }); - - it('should correctly parse hostname without port', () => { - const { address, port } = getPortAndAddress({ host: 'example.com' }); - - expect(address).toBe('example.com'); - expect(port).toBeUndefined(); - }); - - it('should correctly parse hostname with port', () => { - const { address, port } = getPortAndAddress({ host: 'example.com:4000' }); - - expect(address).toBe('example.com'); - expect(port).toBe(4000); - }); - - it('should handle empty string', () => { - const { address, port } = getPortAndAddress({ host: '' }); - - expect(address).toBe(''); - expect(port).toBeUndefined(); - }); - }); -}); diff --git a/packages/node/test/integrations/tracing/graphql/addSpanSource.test.ts b/packages/node/test/integrations/tracing/graphql/addSpanSource.test.ts deleted file mode 100644 index 7df7df97ead1..000000000000 --- a/packages/node/test/integrations/tracing/graphql/addSpanSource.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import * as core from '@sentry/core'; -import { addSpanSource } from '../../../../src/integrations/tracing/graphql/vendored/utils'; -import type { Location, Token } from '@sentry/server-utils/orchestrion'; - -vi.spyOn(core, 'getClient'); - -function mockClient(graphQLDocument: boolean): void { - vi.mocked(core.getClient).mockReturnValue({ - getDataCollectionOptions: () => ({ graphQL: { document: graphQLDocument, variables: true } }), - } as any); -} - -function makeLocation(): Location { - const token: Token = { - kind: 'Name', - start: 0, - end: 5, - line: 1, - column: 1, - value: 'hello', - prev: null, - next: null, - }; - return { start: 0, end: 5, startToken: token }; -} - -describe('addSpanSource dataCollection gate', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('sets graphql.source attribute when graphQL.document is true', () => { - mockClient(true); - const span = { setAttribute: vi.fn() } as any; - addSpanSource(span, makeLocation()); - expect(span.setAttribute).toHaveBeenCalledWith('graphql.source', expect.any(String)); - }); - - it('does not set attribute when graphQL.document is false', () => { - mockClient(false); - const span = { setAttribute: vi.fn() } as any; - addSpanSource(span, makeLocation()); - expect(span.setAttribute).not.toHaveBeenCalled(); - }); - - it('does not set attribute when getClient() is undefined', () => { - vi.mocked(core.getClient).mockReturnValue(undefined); - const span = { setAttribute: vi.fn() } as any; - addSpanSource(span, makeLocation()); - expect(span.setAttribute).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/node/test/integrations/tracing/hapi.test.ts b/packages/node/test/integrations/tracing/hapi.test.ts deleted file mode 100644 index a11d93099d62..000000000000 --- a/packages/node/test/integrations/tracing/hapi.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - getExtMetadata, - getPluginName, - getRouteMetadata, - isDirectExtInput, - isLifecycleExtType, - isPatchableExtMethod, -} from '../../../src/integrations/tracing/hapi/vendored/utils'; - -describe('getRouteMetadata', () => { - const route = { path: '/users/{id}', method: 'get' } as any; - - it('describes a directly-registered route as a router layer', () => { - expect(getRouteMetadata(route)).toEqual({ - name: 'route - /users/{id}', - attributes: { - 'http.route': '/users/{id}', - 'http.method': 'get', - 'hapi.type': 'router', - }, - }); - }); - - it('describes a plugin-registered route as a plugin layer', () => { - expect(getRouteMetadata(route, 'my-plugin')).toEqual({ - name: 'my-plugin: route - /users/{id}', - attributes: { - 'http.route': '/users/{id}', - 'http.method': 'get', - 'hapi.type': 'plugin', - 'hapi.plugin.name': 'my-plugin', - }, - }); - }); -}); - -describe('getExtMetadata', () => { - it('names an extension by its point', () => { - expect(getExtMetadata('onPreHandler')).toEqual({ - name: 'ext - onPreHandler', - attributes: { 'server.ext.type': 'onPreHandler', 'hapi.type': 'server.ext' }, - }); - }); - - it('includes the method name when it is not the default `method`', () => { - expect(getExtMetadata('onPreHandler', undefined, 'myHandler').name).toBe('ext - onPreHandler - myHandler'); - expect(getExtMetadata('onPreHandler', undefined, 'method').name).toBe('ext - onPreHandler'); - }); - - it('includes the plugin name and prefixes the span name', () => { - expect(getExtMetadata('onPreHandler', 'my-plugin')).toEqual({ - name: 'my-plugin: ext - onPreHandler', - attributes: { - 'server.ext.type': 'onPreHandler', - 'hapi.type': 'server.ext', - 'hapi.plugin.name': 'my-plugin', - }, - }); - }); -}); - -describe('getPluginName', () => { - it('reads the name property when present', () => { - expect(getPluginName({ name: 'direct-name' } as any)).toBe('direct-name'); - }); - - it('falls back to the package name', () => { - expect(getPluginName({ pkg: { name: 'pkg-name' } } as any)).toBe('pkg-name'); - }); -}); - -describe('ext type guards', () => { - it('isLifecycleExtType recognizes lifecycle points', () => { - expect(isLifecycleExtType('onPreHandler')).toBe(true); - expect(isLifecycleExtType('onPreStart')).toBe(false); - expect(isLifecycleExtType(undefined)).toBe(false); - }); - - it('isDirectExtInput recognizes the [type, method] tuple form', () => { - expect(isDirectExtInput(['onPreHandler', () => {}])).toBe(true); - expect(isDirectExtInput(['onPreHandler', 'not-a-fn'])).toBe(false); - expect(isDirectExtInput({ type: 'onPreHandler' })).toBe(false); - }); - - it('isPatchableExtMethod is false for arrays', () => { - expect(isPatchableExtMethod((() => {}) as any)).toBe(true); - expect(isPatchableExtMethod([() => {}] as any)).toBe(false); - }); -}); diff --git a/packages/node/test/integrations/tracing/instrumentationNodeModuleFile.test.ts b/packages/node/test/integrations/tracing/instrumentationNodeModuleFile.test.ts deleted file mode 100644 index 00c014835b62..000000000000 --- a/packages/node/test/integrations/tracing/instrumentationNodeModuleFile.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { describe, expect, it } from 'vitest'; - -const TRACING_DIR = path.resolve(__dirname, '../../../src/integrations/tracing'); - -/** - * Importing InstrumentationNodeModuleFile from @opentelemetry/instrumentation causes - * Bun's --bytecode compiler to inline a re-export chain that loses scope bindings. - * All instrumentations must use the local vendored copy instead. - */ -describe('InstrumentationNodeModuleFile import guard', () => { - it('no file should import InstrumentationNodeModuleFile from @opentelemetry/instrumentation', () => { - const offendingFiles: string[] = []; - - function walkDir(dir: string): void { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - walkDir(fullPath); - } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')) { - const content = fs.readFileSync(fullPath, 'utf-8'); - // Match multi-line imports: look for an import block from @opentelemetry/instrumentation - // that includes InstrumentationNodeModuleFile as a named import - const importBlockRegex = - /import\s*\{[^}]*InstrumentationNodeModuleFile[^}]*\}\s*from\s*['"]@opentelemetry\/instrumentation['"]/s; - if (importBlockRegex.test(content)) { - offendingFiles.push(path.relative(TRACING_DIR, fullPath)); - } - } - } - } - - walkDir(TRACING_DIR); - - expect(offendingFiles).toEqual([]); - }); -}); diff --git a/packages/node/test/integrations/tracing/koa.test.ts b/packages/node/test/integrations/tracing/koa.test.ts deleted file mode 100644 index 067aa9322383..000000000000 --- a/packages/node/test/integrations/tracing/koa.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest'; -import { KoaInstrumentation } from '../../../src/integrations/tracing/koa/vendored/instrumentation'; -import { INSTRUMENTED } from '../../../src/otel/instrument'; -import { koaIntegration } from '@sentry/server-utils/orchestrion'; -import { instrumentKoa } from '../../../src/integrations/tracing/koa'; -import { isLayerIgnored } from '../../../src/integrations/tracing/koa/vendored/utils'; -import { KoaLayerType, type KoaInstrumentationConfig } from '../../../src/integrations/tracing/koa/vendored/types'; - -vi.mock('../../../src/integrations/tracing/koa/vendored/instrumentation'); - -describe('Koa', () => { - beforeEach(() => { - vi.clearAllMocks(); - delete INSTRUMENTED.Koa; - - (KoaInstrumentation as unknown as MockInstance).mockImplementation(() => { - return { - setTracerProvider: () => undefined, - setMeterProvider: () => undefined, - getConfig: () => ({}), - setConfig: () => ({}), - enable: () => undefined, - }; - }); - }); - - it('defaults are correct for instrumentKoa', () => { - instrumentKoa({}); - - expect(KoaInstrumentation).toHaveBeenCalledTimes(1); - expect(KoaInstrumentation).toHaveBeenCalledWith({ - ignoreLayersType: undefined, - }); - }); - - it('passes ignoreLayersType option to instrumentation', () => { - instrumentKoa({ ignoreLayersType: ['middleware'] }); - - expect(KoaInstrumentation).toHaveBeenCalledTimes(1); - expect(KoaInstrumentation).toHaveBeenCalledWith({ - ignoreLayersType: ['middleware'], - }); - }); - - it('passes multiple ignoreLayersType values to instrumentation', () => { - instrumentKoa({ ignoreLayersType: ['middleware', 'router'] }); - - expect(KoaInstrumentation).toHaveBeenCalledTimes(1); - expect(KoaInstrumentation).toHaveBeenCalledWith({ - ignoreLayersType: ['middleware', 'router'], - }); - }); - - // `koaIntegration()` is now the channel-based (orchestrion) integration by default; it no longer - // sets up the vendored OTel `KoaInstrumentation`. The channel subscriber's span behavior is covered - // in `@sentry/server-utils` and the node-integration koa suite. Here we only assert the public - // factory keeps the `Koa` name so the default-integration set and user overrides stay aligned. - it('koaIntegration is the channel integration with the Koa name', () => { - expect(koaIntegration().name).toBe('Koa'); - expect(koaIntegration({ ignoreLayersType: ['middleware'] }).name).toBe('Koa'); - }); -}); - -describe('isLayerIgnored', () => { - it('does not fail with invalid config', () => { - expect(isLayerIgnored(KoaLayerType.MIDDLEWARE)).toBe(false); - expect(isLayerIgnored(KoaLayerType.MIDDLEWARE, {} as KoaInstrumentationConfig)).toBe(false); - expect(isLayerIgnored(KoaLayerType.MIDDLEWARE, { ignoreLayersType: {} } as KoaInstrumentationConfig)).toBe(false); - expect(isLayerIgnored(KoaLayerType.ROUTER, { ignoreLayersType: {} } as KoaInstrumentationConfig)).toBe(false); - }); - - it('ignores based on type', () => { - expect(isLayerIgnored(KoaLayerType.MIDDLEWARE, { ignoreLayersType: [KoaLayerType.MIDDLEWARE] })).toBe(true); - expect(isLayerIgnored(KoaLayerType.ROUTER, { ignoreLayersType: [KoaLayerType.MIDDLEWARE] })).toBe(false); - }); -}); diff --git a/packages/node/test/integrations/tracing/mysql2.test.ts b/packages/node/test/integrations/tracing/mysql2.test.ts deleted file mode 100644 index 5d3d2bc42f58..000000000000 --- a/packages/node/test/integrations/tracing/mysql2.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { getConnectionPrototypeToInstrument } from '../../../src/integrations/tracing/mysql2/vendored/utils'; - -// The instrumentation patches `query`/`execute` on whichever prototype actually owns them. -// mysql2's layout differs across major versions: older versions define them directly on -// `Connection.prototype`, newer versions inherit them from a base class. This is the only -// version-sensitive logic in the instrumentation, so it's covered here as a pure unit. -// The end-to-end span behavior is covered by the real-package integration suite. -describe('getConnectionPrototypeToInstrument', () => { - it('returns the connection prototype when query/execute live on it directly', () => { - class Connection {} - (Connection.prototype as any).query = (): void => {}; - (Connection.prototype as any).execute = (): void => {}; - - expect(getConnectionPrototypeToInstrument(Connection)).toBe(Connection.prototype); - }); - - it('returns the base prototype when query/execute are inherited from a base class', () => { - class Base {} - (Base.prototype as any).query = (): void => {}; - (Base.prototype as any).execute = (): void => {}; - class Connection extends Base {} - - expect(getConnectionPrototypeToInstrument(Connection)).toBe(Base.prototype); - }); - - it('falls back to the connection prototype when the base lacks query/execute', () => { - class Base {} - (Base.prototype as any).query = (): void => {}; - // base only has `query`, not `execute` -> not a valid instrumentation target - class Connection extends Base {} - (Connection.prototype as any).query = (): void => {}; - (Connection.prototype as any).execute = (): void => {}; - - expect(getConnectionPrototypeToInstrument(Connection)).toBe(Connection.prototype); - }); -}); diff --git a/packages/node/test/integrations/tracing/postgresjs.test.ts b/packages/node/test/integrations/tracing/postgresjs.test.ts deleted file mode 100644 index a20b1941bb28..000000000000 --- a/packages/node/test/integrations/tracing/postgresjs.test.ts +++ /dev/null @@ -1,411 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { PostgresJsInstrumentation } from '../../../src/integrations/tracing/postgresjs'; - -describe('PostgresJs', () => { - const instrumentation = new PostgresJsInstrumentation({ requireParentSpan: true }); - - describe('_reconstructQuery', () => { - const reconstruct = (strings: string[] | undefined) => - ( - instrumentation as unknown as { _reconstructQuery: (s: string[] | undefined) => string | undefined } - )._reconstructQuery(strings); - - describe('empty input handling', () => { - it.each([ - [undefined, undefined], - [null as unknown as undefined, undefined], - [[], undefined], - [[''], undefined], - ])('returns undefined for %p', (input, expected) => { - expect(reconstruct(input)).toBe(expected); - }); - - it('returns whitespace-only string as-is', () => { - expect(reconstruct([' '])).toBe(' '); - }); - }); - - describe('single-element array (non-parameterized)', () => { - it.each([ - ['SELECT * FROM users', 'SELECT * FROM users'], - ['SELECT * FROM users WHERE id = $1', 'SELECT * FROM users WHERE id = $1'], - ['INSERT INTO users (email, name) VALUES ($1, $2)', 'INSERT INTO users (email, name) VALUES ($1, $2)'], - ])('returns %p as-is', (input, expected) => { - expect(reconstruct([input])).toBe(expected); - }); - }); - - describe('multi-element array (parameterized)', () => { - it.each([ - [['SELECT * FROM users WHERE id = ', ''], 'SELECT * FROM users WHERE id = $1'], - [['SELECT * FROM users WHERE id = ', ' AND name = ', ''], 'SELECT * FROM users WHERE id = $1 AND name = $2'], - [['INSERT INTO t VALUES (', ', ', ', ', ')'], 'INSERT INTO t VALUES ($1, $2, $3)'], - [['', ' WHERE id = ', ''], '$1 WHERE id = $2'], - [ - ['SELECT * FROM ', ' WHERE id = ', ' AND status IN (', ', ', ') ORDER BY ', ''], - 'SELECT * FROM $1 WHERE id = $2 AND status IN ($3, $4) ORDER BY $5', - ], - ])('reconstructs %p to %p', (input, expected) => { - expect(reconstruct(input)).toBe(expected); - }); - }); - - describe('edge cases', () => { - it('handles 10+ parameters', () => { - const strings = ['INSERT INTO t VALUES (', ', ', ', ', ', ', ', ', ', ', ', ', ', ', ', ', ', ', ')']; - expect(reconstruct(strings)).toBe('INSERT INTO t VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)'); - }); - - it.each([ - [['SELECT * FROM users WHERE id = ', ' ', ''], 'SELECT * FROM users WHERE id = $1 $2'], - [['SELECT * FROM users WHERE id = ', ' LIMIT 10'], 'SELECT * FROM users WHERE id = $1 LIMIT 10'], - [['SELECT *\nFROM users\nWHERE id = ', ''], 'SELECT *\nFROM users\nWHERE id = $1'], - [['SELECT * FROM "User" WHERE "email" = ', ''], 'SELECT * FROM "User" WHERE "email" = $1'], - [['SELECT ', '', '', ''], 'SELECT $1$2$3'], - [['', ''], '$1'], - ])('handles edge case %p', (input, expected) => { - expect(reconstruct(input)).toBe(expected); - }); - }); - - describe('integration with _sanitizeSqlQuery', () => { - const sanitize = (query: string | undefined) => - (instrumentation as unknown as { _sanitizeSqlQuery: (q: string | undefined) => string })._sanitizeSqlQuery( - query, - ); - - it('preserves $n placeholders per OTEL spec', () => { - const strings = ['SELECT * FROM users WHERE id = ', ' AND name = ', '']; - expect(sanitize(reconstruct(strings))).toBe('SELECT * FROM users WHERE id = $1 AND name = $2'); - }); - - it('collapses IN clause with $n to IN ($?)', () => { - const strings = ['SELECT * FROM users WHERE id = ', ' AND status IN (', ', ', ', ', ')']; - expect(sanitize(reconstruct(strings))).toBe('SELECT * FROM users WHERE id = $1 AND status IN ($?)'); - }); - - it('returns Unknown SQL Query for undefined input', () => { - expect(sanitize(reconstruct(undefined))).toBe('Unknown SQL Query'); - }); - - it('normalizes whitespace and removes trailing semicolon', () => { - const strings = ['SELECT *\n FROM users\n WHERE id = ', ';']; - expect(sanitize(reconstruct(strings))).toBe('SELECT * FROM users WHERE id = $1'); - }); - }); - }); - - describe('_sanitizeSqlQuery', () => { - const sanitize = (query: string | undefined) => - (instrumentation as unknown as { _sanitizeSqlQuery: (q: string | undefined) => string })._sanitizeSqlQuery(query); - - describe('passthrough (no literals)', () => { - it.each([ - ['SELECT * FROM users', 'SELECT * FROM users'], - ['INSERT INTO users (a, b) SELECT a, b FROM other', 'INSERT INTO users (a, b) SELECT a, b FROM other'], - [ - 'SELECT col1, col2 FROM table1 JOIN table2 ON table1.id = table2.id', - 'SELECT col1, col2 FROM table1 JOIN table2 ON table1.id = table2.id', - ], - ])('passes through %p unchanged', (input, expected) => { - expect(sanitize(input)).toBe(expected); - }); - }); - - describe('comment removal', () => { - it.each([ - ['SELECT * FROM users -- comment', 'SELECT * FROM users'], - ['SELECT * -- comment\nFROM users', 'SELECT * FROM users'], - ['SELECT /* comment */ * FROM users', 'SELECT * FROM users'], - ['SELECT /* multi\nline */ * FROM users', 'SELECT * FROM users'], - ['SELECT /* c1 */ * FROM /* c2 */ users -- c3', 'SELECT * FROM users'], - ])('removes comments: %p', (input, expected) => { - expect(sanitize(input)).toBe(expected); - }); - }); - - describe('whitespace normalization', () => { - it.each([ - ['SELECT * FROM users', 'SELECT * FROM users'], - ['SELECT *\n\tFROM\n\tusers', 'SELECT * FROM users'], - [' SELECT * FROM users ', 'SELECT * FROM users'], - [' SELECT \n\t * \r\n FROM \t\t users ', 'SELECT * FROM users'], - ])('normalizes %p', (input, expected) => { - expect(sanitize(input)).toBe(expected); - }); - }); - - describe('trailing semicolon removal', () => { - it.each([ - ['SELECT * FROM users;', 'SELECT * FROM users'], - ['SELECT * FROM users; ', 'SELECT * FROM users'], - ])('removes trailing semicolon: %p', (input, expected) => { - expect(sanitize(input)).toBe(expected); - }); - }); - - describe('$n placeholder preservation (OTEL compliance)', () => { - it.each([ - ['SELECT * FROM users WHERE id = $1', 'SELECT * FROM users WHERE id = $1'], - ['SELECT * FROM users WHERE id = $1 AND name = $2', 'SELECT * FROM users WHERE id = $1 AND name = $2'], - ['INSERT INTO t VALUES ($1, $10, $100)', 'INSERT INTO t VALUES ($1, $10, $100)'], - ['$1 UNION SELECT * FROM users', '$1 UNION SELECT * FROM users'], - ['SELECT * FROM users LIMIT $1', 'SELECT * FROM users LIMIT $1'], - ['SELECT $1$2$3', 'SELECT $1$2$3'], - ['SELECT generate_series($1, $2)', 'SELECT generate_series($1, $2)'], - ])('preserves $n: %p', (input, expected) => { - expect(sanitize(input)).toBe(expected); - }); - }); - - describe('string literal sanitization', () => { - it.each([ - ["SELECT * FROM users WHERE name = 'John'", 'SELECT * FROM users WHERE name = ?'], - ["SELECT * FROM users WHERE a = 'x' AND b = 'y'", 'SELECT * FROM users WHERE a = ? AND b = ?'], - ["SELECT * FROM users WHERE name = ''", 'SELECT * FROM users WHERE name = ?'], - ["SELECT * FROM users WHERE name = 'it''s'", 'SELECT * FROM users WHERE name = ?'], - ["SELECT * FROM users WHERE data = 'a''b''c'", 'SELECT * FROM users WHERE data = ?'], - ["SELECT * FROM t WHERE desc = 'Use $1 for param'", 'SELECT * FROM t WHERE desc = ?'], - ["SELECT * FROM users WHERE name = '日本語'", 'SELECT * FROM users WHERE name = ?'], - ])('sanitizes string: %p', (input, expected) => { - expect(sanitize(input)).toBe(expected); - }); - }); - - describe('numeric literal sanitization', () => { - it.each([ - ['SELECT * FROM users WHERE id = 123', 'SELECT * FROM users WHERE id = ?'], - ['SELECT * FROM users WHERE count = 0', 'SELECT * FROM users WHERE count = ?'], - ['SELECT * FROM products WHERE price = 19.99', 'SELECT * FROM products WHERE price = ?'], - ['SELECT * FROM products WHERE discount = .5', 'SELECT * FROM products WHERE discount = ?'], - ['SELECT * FROM accounts WHERE balance = -500', 'SELECT * FROM accounts WHERE balance = ?'], - ['SELECT * FROM accounts WHERE rate = -0.05', 'SELECT * FROM accounts WHERE rate = ?'], - ['SELECT * FROM data WHERE value = 1e10', 'SELECT * FROM data WHERE value = ?'], - ['SELECT * FROM data WHERE value = 1.5e-3', 'SELECT * FROM data WHERE value = ?'], - ['SELECT * FROM data WHERE value = 2.5E+10', 'SELECT * FROM data WHERE value = ?'], - ['SELECT * FROM data WHERE value = -1e10', 'SELECT * FROM data WHERE value = ?'], - ['SELECT * FROM users LIMIT 10 OFFSET 20', 'SELECT * FROM users LIMIT ? OFFSET ?'], - ])('sanitizes number: %p', (input, expected) => { - expect(sanitize(input)).toBe(expected); - }); - - it('preserves numbers in identifiers', () => { - expect(sanitize('SELECT * FROM users2 WHERE col1 = 5')).toBe('SELECT * FROM users2 WHERE col1 = ?'); - expect(sanitize('SELECT * FROM "table1" WHERE "col2" = 5')).toBe('SELECT * FROM "table1" WHERE "col2" = ?'); - }); - }); - - describe('hex and binary literal sanitization', () => { - it.each([ - ["SELECT * FROM t WHERE data = X'1A2B'", 'SELECT * FROM t WHERE data = ?'], - ["SELECT * FROM t WHERE data = x'ff'", 'SELECT * FROM t WHERE data = ?'], - ["SELECT * FROM t WHERE data = X''", 'SELECT * FROM t WHERE data = ?'], - ['SELECT * FROM t WHERE flags = 0x1A2B', 'SELECT * FROM t WHERE flags = ?'], - ['SELECT * FROM t WHERE flags = 0XFF', 'SELECT * FROM t WHERE flags = ?'], - ["SELECT * FROM t WHERE bits = B'1010'", 'SELECT * FROM t WHERE bits = ?'], - ["SELECT * FROM t WHERE bits = b'1111'", 'SELECT * FROM t WHERE bits = ?'], - ["SELECT * FROM t WHERE bits = B''", 'SELECT * FROM t WHERE bits = ?'], - ])('sanitizes hex/binary: %p', (input, expected) => { - expect(sanitize(input)).toBe(expected); - }); - }); - - describe('boolean literal sanitization', () => { - it.each([ - ['SELECT * FROM users WHERE active = TRUE', 'SELECT * FROM users WHERE active = ?'], - ['SELECT * FROM users WHERE active = FALSE', 'SELECT * FROM users WHERE active = ?'], - ['SELECT * FROM users WHERE a = true AND b = false', 'SELECT * FROM users WHERE a = ? AND b = ?'], - ['SELECT * FROM users WHERE a = True AND b = False', 'SELECT * FROM users WHERE a = ? AND b = ?'], - ])('sanitizes boolean: %p', (input, expected) => { - expect(sanitize(input)).toBe(expected); - }); - - it('does not affect identifiers containing TRUE/FALSE', () => { - expect(sanitize('SELECT TRUE_FLAG FROM users WHERE active = TRUE')).toBe( - 'SELECT TRUE_FLAG FROM users WHERE active = ?', - ); - }); - }); - - describe('IN clause collapsing', () => { - it.each([ - ['SELECT * FROM users WHERE id IN (?, ?, ?)', 'SELECT * FROM users WHERE id IN (?)'], - ['SELECT * FROM users WHERE id IN ($1, $2, $3)', 'SELECT * FROM users WHERE id IN ($?)'], - ['SELECT * FROM users WHERE id in ($1, $2)', 'SELECT * FROM users WHERE id IN ($?)'], - ['SELECT * FROM users WHERE id IN ( $1 , $2 , $3 )', 'SELECT * FROM users WHERE id IN ($?)'], - [ - 'SELECT * FROM users WHERE id IN ($1, $2) AND status IN ($3, $4)', - 'SELECT * FROM users WHERE id IN ($?) AND status IN ($?)', - ], - ['SELECT * FROM users WHERE id NOT IN ($1, $2)', 'SELECT * FROM users WHERE id NOT IN ($?)'], - ['SELECT * FROM users WHERE id NOT IN (?, ?)', 'SELECT * FROM users WHERE id NOT IN (?)'], - ['SELECT * FROM users WHERE id IN ($1)', 'SELECT * FROM users WHERE id IN ($?)'], - ['SELECT * FROM users WHERE id IN (1, 2, 3)', 'SELECT * FROM users WHERE id IN (?)'], - ])('collapses IN clause: %p', (input, expected) => { - expect(sanitize(input)).toBe(expected); - }); - }); - - describe('mixed scenarios (params + literals)', () => { - it.each([ - ["SELECT * FROM users WHERE id = $1 AND status = 'active'", 'SELECT * FROM users WHERE id = $1 AND status = ?'], - ['SELECT * FROM users WHERE id = $1 AND limit = 100', 'SELECT * FROM users WHERE id = $1 AND limit = ?'], - [ - "SELECT * FROM t WHERE a = $1 AND b = 'foo' AND c = 123 AND d = TRUE AND e IN ($2, $3)", - 'SELECT * FROM t WHERE a = $1 AND b = ? AND c = ? AND d = ? AND e IN ($?)', - ], - ])('handles mixed: %p', (input, expected) => { - expect(sanitize(input)).toBe(expected); - }); - }); - - describe('PostgreSQL-specific syntax', () => { - it.each([ - ['SELECT $1::integer', 'SELECT $1::integer'], - ['SELECT $1::text', 'SELECT $1::text'], - ['SELECT * FROM t WHERE tags = ARRAY[1, 2, 3]', 'SELECT * FROM t WHERE tags = ARRAY[?, ?, ?]'], - ['SELECT * FROM t WHERE tags = ARRAY[$1, $2]', 'SELECT * FROM t WHERE tags = ARRAY[$1, $2]'], - ["SELECT data->'key' FROM t WHERE id = $1", 'SELECT data->? FROM t WHERE id = $1'], - ["SELECT data->>'key' FROM t WHERE id = $1", 'SELECT data->>? FROM t WHERE id = $1'], - ["SELECT * FROM t WHERE data @> '{}'", 'SELECT * FROM t WHERE data @> ?'], - [ - "SELECT * FROM t WHERE created_at > NOW() - INTERVAL '7 days'", - 'SELECT * FROM t WHERE created_at > NOW() - INTERVAL ?', - ], - ['CREATE TABLE t (created_at TIMESTAMP(3))', 'CREATE TABLE t (created_at TIMESTAMP(?))'], - ['CREATE TABLE t (price NUMERIC(10, 2))', 'CREATE TABLE t (price NUMERIC(?, ?))'], - ])('handles PostgreSQL syntax: %p', (input, expected) => { - expect(sanitize(input)).toBe(expected); - }); - }); - - describe('empty/undefined input', () => { - it.each([ - [undefined, 'Unknown SQL Query'], - ['', 'Unknown SQL Query'], - [' ', ''], - [' \n\t ', ''], - ])('handles empty input %p', (input, expected) => { - expect(sanitize(input)).toBe(expected); - }); - }); - - describe('complex real-world queries', () => { - it('handles query with comments, whitespace, and IN clause', () => { - const input = ` - SELECT * FROM users -- fetch all users - WHERE id = $1 - AND status IN ($2, $3, $4); - `; - expect(sanitize(input)).toBe('SELECT * FROM users WHERE id = $1 AND status IN ($?)'); - }); - - it('handles Prisma-style query', () => { - const input = ` - SELECT "User"."id", "User"."email", "User"."name" - FROM "User" - WHERE "User"."email" = $1 - AND "User"."deleted_at" IS NULL - LIMIT $2; - `; - expect(sanitize(input)).toBe( - 'SELECT "User"."id", "User"."email", "User"."name" FROM "User" WHERE "User"."email" = $1 AND "User"."deleted_at" IS NULL LIMIT $2', - ); - }); - - it('handles CREATE TABLE with various types', () => { - const input = ` - CREATE TABLE "User" ( - "id" SERIAL NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "email" TEXT NOT NULL, - "balance" NUMERIC(10, 2) DEFAULT 0.00, - CONSTRAINT "User_pkey" PRIMARY KEY ("id") - ); - `; - expect(sanitize(input)).toBe( - 'CREATE TABLE "User" ( "id" SERIAL NOT NULL, "createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP, "email" TEXT NOT NULL, "balance" NUMERIC(?, ?) DEFAULT ?, CONSTRAINT "User_pkey" PRIMARY KEY ("id") )', - ); - }); - - it('handles INSERT/UPDATE with mixed literals and params', () => { - expect(sanitize("INSERT INTO users (name, age, active) VALUES ('John', 30, TRUE)")).toBe( - 'INSERT INTO users (name, age, active) VALUES (?, ?, ?)', - ); - expect(sanitize("UPDATE users SET name = $1, updated_at = '2024-01-01' WHERE id = 123")).toBe( - 'UPDATE users SET name = $1, updated_at = ? WHERE id = ?', - ); - }); - }); - - describe('edge cases', () => { - it.each([ - ['SELECT * FROM "my-table" WHERE "my-column" = $1', 'SELECT * FROM "my-table" WHERE "my-column" = $1'], - ['SELECT * FROM t WHERE big_id = 99999999999999999999', 'SELECT * FROM t WHERE big_id = ?'], - ['SELECT * FROM t WHERE val > -5', 'SELECT * FROM t WHERE val > ?'], - ['SELECT * FROM t WHERE id IN (1, -2, 3)', 'SELECT * FROM t WHERE id IN (?)'], - ['SELECT 1+2*3', 'SELECT ?+?*?'], - ["SELECT * FROM users WHERE name LIKE '%john%'", 'SELECT * FROM users WHERE name LIKE ?'], - ['SELECT * FROM t WHERE age BETWEEN 18 AND 65', 'SELECT * FROM t WHERE age BETWEEN ? AND ?'], - ['SELECT * FROM t WHERE age BETWEEN $1 AND $2', 'SELECT * FROM t WHERE age BETWEEN $1 AND $2'], - [ - "SELECT CASE WHEN status = 'active' THEN 1 ELSE 0 END FROM users", - 'SELECT CASE WHEN status = ? THEN ? ELSE ? END FROM users', - ], - [ - 'SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 100)', - 'SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > ?)', - ], - [ - "WITH cte AS (SELECT * FROM users WHERE status = 'active') SELECT * FROM cte WHERE id = $1", - 'WITH cte AS (SELECT * FROM users WHERE status = ?) SELECT * FROM cte WHERE id = $1', - ], - [ - 'SELECT COUNT(*), SUM(amount), AVG(price) FROM orders WHERE status = $1', - 'SELECT COUNT(*), SUM(amount), AVG(price) FROM orders WHERE status = $1', - ], - [ - 'SELECT status, COUNT(*) FROM orders GROUP BY status HAVING COUNT(*) > 10', - 'SELECT status, COUNT(*) FROM orders GROUP BY status HAVING COUNT(*) > ?', - ], - [ - 'SELECT ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) FROM orders', - 'SELECT ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) FROM orders', - ], - ])('handles edge case: %p', (input, expected) => { - expect(sanitize(input)).toBe(expected); - }); - }); - - describe('regression tests', () => { - it('does not replace $n with ? (OTEL compliance)', () => { - const result = sanitize('SELECT * FROM users WHERE id = $1'); - expect(result).not.toContain('?'); - expect(result).toBe('SELECT * FROM users WHERE id = $1'); - }); - - it('does not split decimal numbers into ?.?', () => { - const result = sanitize('SELECT * FROM t WHERE price = 19.99'); - expect(result).not.toBe('SELECT * FROM t WHERE price = ?.?'); - expect(result).toBe('SELECT * FROM t WHERE price = ?'); - }); - - it('does not leave minus sign when sanitizing negative numbers', () => { - const result = sanitize('SELECT * FROM t WHERE val = -500'); - expect(result).not.toBe('SELECT * FROM t WHERE val = -?'); - expect(result).toBe('SELECT * FROM t WHERE val = ?'); - }); - - it('handles exact queries from integration tests', () => { - expect( - sanitize( - 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', - ), - ).toBe( - 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', - ); - expect(sanitize('SELECT * from generate_series(1,1000) as x')).toBe('SELECT * from generate_series(?,?) as x'); - }); - }); - }); -}); diff --git a/packages/node/test/integrations/tracing/tedious.test.ts b/packages/node/test/integrations/tracing/tedious.test.ts deleted file mode 100644 index bcf2f9261fb6..000000000000 --- a/packages/node/test/integrations/tracing/tedious.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { getSpanName } from '../../../src/integrations/tracing/tedious/vendored/utils'; - -describe('getSpanName', () => { - it('names execBulkLoad with the table and database', () => { - expect(getSpanName('execBulkLoad', 'master', undefined, 'test_bulk')).toBe('execBulkLoad test_bulk master'); - }); - - it('names callProcedure with the procedure and database', () => { - expect(getSpanName('callProcedure', 'master', '[dbo].[test_proced]', undefined)).toBe( - 'callProcedure [dbo].[test_proced] master', - ); - }); - - it('names callProcedure with the procedure when no database', () => { - expect(getSpanName('callProcedure', undefined, '[dbo].[test_proced]', undefined)).toBe( - 'callProcedure [dbo].[test_proced]', - ); - }); - - it('names a general operation with the database, not the sql', () => { - expect(getSpanName('execSql', 'master', 'SELECT GETDATE()', undefined)).toBe('execSql master'); - }); - - it('names a general operation with just the operation when no database', () => { - expect(getSpanName('execSql', undefined, 'SELECT GETDATE()', undefined)).toBe('execSql'); - }); -}); diff --git a/packages/node/test/integrations/tracing/vercelai/instrumentation.test.ts b/packages/node/test/integrations/tracing/vercelai/instrumentation.test.ts deleted file mode 100644 index f27098cfe138..000000000000 --- a/packages/node/test/integrations/tracing/vercelai/instrumentation.test.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { _INTERNAL_getSpanContextForToolCallId, _INTERNAL_toolCallSpanContextMap } from '@sentry/core'; -import { beforeEach, describe, expect, test } from 'vitest'; -import { - cleanupToolCallSpanContexts, - determineRecordingSettings, -} from '../../../../src/integrations/tracing/vercelai/instrumentation'; - -describe('determineRecordingSettings', () => { - test('should use integration recording options when provided (recordInputs: true, recordOutputs: false)', () => { - const result = determineRecordingSettings( - { recordInputs: true, recordOutputs: false }, // integrationRecordingOptions - {}, // methodTelemetryOptions - undefined, // telemetryExplicitlyEnabled - false, // defaultInputsEnabled - false, // defaultOutputsEnabled - ); - - expect(result).toEqual({ - recordInputs: true, - recordOutputs: false, - }); - }); - - test('should use integration recording options when provided (recordInputs: false, recordOutputs: true)', () => { - const result = determineRecordingSettings( - { recordInputs: false, recordOutputs: true }, // integrationRecordingOptions - {}, // methodTelemetryOptions - true, // telemetryExplicitlyEnabled - true, // defaultInputsEnabled - true, // defaultOutputsEnabled - ); - - expect(result).toEqual({ - recordInputs: false, - recordOutputs: true, - }); - }); - - test('should fall back to method telemetry options when integration options not provided', () => { - const result = determineRecordingSettings( - {}, // integrationRecordingOptions - { recordInputs: true, recordOutputs: false }, // methodTelemetryOptions - undefined, // telemetryExplicitlyEnabled - false, // defaultInputsEnabled - false, // defaultOutputsEnabled - ); - - expect(result).toEqual({ - recordInputs: true, - recordOutputs: false, - }); - }); - - test('should prefer integration recording options over method telemetry options', () => { - const result = determineRecordingSettings( - { recordInputs: false, recordOutputs: false }, // integrationRecordingOptions - { recordInputs: true, recordOutputs: true }, // methodTelemetryOptions - undefined, // telemetryExplicitlyEnabled - true, // defaultInputsEnabled - true, // defaultOutputsEnabled - ); - - expect(result).toEqual({ - recordInputs: false, - recordOutputs: false, - }); - }); - - test('should default to recording when telemetry is explicitly enabled', () => { - const result = determineRecordingSettings( - {}, // integrationRecordingOptions - {}, // methodTelemetryOptions - true, // telemetryExplicitlyEnabled - false, // defaultInputsEnabled - false, // defaultOutputsEnabled - ); - - expect(result).toEqual({ - recordInputs: true, - recordOutputs: true, - }); - }); - - test('should use default recording setting when telemetry is explicitly disabled', () => { - const result = determineRecordingSettings( - {}, // integrationRecordingOptions - {}, // methodTelemetryOptions - false, // telemetryExplicitlyEnabled - true, // defaultInputsEnabled - true, // defaultOutputsEnabled - ); - - expect(result).toEqual({ - recordInputs: true, - recordOutputs: true, - }); - }); - - test('should use default recording setting when telemetry enablement is undefined', () => { - const result = determineRecordingSettings( - {}, // integrationRecordingOptions - {}, // methodTelemetryOptions - undefined, // telemetryExplicitlyEnabled - true, // defaultInputsEnabled - true, // defaultOutputsEnabled - ); - - expect(result).toEqual({ - recordInputs: true, - recordOutputs: true, - }); - }); - - test('should not record when default recording is disabled and no explicit configuration', () => { - const result = determineRecordingSettings( - {}, // integrationRecordingOptions - {}, // methodTelemetryOptions - undefined, // telemetryExplicitlyEnabled - false, // defaultInputsEnabled - false, // defaultOutputsEnabled - ); - - expect(result).toEqual({ - recordInputs: false, - recordOutputs: false, - }); - }); - - test('should handle partial integration recording options (only recordInputs)', () => { - const result = determineRecordingSettings( - { recordInputs: true }, // integrationRecordingOptions - {}, // methodTelemetryOptions - false, // telemetryExplicitlyEnabled - false, // defaultInputsEnabled - false, // defaultOutputsEnabled - ); - - expect(result).toEqual({ - recordInputs: true, - recordOutputs: false, // falls back to defaultOutputsEnabled - }); - }); - - test('should handle partial integration recording options (only recordOutputs)', () => { - const result = determineRecordingSettings( - { recordOutputs: true }, // integrationRecordingOptions - {}, // methodTelemetryOptions - false, // telemetryExplicitlyEnabled - false, // defaultInputsEnabled - false, // defaultOutputsEnabled - ); - - expect(result).toEqual({ - recordInputs: false, // falls back to defaultInputsEnabled - recordOutputs: true, - }); - }); - - test('should handle partial method telemetry options (only recordInputs)', () => { - const result = determineRecordingSettings( - {}, // integrationRecordingOptions - { recordInputs: true }, // methodTelemetryOptions - false, // telemetryExplicitlyEnabled - false, // defaultInputsEnabled - false, // defaultOutputsEnabled - ); - - expect(result).toEqual({ - recordInputs: true, - recordOutputs: false, // falls back to defaultOutputsEnabled - }); - }); - - test('should handle partial method telemetry options (only recordOutputs)', () => { - const result = determineRecordingSettings( - {}, // integrationRecordingOptions - { recordOutputs: true }, // methodTelemetryOptions - false, // telemetryExplicitlyEnabled - false, // defaultInputsEnabled - false, // defaultOutputsEnabled - ); - - expect(result).toEqual({ - recordInputs: false, // falls back to defaultInputsEnabled - recordOutputs: true, - }); - }); - - test('should prefer integration recording options over method telemetry for partial configs', () => { - const result = determineRecordingSettings( - { recordInputs: false }, // integrationRecordingOptions - { recordInputs: true, recordOutputs: true }, // methodTelemetryOptions - false, // telemetryExplicitlyEnabled - true, // defaultInputsEnabled - true, // defaultOutputsEnabled - ); - - expect(result).toEqual({ - recordInputs: false, // from integration recording options - recordOutputs: true, // from method telemetry options - }); - }); - - test('complex scenario: dataCollection.genAI enabled, telemetry enablement undefined, mixed options', () => { - const result = determineRecordingSettings( - { recordOutputs: false }, // integrationRecordingOptions - { recordInputs: false }, // methodTelemetryOptions - undefined, // telemetryExplicitlyEnabled - true, // defaultInputsEnabled (dataCollection.genAI.inputs: true) - true, // defaultOutputsEnabled (dataCollection.genAI.outputs: true) - ); - - expect(result).toEqual({ - recordInputs: false, // from method telemetry options - recordOutputs: false, // from integration recording options - }); - }); - - test('complex scenario: explicit telemetry enabled overrides dataCollection.genAI disabled', () => { - const result = determineRecordingSettings( - {}, // integrationRecordingOptions - {}, // methodTelemetryOptions - true, // telemetryExplicitlyEnabled - false, // defaultInputsEnabled (dataCollection.genAI.inputs: false) - false, // defaultOutputsEnabled (dataCollection.genAI.outputs: false) - ); - - expect(result).toEqual({ - recordInputs: true, - recordOutputs: true, - }); - }); - - test('supports asymmetric defaults: inputs enabled, outputs disabled', () => { - const result = determineRecordingSettings( - {}, // integrationRecordingOptions - {}, // methodTelemetryOptions - undefined, // telemetryExplicitlyEnabled - true, // defaultInputsEnabled (dataCollection.genAI.inputs: true) - false, // defaultOutputsEnabled (dataCollection.genAI.outputs: false) - ); - - expect(result).toEqual({ - recordInputs: true, - recordOutputs: false, - }); - }); -}); - -describe('cleanupToolCallSpanContexts', () => { - beforeEach(() => { - _INTERNAL_toolCallSpanContextMap.clear(); - }); - - test('cleans up span context for tool-result items', () => { - _INTERNAL_toolCallSpanContextMap.set('tool-1', { traceId: 't1', spanId: 's1' }); - _INTERNAL_toolCallSpanContextMap.set('tool-2', { traceId: 't2', spanId: 's2' }); - - cleanupToolCallSpanContexts([{ type: 'tool-result', toolCallId: 'tool-1', toolName: 'bash' }]); - - expect(_INTERNAL_getSpanContextForToolCallId('tool-1')).toBeUndefined(); - expect(_INTERNAL_getSpanContextForToolCallId('tool-2')).toEqual({ traceId: 't2', spanId: 's2' }); - }); - - test('cleans up span context for tool-error items', () => { - _INTERNAL_toolCallSpanContextMap.set('tool-1', { traceId: 't1', spanId: 's1' }); - - cleanupToolCallSpanContexts([ - { type: 'tool-error', toolCallId: 'tool-1', toolName: 'bash', error: new Error('fail') }, - ]); - - expect(_INTERNAL_getSpanContextForToolCallId('tool-1')).toBeUndefined(); - }); - - test('cleans up mixed tool-result and tool-error in same content array', () => { - _INTERNAL_toolCallSpanContextMap.set('tool-1', { traceId: 't1', spanId: 's1' }); - _INTERNAL_toolCallSpanContextMap.set('tool-2', { traceId: 't2', spanId: 's2' }); - - cleanupToolCallSpanContexts([ - { type: 'tool-result', toolCallId: 'tool-1', toolName: 'bash' }, - { type: 'tool-error', toolCallId: 'tool-2', toolName: 'bash', error: new Error('fail') }, - ]); - - expect(_INTERNAL_getSpanContextForToolCallId('tool-1')).toBeUndefined(); - expect(_INTERNAL_getSpanContextForToolCallId('tool-2')).toBeUndefined(); - }); - - test('ignores items without toolCallId', () => { - _INTERNAL_toolCallSpanContextMap.set('tool-1', { traceId: 't1', spanId: 's1' }); - - cleanupToolCallSpanContexts([{ type: 'text', text: 'hello' } as unknown as object]); - - expect(_INTERNAL_getSpanContextForToolCallId('tool-1')).toEqual({ traceId: 't1', spanId: 's1' }); - }); -});