Skip to content

Commit 64bbb08

Browse files
feat(server-utils): Add requiresParentSpan option to bindTracingChannelToSpan (#22113)
A bunch of the diagnostics-channel subscribers repeated the same guard at the top of their `getSpan` callback. Usually the logic is to bail out with `undefined` when there's no active span, so we don't open a top-level db span for queries/connects that happen outside a request. This pulls that into a `requiresParentSpan` option on `bindTracingChannelToSpan` and moves the check into the bind store, before `getSpan` runs. Behaviorally identical, just one place instead of five. If you need to actually inspect the parent span before deciding, that still belongs in `getSpan`. Feel free to push back if this over complicates the `bindTracingChannelToSpan`, I thought it was a nice opportunity to normalize this behavior but not a biggie. --------- Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com>
1 parent 9a4e2c3 commit 64bbb08

6 files changed

Lines changed: 82 additions & 79 deletions

File tree

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ module.exports = [
400400
import: createImport('init', 'experimentalUseDiagnosticsChannelInjection'),
401401
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
402402
gzip: true,
403-
limit: '135 KB',
403+
limit: '136 KB',
404404
disablePlugins: ['@size-limit/esbuild'],
405405
},
406406
{

packages/server-utils/src/integrations/tracing-channel/ioredis.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type { IntegrationFn, Span } from '@sentry/core';
88
import {
99
debug,
1010
defineIntegration,
11-
getActiveSpan,
1211
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
1312
startInactiveSpan,
1413
waitForTracingChannelBinding,
@@ -96,10 +95,6 @@ const _ioredisChannelIntegration = ((options: IORedisChannelIntegrationOptions =
9695
bindTracingChannelToSpan(
9796
commandChannel,
9897
data => {
99-
// ioredis' `requireParentSpan` default: only create a span under an active span.
100-
if (!getActiveSpan()) {
101-
return undefined;
102-
}
10398
const command = data.arguments?.[0] as RedisCommand | undefined;
10499
if (!command || typeof command !== 'object') {
105100
return undefined;
@@ -113,6 +108,8 @@ const _ioredisChannelIntegration = ((options: IORedisChannelIntegrationOptions =
113108
});
114109
},
115110
{
111+
// ioredis' `requireParentSpan` default: only create a span under an active span.
112+
requiresParentSpan: true,
116113
beforeSpanEnd(span, data) {
117114
if ('error' in data || !responseHook) {
118115
return;
@@ -125,17 +122,18 @@ const _ioredisChannelIntegration = ((options: IORedisChannelIntegrationOptions =
125122
},
126123
);
127124

128-
bindTracingChannelToSpan(connectChannel, data => {
129-
if (!getActiveSpan()) {
130-
return undefined;
131-
}
132-
const { host, port } = getConnectionOptions(data.self);
133-
return startInactiveSpan({
134-
name: 'connect',
135-
op: 'db',
136-
attributes: { ...connectionAttributes(host, port), [DB_STATEMENT]: 'connect' },
137-
});
138-
});
125+
bindTracingChannelToSpan(
126+
connectChannel,
127+
data => {
128+
const { host, port } = getConnectionOptions(data.self);
129+
return startInactiveSpan({
130+
name: 'connect',
131+
op: 'db',
132+
attributes: { ...connectionAttributes(host, port), [DB_STATEMENT]: 'connect' },
133+
});
134+
},
135+
{ requiresParentSpan: true },
136+
);
139137
});
140138
},
141139
};

packages/server-utils/src/integrations/tracing-channel/postgres-js.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
_INTERNAL_setPostgresOperationName,
1010
debug,
1111
defineIntegration,
12-
getActiveSpan,
1312
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
1413
SPAN_KIND,
1514
SPAN_STATUS_ERROR,
@@ -257,15 +256,12 @@ const _postgresJsChannelIntegration = ((options: PostgresJsChannelIntegrationOpt
257256
return undefined;
258257
}
259258

260-
// Opt out of: re-entrant `handle()` calls (then/catch/finally re-invoke
261-
// it, guarded by `executed`), queries already wrapped by the portable
262-
// `instrumentPostgresJsSql`, and (by default) queries with no parent span.
259+
// Opt out of re-entrant `handle()` calls (then/catch/finally re-invoke it, guarded by
260+
// `executed`) and queries already wrapped by the portable `instrumentPostgresJsSql`. The
261+
// parent-span requirement is applied via `requiresParentSpan` below.
263262
if (query.executed === true || (query as Record<symbol, unknown>)[QUERY_FROM_INSTRUMENTED_SQL]) {
264263
return undefined;
265264
}
266-
if (requireParentSpan !== false && !getActiveSpan()) {
267-
return undefined;
268-
}
269265

270266
const fullQuery = _INTERNAL_reconstructPostgresQuery(query.strings);
271267
const sanitizedSqlQuery = _INTERNAL_sanitizeSqlQuery(fullQuery);
@@ -306,6 +302,7 @@ const _postgresJsChannelIntegration = ((options: PostgresJsChannelIntegrationOpt
306302
return span;
307303
},
308304
{
305+
requiresParentSpan: requireParentSpan !== false,
309306
deferSpanEnd({ data }) {
310307
// `handle` is async: its promise settles on dispatch (asyncEnd), long
311308
// before the query does. The resolve/reject wrappers own the ending.

packages/server-utils/src/integrations/tracing-channel/postgres.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
bindScopeToEmitter,
55
debug,
66
defineIntegration,
7-
getActiveSpan,
87
getCurrentScope,
98
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
109
SPAN_KIND,
@@ -121,12 +120,6 @@ function subscribeQueryLikeChannel(
121120
bindTracingChannelToSpan(
122121
diagnosticsChannel.tracingChannel<PgChannelContext>(channelName),
123122
data => {
124-
// Only instrument when there's an active span; returning `undefined` opts this call out entirely,
125-
// leaving the active context untouched (e.g. connects issued during app startup).
126-
if (!getActiveSpan()) {
127-
return undefined;
128-
}
129-
130123
// Capture the caller's scope while still synchronously inside the call, for the streamed path:
131124
// pg dispatches a `Submittable` emitter's events outside the original async scope, so `deferSpanEnd`
132125
// replays this scope onto that emitter.
@@ -143,6 +136,9 @@ function subscribeQueryLikeChannel(
143136
// `query` can return a streamable `Submittable`, so only it defers.
144137
deferStreamedResult
145138
? {
139+
// Only instrument under an active span, leaving the context untouched otherwise
140+
// (e.g. connects issued during app startup).
141+
requiresParentSpan: true,
146142
// Streamable `Submittable` (e.g. `client.query(new Query())`)
147143
// returns an emitter that orchestrion stores on `ctx.result` while
148144
// firing no async events; the query isn't done until the emitter
@@ -169,7 +165,7 @@ function subscribeQueryLikeChannel(
169165
return true;
170166
},
171167
}
172-
: undefined,
168+
: { requiresParentSpan: true },
173169
);
174170
}
175171

packages/server-utils/src/mysql2/mysql2-dc-subscriber.ts

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
} from '@sentry/conventions/attributes';
1010
import {
1111
_INTERNAL_sanitizeSqlQuery,
12-
getActiveSpan,
1312
SEMANTIC_ATTRIBUTE_SENTRY_OP,
1413
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
1514
startInactiveSpan,
@@ -93,51 +92,49 @@ export function subscribeMysql2DiagnosticChannels(tracingChannel: MySQL2TracingC
9392
}
9493

9594
function setupQueryChannel(tracingChannel: MySQL2TracingChannelFactory, channelName: string): void {
96-
bindTracingChannelToSpan(tracingChannel<MySQL2QueryData>(channelName), data => {
97-
// Only instrument when there's an active span
98-
if (!getActiveSpan()) {
99-
return undefined;
100-
}
95+
bindTracingChannelToSpan(
96+
tracingChannel<MySQL2QueryData>(channelName),
97+
data => {
98+
// mysql2 does not sanitize its channel payload, so the statement may carry
99+
// raw user values (on the `query` channel they are inlined). Strip every
100+
// literal before it leaves the process; `values` is never attached.
101+
const queryText = data.query ? _INTERNAL_sanitizeSqlQuery(data.query) : undefined;
102+
const operation = queryText?.match(SQL_OPERATION_RE)?.[1]?.toUpperCase();
101103

102-
// mysql2 does not sanitize its channel payload, so the statement may carry
103-
// raw user values (on the `query` channel they are inlined). Strip every
104-
// literal before it leaves the process; `values` is never attached.
105-
const queryText = data.query ? _INTERNAL_sanitizeSqlQuery(data.query) : undefined;
106-
const operation = queryText?.match(SQL_OPERATION_RE)?.[1]?.toUpperCase();
107-
108-
return startInactiveSpan({
109-
name: queryText || 'mysql2.query',
110-
attributes: {
111-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,
112-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db',
113-
[DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL,
114-
[DB_QUERY_TEXT]: queryText,
115-
[DB_OPERATION_NAME]: operation,
116-
[DB_NAMESPACE]: data.database || undefined,
117-
[SERVER_ADDRESS]: data.serverAddress,
118-
[SERVER_PORT]: data.serverPort,
119-
},
120-
});
121-
});
104+
return startInactiveSpan({
105+
name: queryText || 'mysql2.query',
106+
attributes: {
107+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,
108+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db',
109+
[DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL,
110+
[DB_QUERY_TEXT]: queryText,
111+
[DB_OPERATION_NAME]: operation,
112+
[DB_NAMESPACE]: data.database || undefined,
113+
[SERVER_ADDRESS]: data.serverAddress,
114+
[SERVER_PORT]: data.serverPort,
115+
},
116+
});
117+
},
118+
{ requiresParentSpan: true },
119+
);
122120
}
123121

124122
function setupConnectChannel(tracingChannel: MySQL2TracingChannelFactory, channelName: string, spanName: string): void {
125-
bindTracingChannelToSpan(tracingChannel<MySQL2ConnectData>(channelName), data => {
126-
// Only instrument when there's an active span.
127-
if (!getActiveSpan()) {
128-
return undefined;
129-
}
130-
131-
return startInactiveSpan({
132-
name: spanName,
133-
attributes: {
134-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,
135-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db',
136-
[DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL,
137-
[DB_NAMESPACE]: data.database || undefined,
138-
[SERVER_ADDRESS]: data.serverAddress,
139-
[SERVER_PORT]: data.serverPort,
140-
},
141-
});
142-
});
123+
bindTracingChannelToSpan(
124+
tracingChannel<MySQL2ConnectData>(channelName),
125+
data => {
126+
return startInactiveSpan({
127+
name: spanName,
128+
attributes: {
129+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,
130+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db',
131+
[DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL,
132+
[DB_NAMESPACE]: data.database || undefined,
133+
[SERVER_ADDRESS]: data.serverAddress,
134+
[SERVER_PORT]: data.serverPort,
135+
},
136+
});
137+
},
138+
{ requiresParentSpan: true },
139+
);
143140
}

packages/server-utils/src/tracing-channel.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import type { TracingChannel, TracingChannelSubscribers } from 'node:diagnostics_channel';
22
import type { AsyncLocalStorage } from 'node:async_hooks';
33
import type { ExclusiveEventHintOrCaptureContext, Span } from '@sentry/core';
4-
import { debug, captureException, SPAN_STATUS_ERROR, getAsyncContextStrategy, getMainCarrier } from '@sentry/core';
4+
import {
5+
debug,
6+
captureException,
7+
SPAN_STATUS_ERROR,
8+
getAsyncContextStrategy,
9+
getMainCarrier,
10+
getActiveSpan,
11+
} from '@sentry/core';
512
import { DEBUG_BUILD } from './debug-build';
613
import { ERROR_TYPE } from '@sentry/conventions/attributes';
714

@@ -58,6 +65,12 @@ export interface TracingChannelLifeCycleOptions<TData extends object = object> {
5865
/** Ends the span: `end()` on success, `end(error)` on failure. Idempotent. */
5966
end: (error?: unknown) => void;
6067
}) => boolean;
68+
69+
/**
70+
* Apply span/scope binding only if there is a parent span, similar to returning `undefined` in the `getSpan` callback.
71+
* If you need to perform some conditional checking on the parent span before deciding, do it in the `getSpan` callback.
72+
*/
73+
requiresParentSpan?: boolean;
6174
}
6275

6376
/** Returned by {@link bindTracingChannelToSpan}: the bound channel plus a teardown handle. */
@@ -90,7 +103,7 @@ export function bindTracingChannelToSpan<TData extends object>(
90103
getSpan: (data: TracingChannelPayloadWithSpan<TData>) => Span | undefined,
91104
opts?: TracingChannelLifeCycleOptions<TData>,
92105
): TracingChannelBindingHandle<TData> {
93-
const handle = bindSpanToChannelStore(channel, getSpan);
106+
const handle = bindSpanToChannelStore(channel, getSpan, opts);
94107

95108
const beforeSpanEnd = opts?.beforeSpanEnd;
96109
const deferSpanEnd = opts?.deferSpanEnd;
@@ -191,6 +204,7 @@ export function bindTracingChannelToSpan<TData extends object>(
191204
function bindSpanToChannelStore<TData extends object>(
192205
channel: TracingChannel<TData, TData>,
193206
getSpan: (data: TracingChannelPayloadWithSpan<TData>) => Span | undefined,
207+
opts?: Pick<TracingChannelLifeCycleOptions, 'requiresParentSpan'>,
194208
): TracingChannelBindingHandle<TData> {
195209
// Grabs the tracing channel binding defined by the AsyncContext strategy implementation
196210
const binding = getAsyncContextStrategy(getMainCarrier()).getTracingChannelBinding?.();
@@ -219,7 +233,8 @@ function bindSpanToChannelStore<TData extends object>(
219233
// callback-style channels (see `_sentryCallerStore`).
220234
data._sentryCallerStore = asyncLocalStorage.getStore();
221235

222-
const span = getSpan(data);
236+
const shouldGetSpan = !opts?.requiresParentSpan || getActiveSpan();
237+
const span = shouldGetSpan ? getSpan(data) : undefined;
223238
if (!span) {
224239
// Leave the active context untouched so nested operations keep parenting to the enclosing span.
225240
return data._sentryCallerStore as TData;

0 commit comments

Comments
 (0)