diff --git a/packages/server-utils/src/integrations/knex.ts b/packages/server-utils/src/integrations/knex.ts index 2928f66bd339..a3f2c236cba9 100644 --- a/packages/server-utils/src/integrations/knex.ts +++ b/packages/server-utils/src/integrations/knex.ts @@ -5,9 +5,14 @@ import * as diagnosticsChannel from 'node:diagnostics_channel'; import type { IntegrationFn, Span, SpanAttributes } from '@sentry/core'; import { + _INTERNAL_getSqlQuerySummary, + _INTERNAL_sanitizeSqlQuery, + DB_SPAN_NAME_FALLBACK, debug, defineIntegration, getActiveSpan, + getClient, + hasSpanStreamingEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, @@ -17,6 +22,7 @@ import { import { DB_NAMESPACE, DB_OPERATION_NAME, + DB_QUERY_SUMMARY, DB_QUERY_TEXT, DB_SYSTEM_NAME, DB_USER, @@ -167,6 +173,11 @@ function subscribeQuery(): void { connection?.filename || connection?.database || extractDatabaseFromConnectionString(connectionString); const dbStatement = query?.sql != null ? truncate(query.sql, MAX_QUERY_LENGTH) : undefined; + // 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 = dbStatement + ? _INTERNAL_getSqlQuerySummary(_INTERNAL_sanitizeSqlQuery(dbStatement)) + : undefined; const attributes: SpanAttributes = { [SENTRY_KIND]: 'client', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, @@ -180,10 +191,20 @@ function subscribeQuery(): void { [SERVER_PORT]: connection?.port ?? extractPortFromConnectionString(connectionString), [NETWORK_TRANSPORT]: connection?.filename === ':memory:' ? 'inproc' : undefined, [DB_QUERY_TEXT]: dbStatement, + [DB_QUERY_SUMMARY]: querySummary, }; + const sentryClient = getClient(); + // With span streaming, span names have to be low cardinality, so `{db.query.summary}` is used + // instead of the full statement, falling back to `getName`'s `{operation} {namespace}.{table}` + // when there is no statement to summarize. + const streamedName = + sentryClient && hasSpanStreamingEnabled(sentryClient) + ? querySummary || getName(name, operation, table) || DB_SPAN_NAME_FALLBACK + : undefined; + return startInactiveSpan({ - name: dbStatement ?? getName(name, operation, table) ?? 'knex.query', + name: streamedName ?? dbStatement ?? getName(name, operation, table) ?? 'knex.query', op: 'db', parentSpan, attributes, diff --git a/packages/server-utils/src/integrations/prisma/tracing-helper.ts b/packages/server-utils/src/integrations/prisma/tracing-helper.ts index 20f1e3552d18..365ac06caa9c 100644 --- a/packages/server-utils/src/integrations/prisma/tracing-helper.ts +++ b/packages/server-utils/src/integrations/prisma/tracing-helper.ts @@ -15,8 +15,12 @@ import type { Span, SpanAttributes } from '@sentry/core'; import { + _INTERNAL_getSqlQuerySummary, + _INTERNAL_sanitizeSqlQuery, debug, getActiveSpan, + getClient, + hasSpanStreamingEnabled, LRUMap, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, @@ -24,7 +28,15 @@ import { } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import type { EngineSpan, ExtendedSpanOptions, SpanCallback, TracingHelper } from './types'; -import { DB_STATEMENT, DB_SYSTEM, DB_SYSTEM_NAME, SENTRY_KIND, SENTRY_OP } from '@sentry/conventions/attributes'; +import { + DB_QUERY_SUMMARY, + DB_QUERY_TEXT, + DB_STATEMENT, + DB_SYSTEM, + DB_SYSTEM_NAME, + SENTRY_KIND, + SENTRY_OP, +} from '@sentry/conventions/attributes'; // Reading `process.env` can throw in runtimes that gate env access (e.g. Deno without `--allow-env`) // and `process` may be absent altogether (edge runtimes), so this degrades to `false` in those cases. @@ -102,24 +114,47 @@ function buildSpanAttributes(name: string, attributes: Record | merged[SENTRY_OP] = 'db'; } + const statement = getSqlStatement(name, merged); + if (statement) { + // Sanitized before summarizing, so that a string literal containing `from`/`join` can't leak a + // value into the summary. + merged[DB_QUERY_SUMMARY] = _INTERNAL_getSqlQuerySummary(_INTERNAL_sanitizeSqlQuery(statement)); + } + return merged; } /** - * Db query spans are named after their SQL text (e.g. `SELECT * FROM "User"`) rather than the generic - * engine name. v5/v6 emit `prisma:engine:db_query`; v7 inlined the engine and emits `prisma:client:db_query`. + * The SQL a span reports, if any. Prisma emits it as the deprecated `db.statement` on older versions + * and as `db.query.text` on the `db_query` spans of newer ones. */ -function buildSpanName(name: string, attributes: SpanAttributes): string { +function getSqlStatement(name: string, attributes: SpanAttributes): string | undefined { // oxlint-disable-next-line typescript/no-deprecated const dbStatement = attributes[DB_STATEMENT]; if (typeof dbStatement === 'string' && dbStatement) { return dbStatement; } - const queryText = attributes['db.query.text']; + const queryText = attributes[DB_QUERY_TEXT]; if ((name === 'prisma:engine:db_query' || name === 'prisma:client:db_query') && typeof queryText === 'string') { return queryText; } - return name; + return undefined; +} + +/** + * Db query spans are named after their SQL text (e.g. `SELECT * FROM "User"`) rather than the generic + * engine name. v5/v6 emit `prisma:engine:db_query`; v7 inlined the engine and emits `prisma:client:db_query`. + */ +function buildSpanName(name: string, attributes: SpanAttributes): string { + const client = getClient(); + + // With span streaming, span names have to be low cardinality, so `{db.query.summary}` is used + // instead of the full statement. Spans that report no SQL keep the engine span name. + if (client && hasSpanStreamingEnabled(client)) { + return (attributes[DB_QUERY_SUMMARY] as string | undefined) || name; + } + + return getSqlStatement(name, attributes) ?? name; } /** diff --git a/packages/server-utils/src/integrations/tedious.ts b/packages/server-utils/src/integrations/tedious.ts index 65f0ea8d2c6b..e4106a5a8baa 100644 --- a/packages/server-utils/src/integrations/tedious.ts +++ b/packages/server-utils/src/integrations/tedious.ts @@ -7,6 +7,8 @@ import * as diagnosticsChannel from 'node:diagnostics_channel'; import type { IntegrationFn, SpanAttributes } from '@sentry/core'; import { defineIntegration, + getClient, + hasSpanStreamingEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, @@ -140,8 +142,14 @@ function subscribeQuery(channelName: string, operation: string): void { [SERVER_PORT]: connection.config?.options?.port, }; + const client = getClient(); + // `getSpanName` already builds `{db.operation.name}` paired with the bulk-load table, the stored + // procedure or `{db.namespace}`, so with span streaming — where span names have to be low + // cardinality — it is used instead of the SQL statement. + const spanName = getSpanName(operation, databaseName, sql, request.table); + const span = startInactiveSpan({ - name: sql || getSpanName(operation, databaseName, sql, request.table), + name: client && hasSpanStreamingEnabled(client) ? spanName : sql || spanName, op: 'db', attributes, });