From c57ede4cc0ca101d27fed063948083a026615adc Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 25 Aug 2026 17:37:53 +0200 Subject: [PATCH] feat(server-utils)!: Emit low cardinality knex, tedious and prisma db span names With span streaming, these three name their query spans from the span name conventions instead of the SQL statement. They are grouped because each needs a different fallback: knex drops to its existing `{operation} {namespace}.{table}`, tedious has no statement to summarize and keeps `getSpanName`, and prisma resolves its statement from either `db.statement` or `db.query.text` depending on version. knex and prisma also report the new `db.query.summary` attribute. `traceLifecycle: 'static'` keeps the existing names. Refs #23523 Co-Authored-By: Claude Opus 5 (1M context) --- .../server-utils/src/integrations/knex.ts | 23 ++++++++- .../src/integrations/prisma/tracing-helper.ts | 47 ++++++++++++++++--- .../server-utils/src/integrations/tedious.ts | 10 +++- 3 files changed, 72 insertions(+), 8 deletions(-) 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, });