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
24 changes: 22 additions & 2 deletions packages/server-utils/src/integrations/mongodb/mongodb-span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import {
SERVER_PORT,
} from '@sentry/conventions/attributes';
import type { Span, SpanAttributes } from '@sentry/core';
import { isObjectLike, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core';
import {
getClient,
hasSpanStreamingEnabled,
isObjectLike,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startInactiveSpan,
} from '@sentry/core';

// `db.connection_string` is not part of `@sentry/conventions`, so it stays inlined to match
// what `@opentelemetry/instrumentation-mongodb` emitted.
Expand Down Expand Up @@ -220,8 +226,22 @@ export function getV3SpanAttributes(
* to support platforms that lack it (ie, Deno).
*/
export function startMongoSpan(attributes: SpanAttributes): Span {
const client = getClient();
const operation = attributes[DB_OPERATION_NAME] as string | undefined;
const target = (attributes[DB_COLLECTION_NAME] || attributes[DB_NAMESPACE]) as string | undefined;
// With span streaming, span names have to be low cardinality, so `{db.operation.name}` paired with
// `{db.collection.name}` (or `{db.namespace}`) is used instead of the query document, falling back
// to that target alone and then to `{db.system.name}`.
const streamedName =
client && hasSpanStreamingEnabled(client)
? operation && target
? `${operation} ${target}`
: target || DB_SYSTEM_VALUE_MONGODB
: undefined;

return startInactiveSpan({
name: (attributes[DB_QUERY_TEXT] as string) || `mongodb.${attributes[DB_OPERATION_NAME] || 'command'}`,
name:
streamedName || (attributes[DB_QUERY_TEXT] as string) || `mongodb.${attributes[DB_OPERATION_NAME] || 'command'}`,
op: 'db',
attributes: {
[SENTRY_KIND]: 'client',
Expand Down
17 changes: 16 additions & 1 deletion packages/server-utils/src/integrations/redis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
isObjectLike,
defineIntegration,
getActiveSpan,
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SPAN_STATUS_ERROR,
startInactiveSpan,
Expand Down Expand Up @@ -101,8 +103,21 @@ function nodeRedisAttributes(options: NodeRedisClientOptions | undefined): SpanA

function startCommandSpan(commandName: string, commandArgs: Array<string | Buffer>, attributes: SpanAttributes): Span {
const dbStatement = defaultDbStatementSerializer(commandName, commandArgs);
const client = getClient();
const host = attributes[SERVER_ADDRESS];
const port = attributes[SERVER_PORT];
// The serialized statement carries command arguments, so with span streaming — where span names have
// to be low cardinality — `{db.operation.name} {server.address}:{server.port}` is used instead.
// Redis has no collection or namespace to pair with, so `{db.system.name}` is next.
const streamedName =
client && hasSpanStreamingEnabled(client)
? host && port != null
? `${commandName} ${host}:${port}`
: DB_SYSTEM_VALUE_REDIS
: undefined;

return startInactiveSpan({
name: dbStatement || `redis-${commandName}`,
name: streamedName || dbStatement || `redis-${commandName}`,
attributes: {
[SENTRY_KIND]: 'client',
...attributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@sentry/conventions/attributes';
import { DB_QUERY, DB } from '@sentry/conventions/op';
import type { Span } from '@sentry/core';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core';
import { getClient, hasSpanStreamingEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core';
import { CHANNELS } from '../../orchestrion/channels';
import { bindTracingChannelToSpan } from '../../tracing-channel';
import type { RedisCacheOptions } from './redis-cache';
Expand Down Expand Up @@ -76,8 +76,19 @@ export function startIORedisCommandSpan(data: IORedisCommandContext): Span | und
tracedCommands.add(command);
const { host, port } = getConnectionOptions(data.self);
const statement = defaultDbStatementSerializer(command.name, command.args ?? []);
const client = getClient();
// The serialized statement carries command arguments, so with span streaming — where span names have
// to be low cardinality — `{db.operation.name} {server.address}:{server.port}` is used instead.
// Redis has no collection or namespace to pair with, so `{db.system.name}` is next.
const streamedName =
client && hasSpanStreamingEnabled(client)
? host && port != null
? `${command.name} ${host}:${port}`
: 'redis'
: undefined;

return startInactiveSpan({
name: statement,
name: streamedName || statement,
attributes: {
[SENTRY_KIND]: 'client',
...connectionAttributes(host, port),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ describe('startIORedisCommandSpan', () => {
);
});

it('names the span from the conventions with span streaming enabled', () => {
vi.spyOn(SentryCore, 'getClient').mockReturnValue({
getOptions: () => ({ traceLifecycle: 'stream' }),
} as unknown as ReturnType<typeof SentryCore.getClient>);

startIORedisCommandSpan(ctx({ name: 'set', args: ['test-key', 'test-value'] }));

expect(startInactiveSpanSpy).toHaveBeenCalledWith(
expect.objectContaining({
// `{db.operation.name} {server.address}:{server.port}` — redis has no collection or namespace
name: 'set localhost:6379',
// the serialized statement, which carries the key, is still reported as an attribute
attributes: expect.objectContaining({ 'db.query.text': 'set test-key [1 other arguments]' }),
}),
);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing streamed integration tests

Medium Severity

This feat PR only adds a unit test for the ioredis orchestrion path. Per the review rules, feat PRs need at least one integration or E2E test. The new low-cardinality naming for redis (startCommandSpan) and mongodb (startMongoSpan) also has no coverage, and there is no streamed suite like postgres-streamed. Flagged because it was mentioned in the rules file.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 866f29c. Configure here.


it('emits a single span when the same command is re-sent from the offline queue', () => {
const command = { name: 'set', args: ['test-key', 'test-value'] };

Expand Down
Loading