Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion packages/server-utils/src/integrations/knex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -17,6 +22,7 @@ import {
import {
DB_NAMESPACE,
DB_OPERATION_NAME,
DB_QUERY_SUMMARY,
DB_QUERY_TEXT,
DB_SYSTEM_NAME,
DB_USER,
Expand Down Expand Up @@ -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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Truncation breaks summary sanitization

Medium Severity

Knex truncates the SQL with truncate before _INTERNAL_sanitizeSqlQuery and _INTERNAL_getSqlQuerySummary. Truncation can cut an open string literal, so the sanitize pass misses it and FROM/JOIN tokens inside that literal can land in db.query.summary and the streamed span name, which is the leak this sanitization step is meant to block.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c57ede4. Configure here.

const attributes: SpanAttributes = {
[SENTRY_KIND]: 'client',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,
Expand All @@ -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',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feat PR missing integration tests

Medium Severity

Flagged because the PR review guidelines ask feat PRs to include at least one integration or E2E test. This change adds streamed low-cardinality names and db.query.summary for knex, prisma, and tedious, but the commit has no tests that assert the new naming or attributes under traceLifecycle: 'stream' versus static.

Additional Locations (2)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit c57ede4. Configure here.

op: 'db',
parentSpan,
attributes,
Expand Down
47 changes: 41 additions & 6 deletions packages/server-utils/src/integrations/prisma/tracing-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,28 @@

import type { Span, SpanAttributes } from '@sentry/core';
import {
_INTERNAL_getSqlQuerySummary,
_INTERNAL_sanitizeSqlQuery,
debug,
getActiveSpan,
getClient,
hasSpanStreamingEnabled,
LRUMap,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startInactiveSpan,
startSpanManual,
} 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.
Expand Down Expand Up @@ -102,24 +114,47 @@ function buildSpanAttributes(name: string, attributes: Record<string, unknown> |
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;
}

/**
Expand Down
10 changes: 9 additions & 1 deletion packages/server-utils/src/integrations/tedious.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
});
Expand Down
Loading