From 5293e6286add8d72f7b2c0503721705d217b8fd5 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:05:46 +0200 Subject: [PATCH 1/8] fix(core): Apply `dataCollection.databaseQueryData` to inline SQL literals Co-Authored-By: Claude Opus 5 --- .../instrumentations/worker/instrumentD1.ts | 11 ++++-- .../worker/instrumentD1.test.ts | 1 + packages/core/src/server.ts | 1 + .../filterCollectedDbQueryText.ts | 35 +++++++++++++++++++ .../filterCollectedDbQueryText.test.ts | 29 +++++++++++++++ .../src/runtime/utils/instrumentDatabase.ts | 9 +++-- .../server-utils/src/integrations/knex.ts | 9 +++-- .../server-utils/src/integrations/mysql.ts | 11 ++++-- .../src/integrations/mysql2/index.ts | 11 ++++-- .../server-utils/src/integrations/postgres.ts | 11 ++++-- .../server-utils/src/integrations/tedious.ts | 8 +++-- 11 files changed, 118 insertions(+), 18 deletions(-) create mode 100644 packages/core/src/utils/data-collection/filterCollectedDbQueryText.ts create mode 100644 packages/core/test/lib/utils/data-collection/filterCollectedDbQueryText.test.ts diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts b/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts index 322ffa4c4e4b..efb5b373b705 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts @@ -11,7 +11,11 @@ import { SPAN_STATUS_ERROR, startSpan, } from '@sentry/core'; -import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery } from '@sentry/core/server'; +import { + _INTERNAL_getSqlQuerySummary, + _INTERNAL_sanitizeSqlQuery, + filterCollectedDbQueryText, +} from '@sentry/core/server'; import { ensureInstrumented } from '../../instrument'; // Patching is based on internal Cloudflare D1 API @@ -134,7 +138,8 @@ function createStartSpanOptions(query: string, type: D1QueryType): StartSpanOpti const querySummary = query ? _INTERNAL_getSqlQuerySummary(_INTERNAL_sanitizeSqlQuery(query)) : undefined; const client = getClient(); - const name = client && hasSpanStreamingEnabled(client) ? querySummary || 'cloudflare-d1' : query; + const queryText = filterCollectedDbQueryText(query, undefined, client); + const name = client && hasSpanStreamingEnabled(client) ? querySummary || 'cloudflare-d1' : queryText; return { name, @@ -142,7 +147,7 @@ function createStartSpanOptions(query: string, type: D1QueryType): StartSpanOpti [SENTRY_OP]: DB_QUERY, 'db.system.name': 'cloudflare-d1', 'db.operation.name': type, - 'db.query.text': query, + 'db.query.text': queryText, 'db.query.summary': querySummary, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.d1', }, diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts index b59f0fc5918a..7a42f748cabd 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts @@ -116,6 +116,7 @@ describe('instrumentD1', () => { beforeEach(() => { getClientSpy = vi.spyOn(SentryCore, 'getClient').mockReturnValue({ getOptions: () => ({ traceLifecycle: 'stream' }), + getDataCollectionOptions: () => ({ databaseQueryData: true }), } as unknown as ReturnType); }); diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 2d2d76127f6a..965f7404c98b 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -31,6 +31,7 @@ export { getSqlQuerySummary as _INTERNAL_getSqlQuerySummary, sanitizeSqlQuery as _INTERNAL_sanitizeSqlQuery, } from './utils/sql'; +export { filterCollectedDbQueryText } from './utils/data-collection/filterCollectedDbQueryText'; export type { SqlDialect } from './utils/sql'; export { patchHttpModuleClient } from './integrations/http/client-patch'; diff --git a/packages/core/src/utils/data-collection/filterCollectedDbQueryText.ts b/packages/core/src/utils/data-collection/filterCollectedDbQueryText.ts new file mode 100644 index 000000000000..6788add2b2e9 --- /dev/null +++ b/packages/core/src/utils/data-collection/filterCollectedDbQueryText.ts @@ -0,0 +1,35 @@ +import type { Client } from '../../client'; +import { getClient } from '../../currentScopes'; +import type { SqlDialect } from '../sql'; +import { sanitizeSqlQuery } from '../sql'; + +/** + * Applies `dataCollection.databaseQueryData` to a SQL statement the SDK collected itself, for use as + * `db.query.text`. + * + * A statement can carry inline literal values (`WHERE email = 'jane@example.com'`), which the spec + * counts as database query data. Sanitized statements are not gated, so with the option off the + * literals are replaced with `?` rather than the attribute being dropped — the shape of the query + * stays available for debugging. + * + * Pass the `client` the statement belongs to whenever one is at hand; falling back to `getClient()` + * resolves against the current scope, which is the wrong client in a multi-client setup. + */ +export function filterCollectedDbQueryText(query: string, dialect?: SqlDialect, client?: Client): string; +export function filterCollectedDbQueryText( + query: string | undefined, + dialect?: SqlDialect, + client?: Client, +): string | undefined; +export function filterCollectedDbQueryText( + query: string | undefined, + dialect?: SqlDialect, + client?: Client, +): string | undefined { + if (query === undefined) { + return undefined; + } + // Instrumentation can run before a client exists; collecting is the documented default. + const collect = (client ?? getClient())?.getDataCollectionOptions().databaseQueryData !== false; + return collect ? query : sanitizeSqlQuery(query, dialect); +} diff --git a/packages/core/test/lib/utils/data-collection/filterCollectedDbQueryText.test.ts b/packages/core/test/lib/utils/data-collection/filterCollectedDbQueryText.test.ts new file mode 100644 index 000000000000..13e42a36e92f --- /dev/null +++ b/packages/core/test/lib/utils/data-collection/filterCollectedDbQueryText.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; +import type { Client } from '../../../../src/client'; +import type { DataCollection } from '../../../../src/types/datacollection'; +import { filterCollectedDbQueryText } from '../../../../src/utils/data-collection/filterCollectedDbQueryText'; +import { resolveDataCollectionOptions } from '../../../../src/utils/data-collection/resolveDataCollectionOptions'; + +function mockClient(dataCollection?: DataCollection): Client { + return { getDataCollectionOptions: () => resolveDataCollectionOptions({ dataCollection }) } as unknown as Client; +} + +describe('filterCollectedDbQueryText', () => { + it('returns undefined for an absent statement', () => { + expect(filterCollectedDbQueryText(undefined, undefined, mockClient())).toBeUndefined(); + }); + + it('keeps inline literals by default', () => { + expect( + filterCollectedDbQueryText("SELECT * FROM users WHERE email = 'jane@example.com'", undefined, mockClient()), + ).toBe("SELECT * FROM users WHERE email = 'jane@example.com'"); + }); + + it('sanitizes inline literals when databaseQueryData is off', () => { + expect( + filterCollectedDbQueryText("SELECT * FROM users WHERE email = 'jane@example.com'", undefined, { + ...mockClient({ databaseQueryData: false }), + }), + ).toBe('SELECT * FROM users WHERE email = ?'); + }); +}); diff --git a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts index 9bb3d25e4fe1..6532ba9cbbad 100644 --- a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts +++ b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts @@ -13,7 +13,12 @@ import { startSpan, type StartSpanOptions, } from '@sentry/core'; -import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery, flushIfServerless } from '@sentry/core/server'; +import { + _INTERNAL_getSqlQuerySummary, + _INTERNAL_sanitizeSqlQuery, + filterCollectedDbQueryText, + flushIfServerless, +} from '@sentry/core/server'; import type { Database, PreparedStatement } from 'db0'; import { type DatabaseConnectionConfig, type DatabaseSpanData, getDatabaseSpanData } from './database-span-data'; import { DB_NAMESPACE, DB_QUERY_SUMMARY, DB_QUERY_TEXT, DB_SYSTEM_NAME } from '@sentry/conventions/attributes'; @@ -260,7 +265,7 @@ function createStartSpanOptions(query: string, data: DatabaseSpanData): StartSpa return { name, attributes: { - [DB_QUERY_TEXT]: query, + [DB_QUERY_TEXT]: filterCollectedDbQueryText(query), [DB_QUERY_SUMMARY]: querySummary, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SENTRY_ORIGIN, [SENTRY_OP]: DB_QUERY, diff --git a/packages/server-utils/src/integrations/knex.ts b/packages/server-utils/src/integrations/knex.ts index 74f925a8d4ca..88feb533e144 100644 --- a/packages/server-utils/src/integrations/knex.ts +++ b/packages/server-utils/src/integrations/knex.ts @@ -33,7 +33,11 @@ import { DB } from '@sentry/conventions/op'; import { DEBUG_BUILD } from '../debug-build'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; -import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery } from '@sentry/core/server'; +import { + _INTERNAL_getSqlQuerySummary, + _INTERNAL_sanitizeSqlQuery, + filterCollectedDbQueryText, +} from '@sentry/core/server'; // NOTE: this uses the same name as the OTel integration by design. `@sentry/node`'s `knexIntegration` // picks this subscriber over the vendored OTel path when orchestrion injection is active. @@ -174,8 +178,9 @@ function subscribeQuery(): void { connection?.filename || connection?.database || extractDatabaseFromConnectionString(connectionString); const dbSystem = mapSystem(client?.driverName); - const dbStatement = query?.sql != null ? truncate(query.sql, MAX_QUERY_LENGTH) : undefined; const dialect = client?.driverName === 'mysql' || client?.driverName === 'mysql2' ? 'mysql' : undefined; + const dbStatement = + query?.sql != null ? filterCollectedDbQueryText(truncate(query.sql, MAX_QUERY_LENGTH), dialect) : undefined; const querySummary = dbStatement ? _INTERNAL_getSqlQuerySummary(_INTERNAL_sanitizeSqlQuery(dbStatement, dialect)) : undefined; diff --git a/packages/server-utils/src/integrations/mysql.ts b/packages/server-utils/src/integrations/mysql.ts index 3876df576a0a..42879573882e 100644 --- a/packages/server-utils/src/integrations/mysql.ts +++ b/packages/server-utils/src/integrations/mysql.ts @@ -22,7 +22,11 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; -import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery } from '@sentry/core/server'; +import { + _INTERNAL_getSqlQuerySummary, + _INTERNAL_sanitizeSqlQuery, + filterCollectedDbQueryText, +} from '@sentry/core/server'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { mysqlModuleNames } from '../orchestrion/config/mysql'; @@ -91,10 +95,11 @@ function instrumentMysql(): void { const querySummary = sql ? _INTERNAL_getSqlQuerySummary(_INTERNAL_sanitizeSqlQuery(sql, 'mysql')) : undefined; const client = getClient(); + const queryText = filterCollectedDbQueryText(sql, 'mysql', client); const name = client && hasSpanStreamingEnabled(client) ? querySummary || database || DB_SYSTEM_NAME_VALUE_MYSQL - : (sql ?? 'mysql.query'); + : (queryText ?? 'mysql.query'); return startInactiveSpan({ name, @@ -106,7 +111,7 @@ function instrumentMysql(): void { [ATTR_DB_CONNECTION_STRING]: getJDBCString(host, portIsNumber ? portNumber : undefined, database), ...(database ? { [DB_NAMESPACE]: database } : {}), ...(user ? { [DB_USER]: user } : {}), - ...(sql ? { [DB_QUERY_TEXT]: sql } : {}), + ...(queryText ? { [DB_QUERY_TEXT]: queryText } : {}), [DB_QUERY_SUMMARY]: querySummary, [SERVER_ADDRESS]: host, [SERVER_PORT]: portIsNumber ? portNumber : undefined, diff --git a/packages/server-utils/src/integrations/mysql2/index.ts b/packages/server-utils/src/integrations/mysql2/index.ts index e00783fc27bb..453dbb813ba7 100644 --- a/packages/server-utils/src/integrations/mysql2/index.ts +++ b/packages/server-utils/src/integrations/mysql2/index.ts @@ -9,7 +9,11 @@ import { startInactiveSpan, waitForTracingChannelBinding, } from '@sentry/core'; -import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery } from '@sentry/core/server'; +import { + _INTERNAL_getSqlQuerySummary, + _INTERNAL_sanitizeSqlQuery, + filterCollectedDbQueryText, +} from '@sentry/core/server'; import { subscribeMysql2DiagnosticChannels } from './mysql2-dc-subscriber'; import type { ChannelName } from '../../orchestrion/channels'; import { CHANNELS } from '../../orchestrion/channels'; @@ -89,10 +93,11 @@ function subscribeQueryChannel(channelName: ChannelName): void { : undefined; const client = getClient(); + const queryText = filterCollectedDbQueryText(statement, 'mysql', client); const name = client && hasSpanStreamingEnabled(client) ? querySummary || (connectionAttributes[DB_NAMESPACE] as string | undefined) || DB_SYSTEM_VALUE_MYSQL - : (statement ?? 'mysql2.query'); + : (queryText ?? 'mysql2.query'); return startInactiveSpan({ name, @@ -101,7 +106,7 @@ function subscribeQueryChannel(channelName: ChannelName): void { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB, [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_MYSQL, - [DB_QUERY_TEXT]: statement || undefined, + [DB_QUERY_TEXT]: queryText || undefined, [DB_QUERY_SUMMARY]: querySummary, ...connectionAttributes, }, diff --git a/packages/server-utils/src/integrations/postgres.ts b/packages/server-utils/src/integrations/postgres.ts index 49ee295249a1..1633aedc327d 100644 --- a/packages/server-utils/src/integrations/postgres.ts +++ b/packages/server-utils/src/integrations/postgres.ts @@ -22,7 +22,11 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; -import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery } from '@sentry/core/server'; +import { + _INTERNAL_getSqlQuerySummary, + _INTERNAL_sanitizeSqlQuery, + filterCollectedDbQueryText, +} from '@sentry/core/server'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { pgModuleNames } from '../orchestrion/config/pg'; @@ -186,10 +190,11 @@ function querySpanOptions(ctx: PgChannelContext): { name: string; attributes: Sp ? _INTERNAL_getSqlQuerySummary(_INTERNAL_sanitizeSqlQuery(queryConfig.text)) : undefined; + const queryText = filterCollectedDbQueryText(queryConfig?.text, undefined, client); const name = client && hasSpanStreamingEnabled(client) ? querySummary || params.database || DB_SYSTEM_POSTGRESQL - : (queryConfig?.text ?? SPAN_QUERY_FALLBACK); + : (queryText ?? SPAN_QUERY_FALLBACK); return { name, @@ -197,7 +202,7 @@ function querySpanOptions(ctx: PgChannelContext): { name: string; attributes: Sp [SENTRY_OP]: DB, ...getConnectionAttributes(params), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [DB_QUERY_TEXT]: queryConfig?.text || undefined, + [DB_QUERY_TEXT]: queryText || undefined, [DB_QUERY_SUMMARY]: querySummary, [ATTR_PG_PLAN]: typeof queryConfig?.name === 'string' ? queryConfig.name : undefined, }, diff --git a/packages/server-utils/src/integrations/tedious.ts b/packages/server-utils/src/integrations/tedious.ts index b61e8a935242..9acd19d587fc 100644 --- a/packages/server-utils/src/integrations/tedious.ts +++ b/packages/server-utils/src/integrations/tedious.ts @@ -28,7 +28,11 @@ import { DB } from '@sentry/conventions/op'; import { CHANNELS } from '../orchestrion/channels'; import { tediousModuleNames } from '../orchestrion/config/tedious'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; -import { _INTERNAL_getSqlQuerySummary, _INTERNAL_sanitizeSqlQuery } from '@sentry/core/server'; +import { + _INTERNAL_getSqlQuerySummary, + _INTERNAL_sanitizeSqlQuery, + filterCollectedDbQueryText, +} from '@sentry/core/server'; // NOTE: this uses the same name as the OTel integration by design. When orchestrion injection is active, // `_init` swaps the OTel `Tedious` integration out of the defaults and appends this one (matched by name). @@ -143,7 +147,7 @@ function subscribeQuery(channelName: string, operation: string): void { [DB_NAMESPACE]: databaseName, // `>=4` uses the `authentication` object; older versions expose `userName` directly. [DB_USER]: connection.config?.userName ?? connection.config?.authentication?.options?.userName, - [DB_QUERY_TEXT]: sql, + [DB_QUERY_TEXT]: filterCollectedDbQueryText(sql), [DB_QUERY_SUMMARY]: querySummary, [ATTR_DB_SQL_TABLE]: request.table, [SERVER_ADDRESS]: connection.config?.server, From 73e6f7c0d84788457608a1b5ec8cdb7e601d5004 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:44:15 +0200 Subject: [PATCH 2/8] fix(server-utils): Always sanitize `db.query.text` instead of gating it Per OTel, query text may only be collected by default when literals are sanitized, and our docs say `databaseQueryData` does not control `db.query.text`. Sanitize unconditionally at every site that attached raw SQL and drop the `filterCollectedDbQueryText` gate. Co-Authored-By: Claude Opus 5 --- .../astro-6-cf-workers/tests/db.test.ts | 6 +-- .../astro-7-static/tests/db.test.ts | 4 +- .../astro-7/tests/db.test.ts | 4 +- .../bun-mysql/tests/mysql.test.ts | 6 +-- .../tests/mysql.test.ts | 6 +-- .../tests/db.test.ts | 4 +- .../create-remix-app-v2/tests/db.test.ts | 2 +- .../deno-mysql/tests/mysql.test.ts | 6 +-- .../deno-pg/tests/pg.test.ts | 6 +-- .../nextjs-16-static/tests/db-page.test.ts | 4 +- .../nextjs-16/tests/db-page.test.ts | 2 +- .../tests/server.test.ts | 2 +- .../node-express-v5/tests/mysql.test.ts | 2 +- .../nuxt-4-cloudflare/tests/db.test.ts | 6 +-- .../nuxt-4-static/tests/db-drivers.test.ts | 4 +- .../nuxt-4/tests/db-drivers.test.ts | 2 +- .../tests/performance/db.server.test.ts | 2 +- .../tests/db.test.ts | 4 +- .../solidstart-static/tests/db.test.ts | 4 +- .../solidstart/tests/db.test.ts | 6 +-- .../sveltekit-2-static/tests/db.test.ts | 4 +- .../sveltekit-2/tests/db.test.ts | 2 +- .../tests/db.test.ts | 6 +-- .../tests/db-drivers.test.ts | 4 +- .../suites/tracing/knex/mysql2/test.ts | 4 +- .../suites/tracing/knex/pg/test.ts | 4 +- .../suites/tracing/mysql/test.ts | 6 +-- .../suites/tracing/mysql2/test.ts | 8 ++-- .../suites/tracing/postgres/test.ts | 24 +++++------ .../suites/tracing/tedious/test.ts | 42 ++++++++----------- .../instrumentations/worker/instrumentD1.ts | 9 ++-- .../instrumentations/instrumentEnv.test.ts | 3 +- .../worker/instrumentD1.test.ts | 1 - packages/core/src/types/datacollection.ts | 2 +- .../src/runtime/utils/instrumentDatabase.ts | 9 ++-- packages/server-utils/src/exports.ts | 1 - .../server-utils/src/integrations/knex.ts | 7 ++-- .../server-utils/src/integrations/mysql.ts | 7 ++-- .../src/integrations/mysql2/index.ts | 5 +-- .../server-utils/src/integrations/postgres.ts | 10 ++--- .../server-utils/src/integrations/tedious.ts | 8 ++-- .../src/utils/filterCollectedDbQueryText.ts | 35 ---------------- .../utils/filterCollectedDbQueryText.test.ts | 23 ---------- 43 files changed, 121 insertions(+), 185 deletions(-) delete mode 100644 packages/server-utils/src/utils/filterCollectedDbQueryText.ts delete mode 100644 packages/server-utils/test/utils/filterCollectedDbQueryText.test.ts diff --git a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/db.test.ts index 751985066afd..d6037a92ade7 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/db.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/db.test.ts @@ -15,11 +15,11 @@ test('a real mysql query emits a db span with orchestrion-channel attributes', a const transactionEvent = await transactionPromise; const dbSpans = transactionEvent.spans!.filter(span => span.op === 'db'); - const firstQuery = dbSpans.find(span => span.description === 'SELECT 1 + 1 AS solution'); + const firstQuery = dbSpans.find(span => span.description === 'SELECT ? + ? AS solution'); expect(firstQuery).toBeDefined(); expect(firstQuery!.data?.['sentry.origin']).toBe('auto.db.mysql'); expect(firstQuery!.data?.['db.system.name']).toBe('mysql'); - expect(firstQuery!.data?.['db.query.text']).toBe('SELECT 1 + 1 AS solution'); + expect(firstQuery!.data?.['db.query.text']).toBe('SELECT ? + ? AS solution'); expect(firstQuery!.data?.['server.address']).toBe('127.0.0.1'); expect(firstQuery!.data?.['server.port']).toBe(3306); expect(firstQuery!.data?.['db.user']).toBe('root'); @@ -38,6 +38,6 @@ test('a nested query lands on the same transaction (async context restored)', as const transactionEvent = await transactionPromise; const descriptions = transactionEvent.spans!.filter(span => span.op === 'db').map(span => span.description); - expect(descriptions).toContain('SELECT 1 + 1 AS solution'); + expect(descriptions).toContain('SELECT ? + ? AS solution'); expect(descriptions).toContain('SELECT NOW()'); }); diff --git a/dev-packages/e2e-tests/test-applications/astro-7-static/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/astro-7-static/tests/db.test.ts index ffaf9124bcb2..21436c72e7f0 100644 --- a/dev-packages/e2e-tests/test-applications/astro-7-static/tests/db.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-7-static/tests/db.test.ts @@ -58,11 +58,11 @@ test('Instruments mysql automatically', async ({ baseURL }) => { expect.objectContaining({ op: 'db', origin: 'auto.db.mysql', - description: 'SELECT 1 + 1 AS solution', + description: 'SELECT ? + ? AS solution', status: 'ok', data: expect.objectContaining({ 'db.system.name': 'mysql', - 'db.query.text': 'SELECT 1 + 1 AS solution', + 'db.query.text': 'SELECT ? + ? AS solution', 'db.user': 'root', 'db.connection_string': expect.any(String), 'server.address': expect.any(String), diff --git a/dev-packages/e2e-tests/test-applications/astro-7/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/astro-7/tests/db.test.ts index a5fdaccc40ac..a7afae9cd2b1 100644 --- a/dev-packages/e2e-tests/test-applications/astro-7/tests/db.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-7/tests/db.test.ts @@ -58,11 +58,11 @@ test('Instruments mysql automatically', async ({ baseURL }) => { expect.objectContaining({ op: 'db', origin: 'auto.db.mysql', - description: 'SELECT 1 + 1 AS solution', + description: 'SELECT ? + ? AS solution', status: 'ok', data: expect.objectContaining({ 'db.system.name': 'mysql', - 'db.query.text': 'SELECT 1 + 1 AS solution', + 'db.query.text': 'SELECT ? + ? AS solution', 'db.user': 'root', 'db.connection_string': expect.any(String), 'server.address': expect.any(String), diff --git a/dev-packages/e2e-tests/test-applications/bun-mysql/tests/mysql.test.ts b/dev-packages/e2e-tests/test-applications/bun-mysql/tests/mysql.test.ts index 8437061fd1bd..643d71ef6e8d 100644 --- a/dev-packages/e2e-tests/test-applications/bun-mysql/tests/mysql.test.ts +++ b/dev-packages/e2e-tests/test-applications/bun-mysql/tests/mysql.test.ts @@ -25,14 +25,14 @@ test('mysql queries emit a db span with orchestrion-channel attributes', async ( const spans = await spansPromise; const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); - const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT 1 + 1 AS solution'); + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); expect(firstQuery).toBeDefined(); // With span streaming, db span names are the low-cardinality query summary, not the raw SQL expect(firstQuery!.name).toBe('SELECT'); expect(firstQuery!.attributes).toMatchObject({ 'sentry.origin': { value: 'auto.db.mysql', type: 'string' }, 'db.system.name': { value: 'mysql', type: 'string' }, - 'db.query.text': { value: 'SELECT 1 + 1 AS solution', type: 'string' }, + 'db.query.text': { value: 'SELECT ? + ? AS solution', type: 'string' }, 'server.port': { value: 3306, type: 'integer' }, 'db.user': { value: 'root', type: 'string' }, }); @@ -58,7 +58,7 @@ test('a nested query lands on the same trace (async context restored)', async ({ const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); const queries = dbSpans.map(span => span.attributes['db.query.text']?.value); - expect(queries).toContain('SELECT 1 + 1 AS solution'); + expect(queries).toContain('SELECT ? + ? AS solution'); expect(queries).toContain('SELECT NOW()'); expect(dbSpans.every(span => span.parent_span_id === segment.span_id)).toBe(true); }); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/tests/mysql.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/tests/mysql.test.ts index aff772dc73ed..c5ae3b42aa40 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/tests/mysql.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/tests/mysql.test.ts @@ -23,13 +23,13 @@ test('a real mysql query emits a db span with orchestrion-channel attributes', a const spans = await spansPromise; const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); - const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT 1 + 1 AS solution'); + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); expect(firstQuery).toBeDefined(); // With span streaming the span name is the low-cardinality query summary; the statement stays in `db.query.text`. expect(firstQuery!.name).toBe('SELECT'); expect(firstQuery!.attributes['sentry.origin']?.value).toBe('auto.db.mysql'); expect(firstQuery!.attributes['db.system.name']?.value).toBe('mysql'); - expect(firstQuery!.attributes['db.query.text']?.value).toBe('SELECT 1 + 1 AS solution'); + expect(firstQuery!.attributes['db.query.text']?.value).toBe('SELECT ? + ? AS solution'); expect(firstQuery!.attributes['server.address']?.value).toBe('127.0.0.1'); expect(firstQuery!.attributes['server.port']?.value).toBe(3306); expect(firstQuery!.attributes['db.user']?.value).toBe('root'); @@ -58,6 +58,6 @@ test('a nested query lands on the same trace (async context restored)', async ({ const queryTexts = spans .filter(span => getSpanOp(span) === 'db') .map(span => span.attributes['db.query.text']?.value); - expect(queryTexts).toContain('SELECT 1 + 1 AS solution'); + expect(queryTexts).toContain('SELECT ? + ? AS solution'); expect(queryTexts).toContain('SELECT NOW()'); }); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-static/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-static/tests/db.test.ts index 7e58fbb59577..be8068b2958c 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-static/tests/db.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-static/tests/db.test.ts @@ -62,11 +62,11 @@ test.describe('orchestrion DB instrumentation', () => { expect.objectContaining({ op: 'db', origin: 'auto.db.mysql', - description: 'SELECT 1 + 1 AS solution', + description: 'SELECT ? + ? AS solution', status: 'ok', data: expect.objectContaining({ 'db.system.name': 'mysql', - 'db.query.text': 'SELECT 1 + 1 AS solution', + 'db.query.text': 'SELECT ? + ? AS solution', 'db.user': 'root', 'db.connection_string': expect.any(String), 'server.address': expect.any(String), diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/db.test.ts index 2fae830957cd..aa7d75a20127 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/db.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/db.test.ts @@ -53,7 +53,7 @@ test.describe('orchestrion DB instrumentation', () => { // With span streaming the span name is the low-cardinality query summary; the statement // stays in `db.query.text`. - for (const query of ['SELECT 1 + 1 AS solution', 'SELECT NOW()']) { + for (const query of ['SELECT ? + ? AS solution', 'SELECT NOW()']) { expect(mysqlSpans).toContainEqual( expect.objectContaining({ name: 'SELECT', diff --git a/dev-packages/e2e-tests/test-applications/deno-mysql/tests/mysql.test.ts b/dev-packages/e2e-tests/test-applications/deno-mysql/tests/mysql.test.ts index 044336f28ae1..0ac07ed48870 100644 --- a/dev-packages/e2e-tests/test-applications/deno-mysql/tests/mysql.test.ts +++ b/dev-packages/e2e-tests/test-applications/deno-mysql/tests/mysql.test.ts @@ -24,14 +24,14 @@ test('mysql queries emit a db span with orchestrion-channel attributes', async ( const spans = await spansPromise; const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); - const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT 1 + 1 AS solution'); + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); expect(firstQuery).toBeDefined(); // With span streaming, db span names are the low-cardinality query summary, not the raw SQL expect(firstQuery!.name).toBe('SELECT'); expect(firstQuery!.attributes).toMatchObject({ 'sentry.origin': { value: 'auto.db.mysql', type: 'string' }, 'db.system.name': { value: 'mysql', type: 'string' }, - 'db.query.text': { value: 'SELECT 1 + 1 AS solution', type: 'string' }, + 'db.query.text': { value: 'SELECT ? + ? AS solution', type: 'string' }, 'server.port': { value: 3306, type: 'integer' }, 'db.user': { value: 'root', type: 'string' }, }); @@ -57,7 +57,7 @@ test('a nested query lands on the same trace (AsyncLocalStorage context restored const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); const queries = dbSpans.map(span => span.attributes['db.query.text']?.value); - expect(queries).toContain('SELECT 1 + 1 AS solution'); + expect(queries).toContain('SELECT ? + ? AS solution'); expect(queries).toContain('SELECT NOW()'); expect(dbSpans.every(span => span.parent_span_id === segment.span_id)).toBe(true); }); diff --git a/dev-packages/e2e-tests/test-applications/deno-pg/tests/pg.test.ts b/dev-packages/e2e-tests/test-applications/deno-pg/tests/pg.test.ts index 48b07cb5b80e..32f24dcb43a4 100644 --- a/dev-packages/e2e-tests/test-applications/deno-pg/tests/pg.test.ts +++ b/dev-packages/e2e-tests/test-applications/deno-pg/tests/pg.test.ts @@ -24,14 +24,14 @@ test('pg queries emit a db span with orchestrion-channel attributes', async ({ b const spans = await spansPromise; const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); - const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT 1 + 1 AS solution'); + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); expect(firstQuery).toBeDefined(); // With span streaming, db span names are the low-cardinality query summary, not the raw SQL expect(firstQuery!.name).toBe('SELECT'); expect(firstQuery!.attributes).toMatchObject({ 'sentry.origin': { value: 'auto.db.postgres', type: 'string' }, 'db.system.name': { value: 'postgresql', type: 'string' }, - 'db.query.text': { value: 'SELECT 1 + 1 AS solution', type: 'string' }, + 'db.query.text': { value: 'SELECT ? + ? AS solution', type: 'string' }, 'server.port': { value: 5432, type: 'integer' }, 'db.user': { value: 'postgres', type: 'string' }, }); @@ -58,7 +58,7 @@ test('a nested query lands on the same trace (AsyncLocalStorage context restored const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); const queries = dbSpans.map(span => span.attributes['db.query.text']?.value); - expect(queries).toContain('SELECT 1 + 1 AS solution'); + expect(queries).toContain('SELECT ? + ? AS solution'); expect(queries).toContain('SELECT NOW()'); expect(dbSpans.every(span => span.parent_span_id === segment.span_id)).toBe(true); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-static/tests/db-page.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-static/tests/db-page.test.ts index f82a49550158..2627601534de 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-static/tests/db-page.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-static/tests/db-page.test.ts @@ -20,11 +20,11 @@ test('Instruments DB calls made during server-side rendering of a page', async ( expect.objectContaining({ op: 'db', origin: 'auto.db.postgres', - description: 'SELECT 40 + 2 AS answer', + description: 'SELECT ? + ? AS answer', status: 'ok', data: expect.objectContaining({ 'db.system.name': 'postgresql', - 'db.query.text': 'SELECT 40 + 2 AS answer', + 'db.query.text': 'SELECT ? + ? AS answer', }), }), ); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/db-page.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/db-page.test.ts index 30392d2bf741..968891a085c4 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/db-page.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/db-page.test.ts @@ -21,7 +21,7 @@ test('Instruments DB calls made during server-side rendering of a page', async ( 'sentry.op': { value: 'db', type: 'string' }, 'sentry.origin': { value: 'auto.db.postgres', type: 'string' }, 'db.system.name': { value: 'postgresql', type: 'string' }, - 'db.query.text': { value: 'SELECT 40 + 2 AS answer', type: 'string' }, + 'db.query.text': { value: 'SELECT ? + ? AS answer', type: 'string' }, }), }), ); diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/tests/server.test.ts b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/tests/server.test.ts index 9b6f44f13041..a396f5718286 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/tests/server.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/tests/server.test.ts @@ -131,7 +131,7 @@ test('Instruments MySQL via Orchestrion', async ({ baseURL }) => { expect.objectContaining({ op: 'db', origin: 'auto.db.mysql', - description: 'SELECT 1 + 1 AS solution', + description: 'SELECT ? + ? AS solution', }), ); expect(spans).toContainEqual( diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mysql.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mysql.test.ts index 76849657bd30..2fb57774c50b 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mysql.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mysql.test.ts @@ -20,7 +20,7 @@ test('Instruments MySQL via Orchestrion', async ({ baseURL }) => { expect.objectContaining({ op: 'db', origin: 'auto.db.mysql', - description: 'SELECT 1 + 1 AS solution', + description: 'SELECT ? + ? AS solution', }), ); expect(spans).toContainEqual( diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/db.test.ts index af9f1e7259d4..62bfdb837e53 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/db.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/db.test.ts @@ -26,13 +26,13 @@ test('a real mysql query emits a db span with orchestrion-channel attributes', a const dbSpans = spans.filter(span => getSpanOp(span) === 'db' && span.trace_id === rootSpan!.trace_id); - const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT 1 + 1 AS solution'); + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); expect(firstQuery).toBeDefined(); expect(firstQuery!.name).toBe('SELECT'); expect(firstQuery!.attributes).toMatchObject({ 'sentry.origin': { type: 'string', value: 'auto.db.mysql' }, 'db.system.name': { type: 'string', value: 'mysql' }, - 'db.query.text': { type: 'string', value: 'SELECT 1 + 1 AS solution' }, + 'db.query.text': { type: 'string', value: 'SELECT ? + ? AS solution' }, 'server.address': { type: 'string', value: '127.0.0.1' }, 'server.port': { type: 'integer', value: 3306 }, 'db.user': { type: 'string', value: 'root' }, @@ -52,6 +52,6 @@ test('a nested query lands on the same trace (async context restored)', async ({ const queryTexts = spans .filter(span => getSpanOp(span) === 'db' && span.trace_id === rootSpan!.trace_id) .map(span => span.attributes['db.query.text']?.value); - expect(queryTexts).toContain('SELECT 1 + 1 AS solution'); + expect(queryTexts).toContain('SELECT ? + ? AS solution'); expect(queryTexts).toContain('SELECT NOW()'); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/db-drivers.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/db-drivers.test.ts index 5ef0c290e946..4e265221db2b 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/db-drivers.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/db-drivers.test.ts @@ -70,11 +70,11 @@ test('Instruments mysql automatically', async ({ baseURL }) => { expect.objectContaining({ op: 'db', origin: 'auto.db.mysql', - description: 'SELECT 1 + 1 AS solution', + description: 'SELECT ? + ? AS solution', status: 'ok', data: expect.objectContaining({ 'db.system.name': 'mysql', - 'db.query.text': 'SELECT 1 + 1 AS solution', + 'db.query.text': 'SELECT ? + ? AS solution', 'db.user': 'root', 'db.connection_string': expect.any(String), 'server.address': expect.any(String), diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/db-drivers.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/db-drivers.test.ts index db585cb03a4a..e745198bcf02 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/db-drivers.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/db-drivers.test.ts @@ -83,7 +83,7 @@ test('Instruments mysql automatically', async ({ baseURL }) => { 'sentry.op': { type: 'string', value: 'db' }, 'sentry.origin': { type: 'string', value: 'auto.db.mysql' }, 'db.system.name': { type: 'string', value: 'mysql' }, - 'db.query.text': { type: 'string', value: 'SELECT 1 + 1 AS solution' }, + 'db.query.text': { type: 'string', value: 'SELECT ? + ? AS solution' }, 'db.query.summary': { type: 'string', value: 'SELECT' }, 'db.user': { type: 'string', value: 'root' }, 'db.connection_string': { type: 'string', value: expect.any(String) }, diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/db.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/db.server.test.ts index ed0f3e415c36..d0013168b95f 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/db.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/db.server.test.ts @@ -77,7 +77,7 @@ test.describe('server - orchestrion db instrumentation', () => { const spans = await spansPromise; - for (const queryText of ['SELECT 1 + 1 AS solution', 'SELECT NOW()']) { + for (const queryText of ['SELECT ? + ? AS solution', 'SELECT NOW()']) { expect(spans).toContainEqual( expect.objectContaining({ name: 'SELECT', diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts index ebef990b9bfc..1dfa920d1625 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts @@ -17,7 +17,7 @@ test('a real mysql query emits a db span with orchestrion-channel attributes', a const spans = await spansPromise; const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); - const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT 1 + 1 AS solution'); + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); expect(firstQuery).toBeDefined(); expect(firstQuery!.name).toBe('SELECT'); expect(firstQuery!.attributes['sentry.origin']?.value).toBe('auto.db.mysql'); @@ -45,6 +45,6 @@ test('a nested query lands on the same segment (async context restored)', async const queryTexts = spans .filter(span => getSpanOp(span) === 'db') .map(span => span.attributes['db.query.text']?.value); - expect(queryTexts).toContain('SELECT 1 + 1 AS solution'); + expect(queryTexts).toContain('SELECT ? + ? AS solution'); expect(queryTexts).toContain('SELECT NOW()'); }); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-static/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-static/tests/db.test.ts index 3506759509db..2fa4db593d0c 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-static/tests/db.test.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart-static/tests/db.test.ts @@ -57,11 +57,11 @@ test('Instruments mysql automatically via build-time orchestrion', async ({ base expect.objectContaining({ op: 'db', origin: 'auto.db.mysql', - description: 'SELECT 1 + 1 AS solution', + description: 'SELECT ? + ? AS solution', status: 'ok', data: expect.objectContaining({ 'db.system.name': 'mysql', - 'db.query.text': 'SELECT 1 + 1 AS solution', + 'db.query.text': 'SELECT ? + ? AS solution', 'db.user': 'root', 'db.connection_string': expect.any(String), 'server.address': expect.any(String), diff --git a/dev-packages/e2e-tests/test-applications/solidstart/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/solidstart/tests/db.test.ts index 7143c25aa30f..0c546aba56f1 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/tests/db.test.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart/tests/db.test.ts @@ -64,7 +64,7 @@ test('Instruments mysql automatically via build-time orchestrion', async ({ base getSpanOp(span) === 'http.server' && String(span.attributes['url.path']?.value ?? '').includes('db-mysql'), ) && - spans.some(span => span.attributes['db.query.text']?.value === 'SELECT 1 + 1 AS solution') && + spans.some(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution') && spans.some(span => span.attributes['db.query.text']?.value === 'SELECT NOW()'), ); @@ -73,7 +73,7 @@ test('Instruments mysql automatically via build-time orchestrion', async ({ base const spans = await spansPromise; const mysqlSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.db.mysql'); - const firstQuery = mysqlSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT 1 + 1 AS solution'); + const firstQuery = mysqlSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); expect(firstQuery).toBeDefined(); expect(firstQuery!.name).toBe('SELECT'); expect(firstQuery!.status).toBe('ok'); @@ -81,7 +81,7 @@ test('Instruments mysql automatically via build-time orchestrion', async ({ base 'sentry.op': { type: 'string', value: 'db' }, 'sentry.origin': { type: 'string', value: 'auto.db.mysql' }, 'db.system.name': { type: 'string', value: 'mysql' }, - 'db.query.text': { type: 'string', value: 'SELECT 1 + 1 AS solution' }, + 'db.query.text': { type: 'string', value: 'SELECT ? + ? AS solution' }, 'db.user': { type: 'string', value: 'root' }, 'db.connection_string': { type: 'string', value: expect.any(String) }, 'server.address': { type: 'string', value: expect.any(String) }, diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-static/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-static/tests/db.test.ts index 7deff11315f3..439c2133e6f2 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-static/tests/db.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-static/tests/db.test.ts @@ -58,11 +58,11 @@ test('Instruments mysql automatically', async ({ baseURL }) => { expect.objectContaining({ op: 'db', origin: 'auto.db.mysql', - description: 'SELECT 1 + 1 AS solution', + description: 'SELECT ? + ? AS solution', status: 'ok', data: expect.objectContaining({ 'db.system.name': 'mysql', - 'db.query.text': 'SELECT 1 + 1 AS solution', + 'db.query.text': 'SELECT ? + ? AS solution', 'db.user': 'root', 'db.connection_string': expect.any(String), 'server.address': expect.any(String), diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/db.test.ts index a34026797147..e4a7d2d6acd2 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/db.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/db.test.ts @@ -60,6 +60,6 @@ test('Instruments mysql automatically', async ({ baseURL }) => { }), }); - expect(traceSpans).toContainEqual(mysqlSpan('SELECT 1 + 1 AS solution')); + expect(traceSpans).toContainEqual(mysqlSpan('SELECT ? + ? AS solution')); expect(traceSpans).toContainEqual(mysqlSpan('SELECT NOW()')); }); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tests/db.test.ts index 458f1212de44..e2db562af336 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tests/db.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tests/db.test.ts @@ -18,13 +18,13 @@ test('a real mysql query emits a db span with orchestrion-channel attributes', a const traceSpans = await traceSpansPromise; const dbSpans = traceSpans.filter(span => getSpanOp(span) === 'db'); - const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT 1 + 1 AS solution'); + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); expect(firstQuery).toBeDefined(); expect(firstQuery!.name).toBe('SELECT'); expect(firstQuery!.attributes).toMatchObject({ 'sentry.origin': { value: 'auto.db.mysql', type: 'string' }, 'db.system.name': { value: 'mysql', type: 'string' }, - 'db.query.text': { value: 'SELECT 1 + 1 AS solution', type: 'string' }, + 'db.query.text': { value: 'SELECT ? + ? AS solution', type: 'string' }, 'db.query.summary': { value: 'SELECT', type: 'string' }, 'server.address': { value: '127.0.0.1', type: 'string' }, 'server.port': { value: 3306, type: 'integer' }, @@ -49,7 +49,7 @@ test('a nested query lands on the same trace (async context restored)', async ({ const dbSpans = traceSpans.filter(span => getSpanOp(span) === 'db'); const queryTexts = dbSpans.map(span => span.attributes['db.query.text']?.value); - expect(queryTexts).toContain('SELECT 1 + 1 AS solution'); + expect(queryTexts).toContain('SELECT ? + ? AS solution'); expect(queryTexts).toContain('SELECT NOW()'); const parentSpanIds = new Set(dbSpans.map(span => span.parent_span_id)); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/db-drivers.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/db-drivers.test.ts index cbd54ffb2c96..a8cac8f5e3b6 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/db-drivers.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/db-drivers.test.ts @@ -67,11 +67,11 @@ test('Instruments mysql automatically', async ({ baseURL }) => { expect.objectContaining({ op: 'db', origin: 'auto.db.mysql', - description: 'SELECT 1 + 1 AS solution', + description: 'SELECT ? + ? AS solution', status: 'ok', data: expect.objectContaining({ 'db.system.name': 'mysql', - 'db.query.text': 'SELECT 1 + 1 AS solution', + 'db.query.text': 'SELECT ? + ? AS solution', 'db.user': 'root', 'db.connection_string': expect.any(String), 'server.address': expect.any(String), diff --git a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts index d5f615dbaaec..98cb1127906b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts @@ -25,7 +25,7 @@ describeWithDockerCompose('knex auto instrumentation', { workingDirectory: [__di }), status: 'ok', description: - 'create table `User` (`id` int unsigned not null auto_increment primary key, `createdAt` timestamp(3) not null default CURRENT_TIMESTAMP(3), `email` text not null, `name` text not null)', + 'create table `User` (`id` int unsigned not null auto_increment primary key, `createdAt` timestamp(?) not null default CURRENT_TIMESTAMP(?), `email` text not null, `name` text not null)', origin: ORIGIN, }), expect.objectContaining({ @@ -86,7 +86,7 @@ describeWithDockerCompose('knex auto instrumentation', { workingDirectory: [__di { name: 'create table `User`', summary: 'create table `User`', - text: 'create table `User` (`id` int unsigned not null auto_increment primary key, `createdAt` timestamp(3) not null default CURRENT_TIMESTAMP(3), `email` text not null, `name` text not null)', + text: 'create table `User` (`id` int unsigned not null auto_increment primary key, `createdAt` timestamp(?) not null default CURRENT_TIMESTAMP(?), `email` text not null, `name` text not null)', }, { name: 'insert `User`', diff --git a/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts b/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts index a85a4d0250e3..5d1da5ad89ff 100644 --- a/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts @@ -24,7 +24,7 @@ describe('knex auto instrumentation', () => { }), status: 'ok', description: - 'create table "User" ("id" serial primary key, "createdAt" timestamptz(3) not null default CURRENT_TIMESTAMP(3), "email" text not null, "name" text not null)', + 'create table "User" ("id" serial primary key, "createdAt" timestamptz(?) not null default CURRENT_TIMESTAMP(?), "email" text not null, "name" text not null)', origin: ORIGIN, }), expect.objectContaining({ @@ -100,7 +100,7 @@ describe('knex auto instrumentation', () => { { name: 'create table "User"', summary: 'create table "User"', - text: 'create table "User" ("id" serial primary key, "createdAt" timestamptz(3) not null default CURRENT_TIMESTAMP(3), "email" text not null, "name" text not null)', + text: 'create table "User" ("id" serial primary key, "createdAt" timestamptz(?) not null default CURRENT_TIMESTAMP(?), "email" text not null, "name" text not null)', }, { name: 'insert "User"', diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts index b46624189f5d..53619776fdea 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts @@ -54,7 +54,7 @@ describe('mysql auto instrumentation', () => { return { transaction: 'Test Transaction', - spans: expect.arrayContaining([span('SELECT 1 + 1 AS solution'), span('SELECT NOW()')]), + spans: expect.arrayContaining([span('SELECT ? + ? AS solution'), span('SELECT NOW()')]), ...(override ?? {}), }; } @@ -144,7 +144,7 @@ describe('mysql auto instrumentation', () => { transaction: (transaction): void => { const transactionSpanId = transaction.contexts?.trace?.span_id; const spans = transaction.spans ?? []; - const mysqlSpan = spans.find(span => span.description === 'SELECT 1 + 1 AS solution'); + const mysqlSpan = spans.find(span => span.description === 'SELECT ? + ? AS solution'); const listenerSpan = spans.find(span => span.description === 'listener-child'); const innerSpan = spans.find(span => span.description === 'inner-span'); @@ -261,7 +261,7 @@ describe('mysql auto instrumentation', () => { ...COMMON_ATTRIBUTES, 'db.query.text': { type: 'string', - value: 'SELECT 1 + 1 AS solution', + value: 'SELECT ? + ? AS solution', }, 'db.query.summary': { type: 'string', diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts index f0fb3e4ee9a4..9733d4c7b614 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts @@ -14,12 +14,12 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ transaction: 'Test Transaction', spans: expect.arrayContaining([ expect.objectContaining({ - description: 'SELECT 1 + 1 AS solution', + description: 'SELECT ? + ? AS solution', op: 'db', origin: ORIGIN, data: expect.objectContaining({ 'db.system.name': 'mysql', - 'db.query.text': 'SELECT 1 + 1 AS solution', + 'db.query.text': 'SELECT ? + ? AS solution', 'server.address': 'localhost', 'server.port': 3306, 'db.user': 'root', @@ -50,12 +50,12 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ }), // `execute` is instrumented the same way as `query` expect.objectContaining({ - description: 'SELECT 42 AS answer', + description: 'SELECT ? AS answer', op: 'db', origin: ORIGIN, data: expect.objectContaining({ 'db.system.name': 'mysql', - 'db.query.text': 'SELECT 42 AS answer', + 'db.query.text': 'SELECT ? AS answer', }), }), // a failing query produces a span with an error status diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts index 6907c5e1c210..ce96a147a315 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts @@ -159,11 +159,11 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [ data: expect.objectContaining({ 'db.system.name': 'postgresql', 'db.namespace': 'tests', - 'db.query.text': 'SELECT 1 AS foo', + 'db.query.text': 'SELECT ? AS foo', 'sentry.origin': QUERY_ORIGIN, 'sentry.op': 'db', }), - description: 'SELECT 1 AS foo', + description: 'SELECT ? AS foo', op: 'db', status: 'ok', origin: QUERY_ORIGIN, @@ -220,11 +220,11 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [ data: expect.objectContaining({ 'db.system.name': 'postgresql', 'db.namespace': 'tests', - 'db.query.text': 'SELECT 1 AS connect_then', + 'db.query.text': 'SELECT ? AS connect_then', 'sentry.origin': QUERY_ORIGIN, 'sentry.op': 'db', }), - description: 'SELECT 1 AS connect_then', + description: 'SELECT ? AS connect_then', op: 'db', status: 'ok', origin: QUERY_ORIGIN, @@ -247,7 +247,7 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [ transaction: txn => { const descriptions = txn.spans?.map(span => span.description) ?? []; // The unparented connect + query must not have produced spans - expect(descriptions).not.toContain('SELECT 1 AS unparented'); + expect(descriptions).not.toContain('SELECT ? AS unparented'); expect(descriptions.find(name => name?.includes('connect'))).toBeUndefined(); // Only the parented query is instrumented expect(txn).toMatchObject({ @@ -257,11 +257,11 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [ data: expect.objectContaining({ 'db.system.name': 'postgresql', 'db.namespace': 'tests', - 'db.query.text': 'SELECT 2 AS parented', + 'db.query.text': 'SELECT ? AS parented', 'sentry.origin': QUERY_ORIGIN, 'sentry.op': 'db', }), - description: 'SELECT 2 AS parented', + description: 'SELECT ? AS parented', op: 'db', status: 'ok', origin: QUERY_ORIGIN, @@ -424,11 +424,11 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [ data: expect.objectContaining({ 'db.system.name': 'postgresql', 'db.namespace': 'tests', - 'db.query.text': 'SELECT 1 AS foo', + 'db.query.text': 'SELECT ? AS foo', 'sentry.origin': ORIGIN, 'sentry.op': 'db', }), - description: 'SELECT 1 AS foo', + description: 'SELECT ? AS foo', op: 'db', status: 'ok', origin: ORIGIN, @@ -491,7 +491,7 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [ .expect({ transaction: txn => { const descriptions = txn.spans?.map(span => span.description) ?? []; - expect(descriptions).not.toContain('SELECT 1 AS unparented'); + expect(descriptions).not.toContain('SELECT ? AS unparented'); expect(descriptions.find(name => name?.includes('connect'))).toBeUndefined(); expect(txn).toMatchObject({ transaction: 'Test Transaction', @@ -499,11 +499,11 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [ expect.objectContaining({ data: expect.objectContaining({ 'db.system.name': 'postgresql', - 'db.query.text': 'SELECT 2 AS parented', + 'db.query.text': 'SELECT ? AS parented', 'sentry.origin': ORIGIN, 'sentry.op': 'db', }), - description: 'SELECT 2 AS parented', + description: 'SELECT ? AS parented', op: 'db', status: 'ok', origin: ORIGIN, diff --git a/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts b/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts index 54d63eea226c..9e9d6b7e3291 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts @@ -27,8 +27,8 @@ describeWithDockerCompose('tedious auto instrumentation', { workingDirectory: [_ const EXPECTED_TRANSACTION = { transaction: 'Test Transaction', spans: expect.arrayContaining([ - dbSpan({ description: 'SELECT 1 + 1 AS solution', status: 'ok' }), - dbSpan({ description: 'SELECT 42; SELECT 42;', status: 'ok' }), + dbSpan({ description: 'SELECT ? + ? AS solution', status: 'ok' }), + dbSpan({ description: 'SELECT ?; SELECT ?', status: 'ok' }), dbSpan({ description: 'select !', status: 'internal_error' }), dbSpan({ description: '[dbo].[test_proced]', status: 'ok' }), dbSpan({ description: 'INSERT INTO [dbo].[test_prepared] VALUES (@val1, @val2)', status: 'ok' }), @@ -51,27 +51,25 @@ describeWithDockerCompose('tedious auto instrumentation', { workingDirectory: [_ expect(transaction.spans).toEqual(EXPECTED_TRANSACTION.spans); const CREATE_PROCEDURE = - 'CREATE OR ALTER PROCEDURE [dbo].[test_proced] @inputVal varchar(30), @outputCount int OUTPUT AS set @outputCount = LEN(@inputVal);'; - const CREATE_PREPARED_TABLE = - "if object_id('[dbo].[test_prepared]') is null CREATE TABLE [dbo].[test_prepared] (c1 int, c2 int)"; - const CREATE_BULK_TABLE = - "if object_id('[dbo].[test_bulk]') is null CREATE TABLE [dbo].[test_bulk] (c1 int, c2 varchar(30))"; + 'CREATE OR ALTER PROCEDURE [dbo].[test_proced] @inputVal varchar(?), @outputCount int OUTPUT AS set @outputCount = LEN(@inputVal)'; + const CREATE_PREPARED_TABLE = 'if object_id(?) is null CREATE TABLE [dbo].[test_prepared] (c1 int, c2 int)'; + const CREATE_BULK_TABLE = 'if object_id(?) is null CREATE TABLE [dbo].[test_bulk] (c1 int, c2 varchar(?))'; const INSERT_PREPARED = 'INSERT INTO [dbo].[test_prepared] VALUES (@val1, @val2)'; - const INSERT_BULK = 'insert bulk test_bulk([c1] int, [c2] nvarchar(50)) WITH (KEEP_NULLS)'; + const INSERT_BULK = 'insert bulk test_bulk([c1] int, [c2] nvarchar(?)) WITH (KEEP_NULLS)'; const SELECT_PREPARED = 'SELECT c1, c2 FROM [dbo].[test_prepared]'; const SELECT_JOIN = 'SELECT p.c1 FROM [dbo].[test_prepared] p INNER JOIN [dbo].[test_bulk] b ON p.c1 = b.c1'; - const SELECT_INLINE_LITERAL = 'SELECT c1, c2 FROM [dbo].[test_prepared] WHERE c1 = 42'; + const SELECT_INLINE_LITERAL = 'SELECT c1, c2 FROM [dbo].[test_prepared] WHERE c1 = ?'; const SELECT_PARAMETERIZED = 'SELECT c1, c2 FROM [dbo].[test_prepared] WHERE c1 = @c1'; - const SELECT_STRING_LITERAL = "SELECT c1, c2 FROM [dbo].[test_bulk] WHERE c2 = 'hello from acme'"; + const SELECT_STRING_LITERAL = 'SELECT c1, c2 FROM [dbo].[test_bulk] WHERE c2 = ?'; expect( (transaction.spans ?? []) .filter(span => span.origin === ORIGIN) .map(span => ({ name: span.description, text: span.data?.['db.query.text'] })), ).toEqual([ - { name: 'SELECT 1 + 1 AS solution', text: 'SELECT 1 + 1 AS solution' }, - { name: 'SELECT 42; SELECT 42;', text: 'SELECT 42; SELECT 42;' }, + { name: 'SELECT ? + ? AS solution', text: 'SELECT ? + ? AS solution' }, + { name: 'SELECT ?; SELECT ?', text: 'SELECT ?; SELECT ?' }, { name: 'select !', text: 'select !' }, { name: CREATE_PROCEDURE, text: CREATE_PROCEDURE }, { name: '[dbo].[test_proced]', text: '[dbo].[test_proced]' }, @@ -108,19 +106,19 @@ describeWithDockerCompose('tedious auto instrumentation', { workingDirectory: [_ text: span.attributes['db.query.text']?.value, })), ).toEqual([ - { name: 'SELECT', summary: 'SELECT', text: 'SELECT 1 + 1 AS solution' }, - { name: 'SELECT', summary: 'SELECT', text: 'SELECT 42; SELECT 42;' }, + { name: 'SELECT', summary: 'SELECT', text: 'SELECT ? + ? AS solution' }, + { name: 'SELECT', summary: 'SELECT', text: 'SELECT ?; SELECT ?' }, { name: 'select', summary: 'select', text: 'select !' }, { name: 'CREATE', summary: 'CREATE', - text: 'CREATE OR ALTER PROCEDURE [dbo].[test_proced] @inputVal varchar(30), @outputCount int OUTPUT AS set @outputCount = LEN(@inputVal);', + text: 'CREATE OR ALTER PROCEDURE [dbo].[test_proced] @inputVal varchar(?), @outputCount int OUTPUT AS set @outputCount = LEN(@inputVal)', }, { name: 'callProcedure [dbo].[test_proced]', summary: undefined, text: '[dbo].[test_proced]' }, { name: 'if', summary: 'if', - text: "if object_id('[dbo].[test_prepared]') is null CREATE TABLE [dbo].[test_prepared] (c1 int, c2 int)", + text: 'if object_id(?) is null CREATE TABLE [dbo].[test_prepared] (c1 int, c2 int)', }, { name: 'INSERT [dbo].[test_prepared]', @@ -135,12 +133,12 @@ describeWithDockerCompose('tedious auto instrumentation', { workingDirectory: [_ { name: 'if', summary: 'if', - text: "if object_id('[dbo].[test_bulk]') is null CREATE TABLE [dbo].[test_bulk] (c1 int, c2 varchar(30))", + text: 'if object_id(?) is null CREATE TABLE [dbo].[test_bulk] (c1 int, c2 varchar(?))', }, { name: 'insert', summary: 'insert', - text: 'insert bulk test_bulk([c1] int, [c2] nvarchar(50)) WITH (KEEP_NULLS)', + text: 'insert bulk test_bulk([c1] int, [c2] nvarchar(?)) WITH (KEEP_NULLS)', }, { name: 'execBulkLoad test_bulk', summary: undefined, text: undefined }, { @@ -155,11 +153,9 @@ describeWithDockerCompose('tedious auto instrumentation', { workingDirectory: [_ text: 'SELECT p.c1 FROM [dbo].[test_prepared] p INNER JOIN [dbo].[test_bulk] b ON p.c1 = b.c1', }, { - // TODO: (fix) tedious reports the statement as the caller wrote it, so an inlined literal reaches - // `db.query.text` unsanitized. Only the summary is sanitized. name: 'SELECT [dbo].[test_prepared]', summary: 'SELECT [dbo].[test_prepared]', - text: 'SELECT c1, c2 FROM [dbo].[test_prepared] WHERE c1 = 42', + text: 'SELECT c1, c2 FROM [dbo].[test_prepared] WHERE c1 = ?', }, { name: 'SELECT [dbo].[test_prepared]', @@ -167,11 +163,9 @@ describeWithDockerCompose('tedious auto instrumentation', { workingDirectory: [_ text: 'SELECT c1, c2 FROM [dbo].[test_prepared] WHERE c1 = @c1', }, { - // TODO: (fix) The `from` inside the string literal must not be read as a table: the statement is - // sanitized before it is summarized, so the summary is just the real table. name: 'SELECT [dbo].[test_bulk]', summary: 'SELECT [dbo].[test_bulk]', - text: "SELECT c1, c2 FROM [dbo].[test_bulk] WHERE c2 = 'hello from acme'", + text: 'SELECT c1, c2 FROM [dbo].[test_bulk] WHERE c2 = ?', }, ]); }, diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts b/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts index 74e6a4daa5b7..147b55c573ff 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts @@ -11,7 +11,7 @@ import { SPAN_STATUS_ERROR, startSpan, } from '@sentry/core'; -import { filterCollectedDbQueryText, getSqlQuerySummary, sanitizeSqlQuery } from '@sentry/server-utils'; +import { getSqlQuerySummary, sanitizeSqlQuery } from '@sentry/server-utils'; import { ensureInstrumented } from '../../instrument'; // Patching is based on internal Cloudflare D1 API @@ -122,7 +122,8 @@ type D1QueryType = 'first' | 'run' | 'all' | 'raw' | 'batch' | 'exec'; function createD1Breadcrumb(query: string, type: D1QueryType, d1Result?: D1Response): void { addBreadcrumb({ category: 'query', - message: query, + // The breadcrumb carries the same query text as the span, so it is sanitized the same way. + message: sanitizeSqlQuery(query), data: { ...(d1Result ? getAttributesFromD1Response(d1Result) : {}), 'db.operation.name': type, @@ -131,10 +132,10 @@ function createD1Breadcrumb(query: string, type: D1QueryType, d1Result?: D1Respo } function createStartSpanOptions(query: string, type: D1QueryType): StartSpanOptions { - const querySummary = query ? getSqlQuerySummary(sanitizeSqlQuery(query)) : undefined; + const queryText = sanitizeSqlQuery(query); + const querySummary = query ? getSqlQuerySummary(queryText) : undefined; const client = getClient(); - const queryText = filterCollectedDbQueryText(query, undefined, client); const name = client && hasSpanStreamingEnabled(client) ? querySummary || 'cloudflare-d1' : queryText; return { diff --git a/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts b/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts index 70f680d52853..3e1f366e37e1 100644 --- a/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts +++ b/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts @@ -40,7 +40,8 @@ describe('instrumentEnv', () => { await db.prepare('SELECT 1').first(); expect(startSpanSpy).toHaveBeenCalledWith( - expect.objectContaining({ name: 'SELECT 1', attributes: expect.objectContaining({ 'sentry.op': 'db.query' }) }), + // The numeric literal is sanitized out of the query text. + expect.objectContaining({ name: 'SELECT ?', attributes: expect.objectContaining({ 'sentry.op': 'db.query' }) }), expect.any(Function), ); }); diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts index 7a42f748cabd..b59f0fc5918a 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts @@ -116,7 +116,6 @@ describe('instrumentD1', () => { beforeEach(() => { getClientSpy = vi.spyOn(SentryCore, 'getClient').mockReturnValue({ getOptions: () => ({ traceLifecycle: 'stream' }), - getDataCollectionOptions: () => ({ databaseQueryData: true }), } as unknown as ReturnType); }); diff --git a/packages/core/src/types/datacollection.ts b/packages/core/src/types/datacollection.ts index 8815170eb04e..db4f97a82a77 100644 --- a/packages/core/src/types/datacollection.ts +++ b/packages/core/src/types/datacollection.ts @@ -79,7 +79,7 @@ export interface DataCollection { }; /** - * Include data associated with database queries. This controls collection of query parameters, inline literal values within query text, mutation/request bodies, and returned result data. + * Include data associated with database queries. This controls collection of bound query parameters, data payloads for write operations, and returned result data. * * Sanitized or parameterized DB statements (`db.query.text`) are **not** controlled by this property. Structural metadata such as the database system, query summary, operation name, or the table being acted upon is also **always** collected. * @default true diff --git a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts index 7590982280d4..f870f6d8fe66 100644 --- a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts +++ b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts @@ -14,7 +14,7 @@ import { type StartSpanOptions, } from '@sentry/core'; import { flushIfServerless } from '@sentry/core/server'; -import { filterCollectedDbQueryText, getSqlQuerySummary, sanitizeSqlQuery } from '@sentry/server-utils'; +import { getSqlQuerySummary, sanitizeSqlQuery } from '@sentry/server-utils'; import type { Database, PreparedStatement } from 'db0'; import { type DatabaseConnectionConfig, type DatabaseSpanData, getDatabaseSpanData } from './database-span-data'; import { DB_NAMESPACE, DB_QUERY_SUMMARY, DB_QUERY_TEXT, DB_SYSTEM_NAME } from '@sentry/conventions/attributes'; @@ -250,18 +250,19 @@ function createBreadcrumb(query: string): void { * Creates a start span options object. */ function createStartSpanOptions(query: string, data: DatabaseSpanData): StartSpanOptions { - const querySummary = query ? getSqlQuerySummary(sanitizeSqlQuery(query)) : undefined; + const queryText = query ? sanitizeSqlQuery(query) : undefined; + const querySummary = queryText ? getSqlQuerySummary(queryText) : undefined; const client = getClient(); const name = client && hasSpanStreamingEnabled(client) ? querySummary || (data[DB_NAMESPACE] as string | undefined) || DB_SPAN_NAME_FALLBACK - : query; + : (queryText ?? DB_SPAN_NAME_FALLBACK); return { name, attributes: { - [DB_QUERY_TEXT]: filterCollectedDbQueryText(query), + [DB_QUERY_TEXT]: queryText, [DB_QUERY_SUMMARY]: querySummary, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SENTRY_ORIGIN, [SENTRY_OP]: DB_QUERY, diff --git a/packages/server-utils/src/exports.ts b/packages/server-utils/src/exports.ts index 40ff339fddaf..380d302712c9 100644 --- a/packages/server-utils/src/exports.ts +++ b/packages/server-utils/src/exports.ts @@ -4,7 +4,6 @@ export { setAsyncLocalStorageAsyncContextStrategy } from './async-context'; export { openTelemetryIntegration, getOtlpTracesEndpoint } from './opentelemetry'; export * from './ai'; export { getSqlQuerySummary, sanitizeSqlQuery } from './utils/sql'; -export { filterCollectedDbQueryText } from './utils/filterCollectedDbQueryText'; export type { SqlDialect } from './utils/sql'; export { instrumentPostgresJsSql } from './integrations/postgresjs'; export type { PostgresConnectionContext } from './integrations/postgresjs'; diff --git a/packages/server-utils/src/integrations/knex.ts b/packages/server-utils/src/integrations/knex.ts index 9d7f8a39e624..edc023f3118e 100644 --- a/packages/server-utils/src/integrations/knex.ts +++ b/packages/server-utils/src/integrations/knex.ts @@ -34,7 +34,6 @@ import { DEBUG_BUILD } from '../debug-build'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { getSqlQuerySummary, sanitizeSqlQuery } from '../utils/sql'; -import { filterCollectedDbQueryText } from '../utils/filterCollectedDbQueryText'; // NOTE: this uses the same name as the OTel integration by design. `@sentry/node`'s `knexIntegration` // picks this subscriber over the vendored OTel path when orchestrion injection is active. @@ -176,9 +175,11 @@ function subscribeQuery(): void { const dbSystem = mapSystem(client?.driverName); const dialect = client?.driverName === 'mysql' || client?.driverName === 'mysql2' ? 'mysql' : undefined; + // Per OTel, `db.query.text` must not carry inline literal values (e.g. from `knex.raw`), so the + // sanitized form is attached rather than the raw statement. const dbStatement = - query?.sql != null ? filterCollectedDbQueryText(truncate(query.sql, MAX_QUERY_LENGTH), dialect) : undefined; - const querySummary = dbStatement ? getSqlQuerySummary(sanitizeSqlQuery(dbStatement, dialect)) : undefined; + query?.sql != null ? sanitizeSqlQuery(truncate(query.sql, MAX_QUERY_LENGTH), dialect) : undefined; + const querySummary = dbStatement ? getSqlQuerySummary(dbStatement) : undefined; const attributes: SpanAttributes = { [SENTRY_OP]: DB, [SENTRY_KIND]: 'client', diff --git a/packages/server-utils/src/integrations/mysql.ts b/packages/server-utils/src/integrations/mysql.ts index f3236444fdd5..acf8c3887ea0 100644 --- a/packages/server-utils/src/integrations/mysql.ts +++ b/packages/server-utils/src/integrations/mysql.ts @@ -23,7 +23,6 @@ import { startInactiveSpan, } from '@sentry/core'; import { getSqlQuerySummary, sanitizeSqlQuery } from '../utils/sql'; -import { filterCollectedDbQueryText } from '../utils/filterCollectedDbQueryText'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { mysqlModuleNames } from '../orchestrion/config/mysql'; @@ -89,10 +88,12 @@ function instrumentMysql(): void { // handler with the caller's context lost. `deferSpanEnd` replays this scope onto the emitter. data._sentryCallerScope = getCurrentScope(); - const querySummary = sql ? getSqlQuerySummary(sanitizeSqlQuery(sql, 'mysql')) : undefined; + // Per OTel, `db.query.text` must not carry inline literal values, so the sanitized form is + // attached rather than the raw statement. + const queryText = sql ? sanitizeSqlQuery(sql, 'mysql') : undefined; + const querySummary = queryText ? getSqlQuerySummary(queryText) : undefined; const client = getClient(); - const queryText = filterCollectedDbQueryText(sql, 'mysql', client); const name = client && hasSpanStreamingEnabled(client) ? querySummary || database || DB_SYSTEM_NAME_VALUE_MYSQL diff --git a/packages/server-utils/src/integrations/mysql2/index.ts b/packages/server-utils/src/integrations/mysql2/index.ts index e9f388ecbc78..2d9342878049 100644 --- a/packages/server-utils/src/integrations/mysql2/index.ts +++ b/packages/server-utils/src/integrations/mysql2/index.ts @@ -10,7 +10,6 @@ import { waitForTracingChannelBinding, } from '@sentry/core'; import { getSqlQuerySummary, sanitizeSqlQuery } from '../../utils/sql'; -import { filterCollectedDbQueryText } from '../../utils/filterCollectedDbQueryText'; import { subscribeMysql2DiagnosticChannels } from './mysql2-dc-subscriber'; import type { ChannelName } from '../../orchestrion/channels'; import { CHANNELS } from '../../orchestrion/channels'; @@ -85,10 +84,10 @@ function subscribeQueryChannel(channelName: ChannelName): void { data => { const statement = getQueryText(data.arguments); const connectionAttributes = getConnectionAttributes(data.self?.config); - const querySummary = statement ? getSqlQuerySummary(sanitizeSqlQuery(statement, 'mysql')) : undefined; + const queryText = statement ? sanitizeSqlQuery(statement, 'mysql') : undefined; + const querySummary = queryText ? getSqlQuerySummary(queryText) : undefined; const client = getClient(); - const queryText = filterCollectedDbQueryText(statement, 'mysql', client); const name = client && hasSpanStreamingEnabled(client) ? querySummary || (connectionAttributes[DB_NAMESPACE] as string | undefined) || DB_SYSTEM_VALUE_MYSQL diff --git a/packages/server-utils/src/integrations/postgres.ts b/packages/server-utils/src/integrations/postgres.ts index 8d6ea10d2164..8997b1de5e0e 100644 --- a/packages/server-utils/src/integrations/postgres.ts +++ b/packages/server-utils/src/integrations/postgres.ts @@ -23,7 +23,6 @@ import { startInactiveSpan, } from '@sentry/core'; import { getSqlQuerySummary, sanitizeSqlQuery } from '../utils/sql'; -import { filterCollectedDbQueryText } from '../utils/filterCollectedDbQueryText'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { pgModuleNames } from '../orchestrion/config/pg'; @@ -181,11 +180,10 @@ function querySpanOptions(ctx: PgChannelContext): { name: string; attributes: Sp const params = (ctx.self as { connectionParameters?: PgConnectionParams } | undefined)?.connectionParameters ?? {}; const queryConfig = extractQueryConfig(ctx.arguments); const client = getClient(); - // The statement is sanitized before it is summarized, so that a string literal containing - // `from`/`join` can't leak a value into the summary. - const querySummary = queryConfig?.text ? getSqlQuerySummary(sanitizeSqlQuery(queryConfig.text)) : undefined; - - const queryText = filterCollectedDbQueryText(queryConfig?.text, undefined, client); + // Per OTel, `db.query.text` must not carry inline literal values. `$n` placeholders survive the + // sanitizer, so parameterized statements pass through unchanged. + const queryText = queryConfig?.text ? sanitizeSqlQuery(queryConfig.text) : undefined; + const querySummary = queryText ? getSqlQuerySummary(queryText) : undefined; const name = client && hasSpanStreamingEnabled(client) ? querySummary || params.database || DB_SYSTEM_POSTGRESQL diff --git a/packages/server-utils/src/integrations/tedious.ts b/packages/server-utils/src/integrations/tedious.ts index 85a37a22e4bf..6ed5ab70af1d 100644 --- a/packages/server-utils/src/integrations/tedious.ts +++ b/packages/server-utils/src/integrations/tedious.ts @@ -29,7 +29,6 @@ import { CHANNELS } from '../orchestrion/channels'; import { tediousModuleNames } from '../orchestrion/config/tedious'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; import { getSqlQuerySummary, sanitizeSqlQuery } from '../utils/sql'; -import { filterCollectedDbQueryText } from '../utils/filterCollectedDbQueryText'; // NOTE: this uses the same name as the OTel integration by design. When orchestrion injection is active, // `_init` swaps the OTel `Tedious` integration out of the defaults and appends this one (matched by name). @@ -133,7 +132,8 @@ function subscribeQuery(channelName: string, operation: string): void { const databaseName = connection[currentDatabaseSymbol]; const sql = extractSql(request); - const querySummary = sql && operation !== 'callProcedure' ? getSqlQuerySummary(sanitizeSqlQuery(sql)) : undefined; + const queryText = sql ? sanitizeSqlQuery(sql) : undefined; + const querySummary = queryText && operation !== 'callProcedure' ? getSqlQuerySummary(queryText) : undefined; const attributes: SpanAttributes = { [SENTRY_OP]: DB, @@ -143,7 +143,7 @@ function subscribeQuery(channelName: string, operation: string): void { [DB_NAMESPACE]: databaseName, // `>=4` uses the `authentication` object; older versions expose `userName` directly. [DB_USER]: connection.config?.userName ?? connection.config?.authentication?.options?.userName, - [DB_QUERY_TEXT]: filterCollectedDbQueryText(sql), + [DB_QUERY_TEXT]: queryText, [DB_QUERY_SUMMARY]: querySummary, [ATTR_DB_SQL_TABLE]: request.table, [SERVER_ADDRESS]: connection.config?.server, @@ -156,7 +156,7 @@ function subscribeQuery(channelName: string, operation: string): void { name: client && hasSpanStreamingEnabled(client) ? querySummary || getLowCardinalitySecondarySpanName(operation, databaseName, sql, request.table) - : sql || getSecondarySpanName(operation, databaseName, request.table), + : queryText || getSecondarySpanName(operation, databaseName, request.table), attributes, }); diff --git a/packages/server-utils/src/utils/filterCollectedDbQueryText.ts b/packages/server-utils/src/utils/filterCollectedDbQueryText.ts deleted file mode 100644 index 24c6a451d408..000000000000 --- a/packages/server-utils/src/utils/filterCollectedDbQueryText.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Client } from '@sentry/core'; -import { getClient } from '@sentry/core'; -import type { SqlDialect } from './sql'; -import { sanitizeSqlQuery } from './sql'; - -/** - * Applies `dataCollection.databaseQueryData` to a SQL statement the SDK collected itself, for use as - * `db.query.text`. - * - * A statement can carry inline literal values (`WHERE email = 'jane@example.com'`), which the spec - * counts as database query data. Sanitized statements are not gated, so with the option off the - * literals are replaced with `?` rather than the attribute being dropped. The shape of the query - * stays available for debugging. - * - * Pass the `client` the statement belongs to whenever one is at hand; falling back to `getClient()` - * resolves against the current scope, which is the wrong client in a multi-client setup. - */ -export function filterCollectedDbQueryText(query: string, dialect?: SqlDialect, client?: Client): string; -export function filterCollectedDbQueryText( - query: string | undefined, - dialect?: SqlDialect, - client?: Client, -): string | undefined; -export function filterCollectedDbQueryText( - query: string | undefined, - dialect?: SqlDialect, - client?: Client, -): string | undefined { - if (query === undefined) { - return undefined; - } - // Instrumentation can run before a client exists; collecting is the documented default. - const collect = (client ?? getClient())?.getDataCollectionOptions().databaseQueryData !== false; - return collect ? query : sanitizeSqlQuery(query, dialect); -} diff --git a/packages/server-utils/test/utils/filterCollectedDbQueryText.test.ts b/packages/server-utils/test/utils/filterCollectedDbQueryText.test.ts deleted file mode 100644 index eda61b3d5ea3..000000000000 --- a/packages/server-utils/test/utils/filterCollectedDbQueryText.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Client } from '@sentry/core'; -import { describe, expect, it } from 'vitest'; -import { filterCollectedDbQueryText } from '../../src/utils/filterCollectedDbQueryText'; - -const QUERY = "SELECT * FROM users WHERE email = 'jane@example.com'"; - -function mockClient(databaseQueryData: boolean): Client { - return { getDataCollectionOptions: () => ({ databaseQueryData }) } as unknown as Client; -} - -describe('filterCollectedDbQueryText', () => { - it('returns undefined for an absent statement', () => { - expect(filterCollectedDbQueryText(undefined, undefined, mockClient(true))).toBeUndefined(); - }); - - it('keeps inline literals when databaseQueryData is on', () => { - expect(filterCollectedDbQueryText(QUERY, undefined, mockClient(true))).toBe(QUERY); - }); - - it('sanitizes inline literals when databaseQueryData is off', () => { - expect(filterCollectedDbQueryText(QUERY, undefined, mockClient(false))).toBe('SELECT * FROM users WHERE email = ?'); - }); -}); From 816d177f2a3f7a80ad4d03fa28fdb9a0bb77a48c Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:36:43 +0200 Subject: [PATCH 3/8] sanitize breadcrumb --- packages/nuxt/src/runtime/utils/instrumentDatabase.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts index f870f6d8fe66..15c0d05ea50a 100644 --- a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts +++ b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts @@ -237,11 +237,13 @@ function handleSpanStart(fn: () => unknown, breadcrumbOpts?: { query: string }) } function createBreadcrumb(query: string): void { + // The breadcrumb carries the same query text as the span, so it is sanitized the same way. + const queryText = sanitizeSqlQuery(query); addBreadcrumb({ category: 'query', - message: query, + message: queryText, data: { - 'db.query.text': query, + 'db.query.text': queryText, }, }); } From 7b2cd7e8aa42b8c72c05117ca1ce61f7d22e7658 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:24:04 +0200 Subject: [PATCH 4/8] fix tests --- .../cloudflare-integration-tests/suites/prisma/test.ts | 6 +++--- .../suites/orchestrion-mysql/test.ts | 4 ++-- .../suites/orchestrion-mysql2/test.ts | 4 ++-- .../suites/orchestrion-postgres/test.ts | 4 ++-- .../suites/orchestrion-tedious/test.ts | 4 ++-- .../test-applications/nuxt-3/tests/database.test.ts | 6 +++--- .../nuxt-4-static/tests/database.test.ts | 8 ++++---- .../test-applications/nuxt-4/tests/database.test.ts | 6 +++--- .../test-applications/nuxt-5/tests/database.test.ts | 6 +++--- .../suites/tracing/postgres-streamed/test.ts | 4 ++-- 10 files changed, 26 insertions(+), 26 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/suites/prisma/test.ts b/dev-packages/cloudflare-integration-tests/suites/prisma/test.ts index 586792d669c5..6cbba42facb4 100644 --- a/dev-packages/cloudflare-integration-tests/suites/prisma/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/prisma/test.ts @@ -55,9 +55,9 @@ it('captures a transaction with Prisma spans for a D1 query via the @sentry/clou origin: 'auto.db.cloudflare.d1', }, { - description: expect.stringMatching( - /^SELECT `main`\.`User`\.`id`, `main`\.`User`\.`email`, `main`\.`User`\.`name` FROM `main`\.`User` WHERE 1=1 LIMIT \? OFFSET \? \/\* traceparent='00-[\da-f]{32}-[\da-f]{16}-01' \*\/$/, - ), + // The sanitizer strips the D1 adapter's traceparent comment and replaces the literals. + description: + 'SELECT `main`.`User`.`id`, `main`.`User`.`email`, `main`.`User`.`name` FROM `main`.`User` WHERE ?=? LIMIT ? OFFSET ?', op: 'db.query', origin: 'auto.db.cloudflare.d1', }, diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-mysql/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-mysql/test.ts index c21638cf667a..dd9c526ac32e 100644 --- a/dev-packages/deno-integration-tests/suites/orchestrion-mysql/test.ts +++ b/dev-packages/deno-integration-tests/suites/orchestrion-mysql/test.ts @@ -89,9 +89,9 @@ Deno.test('mysql instrumentation: orchestrion:mysql:query channel produces a nes const mysqlSpan = parent.spans?.find(s => s.op === 'db'); assertExists(mysqlSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`); - assertEquals(mysqlSpan!.description, 'SELECT 1 AS solution'); + assertEquals(mysqlSpan!.description, 'SELECT ? AS solution'); assertEquals(mysqlSpan!.data?.['db.system.name'], 'mysql'); - assertEquals(mysqlSpan!.data?.['db.query.text'], 'SELECT 1 AS solution'); + assertEquals(mysqlSpan!.data?.['db.query.text'], 'SELECT ? AS solution'); assertEquals(mysqlSpan!.data?.['server.address'], '127.0.0.1'); assertEquals(mysqlSpan!.data?.['server.port'], 3306); assertEquals(mysqlSpan!.data?.['db.user'], 'root'); diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-mysql2/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-mysql2/test.ts index afafeb6ea7f8..702baa6f175f 100644 --- a/dev-packages/deno-integration-tests/suites/orchestrion-mysql2/test.ts +++ b/dev-packages/deno-integration-tests/suites/orchestrion-mysql2/test.ts @@ -50,9 +50,9 @@ Deno.test('mysql2 instrumentation: orchestrion:mysql2:query channel produces a n const mysqlSpan = parent.spans?.find(s => s.op === 'db'); assertExists(mysqlSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`); - assertEquals(mysqlSpan!.description, 'SELECT 1 AS solution'); + assertEquals(mysqlSpan!.description, 'SELECT ? AS solution'); assertEquals(mysqlSpan!.data?.['db.system.name'], 'mysql'); - assertEquals(mysqlSpan!.data?.['db.query.text'], 'SELECT 1 AS solution'); + assertEquals(mysqlSpan!.data?.['db.query.text'], 'SELECT ? AS solution'); assertEquals(mysqlSpan!.data?.['db.namespace'], 'mydb'); assertEquals(mysqlSpan!.data?.['db.user'], 'root'); assertEquals(mysqlSpan!.data?.['server.address'], '127.0.0.1'); diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-postgres/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-postgres/test.ts index 2e7ca65e4d69..2880f7c490dd 100644 --- a/dev-packages/deno-integration-tests/suites/orchestrion-postgres/test.ts +++ b/dev-packages/deno-integration-tests/suites/orchestrion-postgres/test.ts @@ -89,9 +89,9 @@ Deno.test('pg instrumentation: orchestrion:pg:query channel produces a nested db const pgSpan = parent.spans?.find(s => s.op === 'db'); assertExists(pgSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`); - assertEquals(pgSpan!.description, 'SELECT 1 AS solution'); + assertEquals(pgSpan!.description, 'SELECT ? AS solution'); assertEquals(pgSpan!.data?.['db.system.name'], 'postgresql'); - assertEquals(pgSpan!.data?.['db.query.text'], 'SELECT 1 AS solution'); + assertEquals(pgSpan!.data?.['db.query.text'], 'SELECT ? AS solution'); assertEquals(pgSpan!.data?.['server.address'], '127.0.0.1'); assertEquals(pgSpan!.data?.['server.port'], 5432); assertEquals(pgSpan!.data?.['db.user'], 'root'); diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-tedious/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-tedious/test.ts index 9cb6c45468bc..4bb448797cad 100644 --- a/dev-packages/deno-integration-tests/suites/orchestrion-tedious/test.ts +++ b/dev-packages/deno-integration-tests/suites/orchestrion-tedious/test.ts @@ -53,11 +53,11 @@ Deno.test('tedious instrumentation: orchestrion:tedious:execSql channel produces const tediousSpan = parent.spans?.find(s => s.op === 'db'); assertExists(tediousSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`); - assertEquals(tediousSpan!.description, 'SELECT 1'); + assertEquals(tediousSpan!.description, 'SELECT ?'); assertEquals(tediousSpan!.data?.['db.system.name'], 'mssql'); assertEquals(tediousSpan!.data?.['db.namespace'], 'mydb'); assertEquals(tediousSpan!.data?.['db.user'], 'sa'); - assertEquals(tediousSpan!.data?.['db.query.text'], 'SELECT 1'); + assertEquals(tediousSpan!.data?.['db.query.text'], 'SELECT ?'); assertEquals(tediousSpan!.data?.['server.address'], '127.0.0.1'); assertEquals(tediousSpan!.data?.['server.port'], 1433); assertEquals(tediousSpan!.data?.['sentry.origin'], 'auto.db.tedious'); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/database.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/database.test.ts index 5c58b6c1a165..c5810a56cbe8 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/database.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/database.test.ts @@ -109,7 +109,7 @@ test.describe('database integration', () => { expect(insertSpan).toBeDefined(); expect(insertSpan?.attributes).toMatchObject({ 'db.query.summary': { type: 'string', value: 'INSERT logs' }, - 'db.query.text': { type: 'string', value: `INSERT INTO logs (message, level) VALUES ('Test log', 'INFO')` }, + 'db.query.text': { type: 'string', value: `INSERT INTO logs (message, level) VALUES (?, ?)` }, 'db.system.name': { type: 'string', value: 'sqlite' }, 'sentry.origin': { type: 'string', value: 'auto.db.nuxt' }, }); @@ -178,8 +178,8 @@ test.describe('database integration', () => { ); expect(dbBreadcrumb).toBeDefined(); - expect(dbBreadcrumb?.message).toBe(`INSERT INTO logs (message, level) VALUES ('Test log', 'INFO')`); - expect(dbBreadcrumb?.data?.['db.query.text']).toBe(`INSERT INTO logs (message, level) VALUES ('Test log', 'INFO')`); + expect(dbBreadcrumb?.message).toBe(`INSERT INTO logs (message, level) VALUES (?, ?)`); + expect(dbBreadcrumb?.data?.['db.query.text']).toBe(`INSERT INTO logs (message, level) VALUES (?, ?)`); }); test('multiple database operations in single request create multiple spans', async ({ request }) => { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/database.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/database.test.ts index a046d2924bef..0c1878c65c3e 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/database.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/database.test.ts @@ -120,9 +120,9 @@ test.describe('database integration', () => { expect(dbSpan).toBeDefined(); expect(dbSpan?.op).toBe('db.query'); - expect(dbSpan?.description).toBe(`INSERT INTO logs (message, level) VALUES ('Test log', 'INFO')`); + expect(dbSpan?.description).toBe(`INSERT INTO logs (message, level) VALUES (?, ?)`); expect(dbSpan?.data?.['db.system.name']).toBe('sqlite'); - expect(dbSpan?.data?.['db.query.text']).toBe(`INSERT INTO logs (message, level) VALUES ('Test log', 'INFO')`); + expect(dbSpan?.data?.['db.query.text']).toBe(`INSERT INTO logs (message, level) VALUES (?, ?)`); expect(dbSpan?.data?.['sentry.origin']).toBe('auto.db.nuxt'); }); @@ -180,8 +180,8 @@ test.describe('database integration', () => { expect(dbBreadcrumb).toBeDefined(); expect(dbBreadcrumb?.category).toBe('query'); - expect(dbBreadcrumb?.message).toBe(`INSERT INTO logs (message, level) VALUES ('Test log', 'INFO')`); - expect(dbBreadcrumb?.data?.['db.query.text']).toBe(`INSERT INTO logs (message, level) VALUES ('Test log', 'INFO')`); + expect(dbBreadcrumb?.message).toBe(`INSERT INTO logs (message, level) VALUES (?, ?)`); + expect(dbBreadcrumb?.data?.['db.query.text']).toBe(`INSERT INTO logs (message, level) VALUES (?, ?)`); }); test('multiple database operations in single request create multiple spans', async ({ request }) => { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database.test.ts index 7f0b60244f62..b73c4ee990f4 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database.test.ts @@ -109,7 +109,7 @@ test.describe('database integration', () => { expect(insertSpan).toBeDefined(); expect(insertSpan?.attributes).toMatchObject({ 'db.query.summary': { type: 'string', value: 'INSERT logs' }, - 'db.query.text': { type: 'string', value: `INSERT INTO logs (message, level) VALUES ('Test log', 'INFO')` }, + 'db.query.text': { type: 'string', value: `INSERT INTO logs (message, level) VALUES (?, ?)` }, 'db.system.name': { type: 'string', value: 'sqlite' }, 'sentry.origin': { type: 'string', value: 'auto.db.nuxt' }, }); @@ -178,8 +178,8 @@ test.describe('database integration', () => { ); expect(dbBreadcrumb).toBeDefined(); - expect(dbBreadcrumb?.message).toBe(`INSERT INTO logs (message, level) VALUES ('Test log', 'INFO')`); - expect(dbBreadcrumb?.data?.['db.query.text']).toBe(`INSERT INTO logs (message, level) VALUES ('Test log', 'INFO')`); + expect(dbBreadcrumb?.message).toBe(`INSERT INTO logs (message, level) VALUES (?, ?)`); + expect(dbBreadcrumb?.data?.['db.query.text']).toBe(`INSERT INTO logs (message, level) VALUES (?, ?)`); }); test('multiple database operations in single request create multiple spans', async ({ request }) => { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/database.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/database.test.ts index 2a1b16f23d5b..de55b7ce8df4 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/database.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/database.test.ts @@ -109,7 +109,7 @@ test.describe('database integration', () => { expect(insertSpan).toBeDefined(); expect(insertSpan?.attributes).toMatchObject({ 'db.query.summary': { type: 'string', value: 'INSERT logs' }, - 'db.query.text': { type: 'string', value: `INSERT INTO logs (message, level) VALUES ('Test log', 'INFO')` }, + 'db.query.text': { type: 'string', value: `INSERT INTO logs (message, level) VALUES (?, ?)` }, 'db.system.name': { type: 'string', value: 'sqlite' }, 'sentry.origin': { type: 'string', value: 'auto.db.nuxt' }, }); @@ -178,8 +178,8 @@ test.describe('database integration', () => { ); expect(dbBreadcrumb).toBeDefined(); - expect(dbBreadcrumb?.message).toBe(`INSERT INTO logs (message, level) VALUES ('Test log', 'INFO')`); - expect(dbBreadcrumb?.data?.['db.query.text']).toBe(`INSERT INTO logs (message, level) VALUES ('Test log', 'INFO')`); + expect(dbBreadcrumb?.message).toBe(`INSERT INTO logs (message, level) VALUES (?, ?)`); + expect(dbBreadcrumb?.data?.['db.query.text']).toBe(`INSERT INTO logs (message, level) VALUES (?, ?)`); }); test('multiple database operations in single request create multiple spans', async ({ request }) => { diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts index 2753ef1708b7..03a3fe70fd1e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts @@ -147,10 +147,10 @@ function expectedDbSpan({ } const CREATE_USER_TABLE_STATEMENT = - '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"));'; + '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"))'; const CREATE_NATIVE_USER_TABLE_STATEMENT = - 'CREATE TABLE "NativeUser" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"));'; + 'CREATE TABLE "NativeUser" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))'; function getDbSpans(container: SerializedStreamedSpanContainer): SerializedStreamedSpanContainer['items'] { return container.items.filter(item => item.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'db'); From d507c4306f5a78730aed4b3865025e4436e4cf98 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 9 Sep 2026 11:12:52 +0200 Subject: [PATCH 5/8] fix(cloudflare): Sanitize batched D1 statements and guard empty queries Co-Authored-By: Claude Fable 5 --- .../src/instrumentations/worker/instrumentD1.ts | 11 +++++++---- .../test/instrumentations/worker/instrumentD1.test.ts | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts b/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts index 147b55c573ff..9136923d9040 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts @@ -123,7 +123,7 @@ function createD1Breadcrumb(query: string, type: D1QueryType, d1Result?: D1Respo addBreadcrumb({ category: 'query', // The breadcrumb carries the same query text as the span, so it is sanitized the same way. - message: sanitizeSqlQuery(query), + message: query ? sanitizeSqlQuery(query) : undefined, data: { ...(d1Result ? getAttributesFromD1Response(d1Result) : {}), 'db.operation.name': type, @@ -132,11 +132,13 @@ function createD1Breadcrumb(query: string, type: D1QueryType, d1Result?: D1Respo } function createStartSpanOptions(query: string, type: D1QueryType): StartSpanOptions { - const queryText = sanitizeSqlQuery(query); - const querySummary = query ? getSqlQuerySummary(queryText) : undefined; + // Guarded so an empty query omits the attribute instead of reporting the sanitizer's fallback text. + const queryText = query ? sanitizeSqlQuery(query) : undefined; + const querySummary = queryText ? getSqlQuerySummary(queryText) : undefined; const client = getClient(); - const name = client && hasSpanStreamingEnabled(client) ? querySummary || 'cloudflare-d1' : queryText; + const name = + client && hasSpanStreamingEnabled(client) ? querySummary || 'cloudflare-d1' : (queryText ?? 'cloudflare-d1'); return { name, @@ -173,6 +175,7 @@ function instrumentBatch( const queryText = statements .map(statement => (statement as unknown as { statement?: string }).statement ?? '') .filter(Boolean) + .map(statement => sanitizeSqlQuery(statement)) .join('\n'); return startSpan( diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts index b59f0fc5918a..7c6a6478853e 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentD1.test.ts @@ -339,7 +339,7 @@ describe('instrumentD1', () => { 'sentry.op': 'db.query', 'db.system.name': 'cloudflare-d1', 'db.operation.name': 'batch', - 'db.query.text': 'SELECT 1\nSELECT 2', + 'db.query.text': 'SELECT ?\nSELECT ?', 'db.operation.batch.size': 2, 'sentry.origin': 'auto.db.cloudflare.d1', }, @@ -482,7 +482,7 @@ describe('instrumentD1', () => { 'sentry.op': 'db.query', 'db.system.name': 'cloudflare-d1', 'db.operation.name': 'batch', - 'db.query.text': 'SELECT 1\nSELECT 2', + 'db.query.text': 'SELECT ?\nSELECT ?', 'db.operation.batch.size': 2, 'sentry.origin': 'auto.db.cloudflare.d1', }, From a8aa09b9af088a1c8fbfb75e0d7223770ac49129 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 9 Sep 2026 11:32:16 +0200 Subject: [PATCH 6/8] fix(nuxt): Guard the DB breadcrumb against an empty query Co-Authored-By: Claude Fable 5 --- packages/nuxt/src/runtime/utils/instrumentDatabase.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts index 15c0d05ea50a..71457ec0f003 100644 --- a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts +++ b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts @@ -237,8 +237,8 @@ function handleSpanStart(fn: () => unknown, breadcrumbOpts?: { query: string }) } function createBreadcrumb(query: string): void { - // The breadcrumb carries the same query text as the span, so it is sanitized the same way. - const queryText = sanitizeSqlQuery(query); + // The breadcrumb carries the same query text as the span, so it is sanitized and guarded the same way. + const queryText = query ? sanitizeSqlQuery(query) : undefined; addBreadcrumb({ category: 'query', message: queryText, From add052ce5b07a4a18fbe8d85ab7405d91d46d04d Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 9 Sep 2026 11:48:10 +0200 Subject: [PATCH 7/8] fix sql dialect in db0 --- .../src/runtime/utils/instrumentDatabase.ts | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts index 71457ec0f003..02777b221727 100644 --- a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts +++ b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts @@ -14,7 +14,7 @@ import { type StartSpanOptions, } from '@sentry/core'; import { flushIfServerless } from '@sentry/core/server'; -import { getSqlQuerySummary, sanitizeSqlQuery } from '@sentry/server-utils'; +import { getSqlQuerySummary, sanitizeSqlQuery, type SqlDialect } from '@sentry/server-utils'; import type { Database, PreparedStatement } from 'db0'; import { type DatabaseConnectionConfig, type DatabaseSpanData, getDatabaseSpanData } from './database-span-data'; import { DB_NAMESPACE, DB_QUERY_SUMMARY, DB_QUERY_TEXT, DB_SYSTEM_NAME } from '@sentry/conventions/attributes'; @@ -107,7 +107,7 @@ function instrumentDatabase(db: MaybeInstrumentedDatabase, config?: DatabaseConn apply(target, thisArg, args: Parameters) { return startSpan( createStartSpanOptions(args[0], metadata), - handleSpanStart(() => target.apply(thisArg, args), { query: args[0] }), + handleSpanStart(() => target.apply(thisArg, args), { query: args[0], data: metadata }), ); }, }); @@ -177,7 +177,7 @@ function instrumentPreparedStatementQueries( apply(target, thisArg, args: Parameters) { return startSpan( createStartSpanOptions(query, data), - handleSpanStart(() => target.apply(thisArg, args), { query }), + handleSpanStart(() => target.apply(thisArg, args), { query, data }), ); }, }); @@ -187,7 +187,7 @@ function instrumentPreparedStatementQueries( apply(target, thisArg, args: Parameters) { return startSpan( createStartSpanOptions(query, data), - handleSpanStart(() => target.apply(thisArg, args), { query }), + handleSpanStart(() => target.apply(thisArg, args), { query, data }), ); }, }); @@ -197,7 +197,7 @@ function instrumentPreparedStatementQueries( apply(target, thisArg, args: Parameters) { return startSpan( createStartSpanOptions(query, data), - handleSpanStart(() => target.apply(thisArg, args), { query }), + handleSpanStart(() => target.apply(thisArg, args), { query, data }), ); }, }); @@ -210,12 +210,12 @@ function instrumentPreparedStatementQueries( /** * Creates a span start callback handler. */ -function handleSpanStart(fn: () => unknown, breadcrumbOpts?: { query: string }) { +function handleSpanStart(fn: () => unknown, breadcrumbOpts?: { query: string; data: DatabaseSpanData }) { return async (span: Span) => { try { const result = await fn(); if (breadcrumbOpts) { - createBreadcrumb(breadcrumbOpts.query); + createBreadcrumb(breadcrumbOpts.query, breadcrumbOpts.data); } return result; @@ -236,9 +236,9 @@ function handleSpanStart(fn: () => unknown, breadcrumbOpts?: { query: string }) }; } -function createBreadcrumb(query: string): void { +function createBreadcrumb(query: string, data: DatabaseSpanData): void { // The breadcrumb carries the same query text as the span, so it is sanitized and guarded the same way. - const queryText = query ? sanitizeSqlQuery(query) : undefined; + const queryText = query ? sanitizeSqlQuery(query, getSqlDialect(data)) : undefined; addBreadcrumb({ category: 'query', message: queryText, @@ -248,11 +248,20 @@ function createBreadcrumb(query: string): void { }); } +/** + * db0's mysql2 connector reports `mysql2` and the dialect field plain `mysql`; both quote strings + * the MySQL way, so the sanitizer needs the `mysql` dialect to strip their literals. + */ +function getSqlDialect(data: DatabaseSpanData): SqlDialect { + const system = data[DB_SYSTEM_NAME]; + return typeof system === 'string' && system.startsWith('mysql') ? 'mysql' : 'standard'; +} + /** * Creates a start span options object. */ function createStartSpanOptions(query: string, data: DatabaseSpanData): StartSpanOptions { - const queryText = query ? sanitizeSqlQuery(query) : undefined; + const queryText = query ? sanitizeSqlQuery(query, getSqlDialect(data)) : undefined; const querySummary = queryText ? getSqlQuerySummary(queryText) : undefined; const client = getClient(); From b63f3bfe5ead3b9e343cb08ac1f7e84a4673a07f Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 9 Sep 2026 14:03:13 +0200 Subject: [PATCH 8/8] fix sanitation --- .../instrumentations/worker/instrumentD1.ts | 33 +++++------ .../instrumentations/instrumentEnv.test.ts | 1 - .../src/runtime/utils/instrumentDatabase.ts | 55 ++++++++++--------- .../server-utils/src/integrations/knex.ts | 5 +- .../server-utils/src/integrations/mysql.ts | 2 - .../server-utils/src/integrations/postgres.ts | 2 - 6 files changed, 46 insertions(+), 52 deletions(-) diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts b/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts index 9136923d9040..d8410ce32a45 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts @@ -27,12 +27,14 @@ function instrumentD1PreparedStatementQueries(statement: D1PreparedStatement, qu return statement; } + const queryText = query ? sanitizeSqlQuery(query) : undefined; + // eslint-disable-next-line @typescript-eslint/unbound-method statement.first = new Proxy(statement.first, { apply(target, thisArg, args: Parameters) { - return startSpan(createStartSpanOptions(query, 'first'), async () => { + return startSpan(createStartSpanOptions(queryText, 'first'), async () => { const res = await Reflect.apply(target, thisArg, args); - createD1Breadcrumb(query, 'first'); + createD1Breadcrumb(queryText, 'first'); return res; }); }, @@ -41,10 +43,10 @@ function instrumentD1PreparedStatementQueries(statement: D1PreparedStatement, qu // eslint-disable-next-line @typescript-eslint/unbound-method statement.run = new Proxy(statement.run, { apply(target, thisArg, args: Parameters) { - return startSpan(createStartSpanOptions(query, 'run'), async span => { + return startSpan(createStartSpanOptions(queryText, 'run'), async span => { const d1Response = await Reflect.apply(target, thisArg, args); applyD1ReturnObjectToSpan(span, d1Response); - createD1Breadcrumb(query, 'run', d1Response); + createD1Breadcrumb(queryText, 'run', d1Response); return d1Response; }); }, @@ -53,10 +55,10 @@ function instrumentD1PreparedStatementQueries(statement: D1PreparedStatement, qu // eslint-disable-next-line @typescript-eslint/unbound-method statement.all = new Proxy(statement.all, { apply(target, thisArg, args: Parameters) { - return startSpan(createStartSpanOptions(query, 'all'), async span => { + return startSpan(createStartSpanOptions(queryText, 'all'), async span => { const d1Result = await Reflect.apply(target, thisArg, args); applyD1ReturnObjectToSpan(span, d1Result); - createD1Breadcrumb(query, 'all', d1Result); + createD1Breadcrumb(queryText, 'all', d1Result); return d1Result; }); }, @@ -65,9 +67,9 @@ function instrumentD1PreparedStatementQueries(statement: D1PreparedStatement, qu // eslint-disable-next-line @typescript-eslint/unbound-method statement.raw = new Proxy(statement.raw, { apply(target, thisArg, args: Parameters) { - return startSpan(createStartSpanOptions(query, 'raw'), async () => { + return startSpan(createStartSpanOptions(queryText, 'raw'), async () => { const res = await Reflect.apply(target, thisArg, args); - createD1Breadcrumb(query, 'raw'); + createD1Breadcrumb(queryText, 'raw'); return res; }); }, @@ -119,11 +121,11 @@ function getAttributesFromD1Response(d1Result: D1Response): SpanAttributes { type D1QueryType = 'first' | 'run' | 'all' | 'raw' | 'batch' | 'exec'; -function createD1Breadcrumb(query: string, type: D1QueryType, d1Result?: D1Response): void { +/** The message is the span's query text (already sanitized) or a static label such as `D1 batch`. */ +function createD1Breadcrumb(message: string | undefined, type: D1QueryType, d1Result?: D1Response): void { addBreadcrumb({ category: 'query', - // The breadcrumb carries the same query text as the span, so it is sanitized the same way. - message: query ? sanitizeSqlQuery(query) : undefined, + message, data: { ...(d1Result ? getAttributesFromD1Response(d1Result) : {}), 'db.operation.name': type, @@ -131,9 +133,7 @@ function createD1Breadcrumb(query: string, type: D1QueryType, d1Result?: D1Respo }); } -function createStartSpanOptions(query: string, type: D1QueryType): StartSpanOptions { - // Guarded so an empty query omits the attribute instead of reporting the sanitizer's fallback text. - const queryText = query ? sanitizeSqlQuery(query) : undefined; +function createStartSpanOptions(queryText: string | undefined, type: D1QueryType): StartSpanOptions { const querySummary = queryText ? getSqlQuerySummary(queryText) : undefined; const client = getClient(); @@ -213,9 +213,10 @@ function _instrumentD1(db: D1Database): D1Database { db.exec = new Proxy(db.exec, { apply(target, thisArg, args: Parameters) { const [query] = args; - return startSpan(createStartSpanOptions(query, 'exec'), async () => { + const queryText = query ? sanitizeSqlQuery(query) : undefined; + return startSpan(createStartSpanOptions(queryText, 'exec'), async () => { const res = await Reflect.apply(target, thisArg, args); - createD1Breadcrumb(query, 'exec'); + createD1Breadcrumb(queryText, 'exec'); return res; }); }, diff --git a/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts b/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts index 3e1f366e37e1..c23f9a2d607e 100644 --- a/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts +++ b/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts @@ -40,7 +40,6 @@ describe('instrumentEnv', () => { await db.prepare('SELECT 1').first(); expect(startSpanSpy).toHaveBeenCalledWith( - // The numeric literal is sanitized out of the query text. expect.objectContaining({ name: 'SELECT ?', attributes: expect.objectContaining({ 'sentry.op': 'db.query' }) }), expect.any(Function), ); diff --git a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts index 02777b221727..9700f4bf3e35 100644 --- a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts +++ b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts @@ -79,11 +79,15 @@ function instrumentDatabase(db: MaybeInstrumentedDatabase, config?: DatabaseConn ...getDatabaseSpanData(config), }; + // The connector name (e.g. `planetscale`) does not identify the SQL flavour, `db.dialect` does. + // MySQL reads `"..."` and `\'` as string syntax, so the sanitizer has to know about it. + const dialect: SqlDialect | undefined = db.dialect === 'mysql' ? 'mysql' : undefined; + db.prepare = new Proxy(db.prepare, { apply(target, thisArg, args: Parameters) { const [query] = args; - return instrumentPreparedStatement(target.apply(thisArg, args), query, metadata); + return instrumentPreparedStatement(target.apply(thisArg, args), query, metadata, dialect); }, }); @@ -94,7 +98,7 @@ function instrumentDatabase(db: MaybeInstrumentedDatabase, config?: DatabaseConn apply(target, thisArg, args: Parameters) { const [strings, ...values] = args; const query = strings ? buildSqlTemplateQuery(strings, values) : ''; - const opts = createStartSpanOptions(query, metadata); + const opts = createStartSpanOptions(query, metadata, dialect); return startSpan( opts, @@ -106,8 +110,8 @@ function instrumentDatabase(db: MaybeInstrumentedDatabase, config?: DatabaseConn db.exec = new Proxy(db.exec, { apply(target, thisArg, args: Parameters) { return startSpan( - createStartSpanOptions(args[0], metadata), - handleSpanStart(() => target.apply(thisArg, args), { query: args[0], data: metadata }), + createStartSpanOptions(args[0], metadata, dialect), + handleSpanStart(() => target.apply(thisArg, args), { query: args[0], dialect }), ); }, }); @@ -148,16 +152,17 @@ function instrumentPreparedStatement( statement: PreparedStatement, query: string, data: DatabaseSpanData, + dialect: SqlDialect | undefined, ): PreparedStatement { // statement.bind() returns a new instance of D1PreparedStatement, so we have to patch it as well. // eslint-disable-next-line @typescript-eslint/unbound-method statement.bind = new Proxy(statement.bind, { apply(target, thisArg, args: Parameters) { - return instrumentPreparedStatementQueries(target.apply(thisArg, args), query, data); + return instrumentPreparedStatementQueries(target.apply(thisArg, args), query, data, dialect); }, }); - return instrumentPreparedStatementQueries(statement, query, data); + return instrumentPreparedStatementQueries(statement, query, data, dialect); } /** @@ -167,6 +172,7 @@ function instrumentPreparedStatementQueries( statement: PreparedStatement, query: string, data: DatabaseSpanData, + dialect: SqlDialect | undefined, ): PreparedStatement { if (patchedStatement.has(statement)) { return statement; @@ -176,8 +182,8 @@ function instrumentPreparedStatementQueries( statement.get = new Proxy(statement.get, { apply(target, thisArg, args: Parameters) { return startSpan( - createStartSpanOptions(query, data), - handleSpanStart(() => target.apply(thisArg, args), { query, data }), + createStartSpanOptions(query, data, dialect), + handleSpanStart(() => target.apply(thisArg, args), { query, dialect }), ); }, }); @@ -186,8 +192,8 @@ function instrumentPreparedStatementQueries( statement.run = new Proxy(statement.run, { apply(target, thisArg, args: Parameters) { return startSpan( - createStartSpanOptions(query, data), - handleSpanStart(() => target.apply(thisArg, args), { query, data }), + createStartSpanOptions(query, data, dialect), + handleSpanStart(() => target.apply(thisArg, args), { query, dialect }), ); }, }); @@ -196,8 +202,8 @@ function instrumentPreparedStatementQueries( statement.all = new Proxy(statement.all, { apply(target, thisArg, args: Parameters) { return startSpan( - createStartSpanOptions(query, data), - handleSpanStart(() => target.apply(thisArg, args), { query, data }), + createStartSpanOptions(query, data, dialect), + handleSpanStart(() => target.apply(thisArg, args), { query, dialect }), ); }, }); @@ -210,12 +216,12 @@ function instrumentPreparedStatementQueries( /** * Creates a span start callback handler. */ -function handleSpanStart(fn: () => unknown, breadcrumbOpts?: { query: string; data: DatabaseSpanData }) { +function handleSpanStart(fn: () => unknown, breadcrumbOpts?: { query: string; dialect: SqlDialect | undefined }) { return async (span: Span) => { try { const result = await fn(); if (breadcrumbOpts) { - createBreadcrumb(breadcrumbOpts.query, breadcrumbOpts.data); + createBreadcrumb(breadcrumbOpts.query, breadcrumbOpts.dialect); } return result; @@ -236,9 +242,9 @@ function handleSpanStart(fn: () => unknown, breadcrumbOpts?: { query: string; da }; } -function createBreadcrumb(query: string, data: DatabaseSpanData): void { +function createBreadcrumb(query: string, dialect: SqlDialect | undefined): void { // The breadcrumb carries the same query text as the span, so it is sanitized and guarded the same way. - const queryText = query ? sanitizeSqlQuery(query, getSqlDialect(data)) : undefined; + const queryText = query ? sanitizeSqlQuery(query, dialect) : undefined; addBreadcrumb({ category: 'query', message: queryText, @@ -248,20 +254,15 @@ function createBreadcrumb(query: string, data: DatabaseSpanData): void { }); } -/** - * db0's mysql2 connector reports `mysql2` and the dialect field plain `mysql`; both quote strings - * the MySQL way, so the sanitizer needs the `mysql` dialect to strip their literals. - */ -function getSqlDialect(data: DatabaseSpanData): SqlDialect { - const system = data[DB_SYSTEM_NAME]; - return typeof system === 'string' && system.startsWith('mysql') ? 'mysql' : 'standard'; -} - /** * Creates a start span options object. */ -function createStartSpanOptions(query: string, data: DatabaseSpanData): StartSpanOptions { - const queryText = query ? sanitizeSqlQuery(query, getSqlDialect(data)) : undefined; +function createStartSpanOptions( + query: string, + data: DatabaseSpanData, + dialect: SqlDialect | undefined, +): StartSpanOptions { + const queryText = query ? sanitizeSqlQuery(query, dialect) : undefined; const querySummary = queryText ? getSqlQuerySummary(queryText) : undefined; const client = getClient(); diff --git a/packages/server-utils/src/integrations/knex.ts b/packages/server-utils/src/integrations/knex.ts index edc023f3118e..fc9e9d162a84 100644 --- a/packages/server-utils/src/integrations/knex.ts +++ b/packages/server-utils/src/integrations/knex.ts @@ -175,10 +175,7 @@ function subscribeQuery(): void { const dbSystem = mapSystem(client?.driverName); const dialect = client?.driverName === 'mysql' || client?.driverName === 'mysql2' ? 'mysql' : undefined; - // Per OTel, `db.query.text` must not carry inline literal values (e.g. from `knex.raw`), so the - // sanitized form is attached rather than the raw statement. - const dbStatement = - query?.sql != null ? sanitizeSqlQuery(truncate(query.sql, MAX_QUERY_LENGTH), dialect) : undefined; + const dbStatement = query?.sql ? sanitizeSqlQuery(truncate(query.sql, MAX_QUERY_LENGTH), dialect) : undefined; const querySummary = dbStatement ? getSqlQuerySummary(dbStatement) : undefined; const attributes: SpanAttributes = { [SENTRY_OP]: DB, diff --git a/packages/server-utils/src/integrations/mysql.ts b/packages/server-utils/src/integrations/mysql.ts index acf8c3887ea0..ba77251bd7c4 100644 --- a/packages/server-utils/src/integrations/mysql.ts +++ b/packages/server-utils/src/integrations/mysql.ts @@ -88,8 +88,6 @@ function instrumentMysql(): void { // handler with the caller's context lost. `deferSpanEnd` replays this scope onto the emitter. data._sentryCallerScope = getCurrentScope(); - // Per OTel, `db.query.text` must not carry inline literal values, so the sanitized form is - // attached rather than the raw statement. const queryText = sql ? sanitizeSqlQuery(sql, 'mysql') : undefined; const querySummary = queryText ? getSqlQuerySummary(queryText) : undefined; diff --git a/packages/server-utils/src/integrations/postgres.ts b/packages/server-utils/src/integrations/postgres.ts index 8997b1de5e0e..57b3350e454c 100644 --- a/packages/server-utils/src/integrations/postgres.ts +++ b/packages/server-utils/src/integrations/postgres.ts @@ -180,8 +180,6 @@ function querySpanOptions(ctx: PgChannelContext): { name: string; attributes: Sp const params = (ctx.self as { connectionParameters?: PgConnectionParams } | undefined)?.connectionParameters ?? {}; const queryConfig = extractQueryConfig(ctx.arguments); const client = getClient(); - // Per OTel, `db.query.text` must not carry inline literal values. `$n` placeholders survive the - // sanitizer, so parameterized statements pass through unchanged. const queryText = queryConfig?.text ? sanitizeSqlQuery(queryConfig.text) : undefined; const querySummary = queryText ? getSqlQuerySummary(queryText) : undefined; const name =