Skip to content

Commit 9edf695

Browse files
logaretmclaude
andauthored
feat: Adopt bindTracingChannelToSpan across runtimes (#21642)
This PR adopts `bindTracingChannelToSpan` utility across our packages, and drops the `tracingChannel` helper we had. The `tracingChannel` drop isn't a breaking change imo, its an internal export used by us exclusively. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6363d30 commit 9edf695

12 files changed

Lines changed: 412 additions & 935 deletions

File tree

packages/deno/src/integrations/redis.ts

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,9 @@
22
// lacking `tracingChannel` (added in Deno 1.44.3).
33
// On older runtimes the integration becomes a no-op.
44
import * as dc from 'node:diagnostics_channel';
5-
import type {
6-
RedisDiagnosticChannelResponseHook,
7-
RedisTracingChannel,
8-
RedisTracingChannelFactory,
9-
RedisTracingChannelSubscribers,
10-
} from '@sentry/server-utils';
5+
import type { RedisDiagnosticChannelResponseHook, RedisTracingChannelFactory } from '@sentry/server-utils';
116
import { subscribeRedisDiagnosticChannels } from '@sentry/server-utils';
12-
import type { Integration, IntegrationFn, Span } from '@sentry/core';
7+
import type { Integration, IntegrationFn } from '@sentry/core';
138
import { defineIntegration } from '@sentry/core';
149
import { setAsyncLocalStorageAsyncContextStrategy } from '../async';
1510

@@ -23,49 +18,17 @@ export interface DenoRedisIntegrationOptions {
2318
responseHook?: RedisDiagnosticChannelResponseHook;
2419
}
2520

26-
/**
27-
* Portable tracing-channel factory: wraps `node:diagnostics_channel.tracingChannel`
28-
* and stamps `data._sentrySpan` from `transformStart` in the `start` subscriber.
29-
*
30-
* Unlike `@sentry/opentelemetry/tracing-channel`, this does not call `bindStore`
31-
*/
32-
type DataWithSpan<T> = T & { _sentrySpan?: Span };
33-
type SubscriberFn<T> = (data: DataWithSpan<T>) => void;
34-
35-
const portableTracingChannel: RedisTracingChannelFactory = <T extends object>(
36-
name: string,
37-
transformStart: (data: T) => Span,
38-
): RedisTracingChannel<T> => {
39-
const channel = dc.tracingChannel<DataWithSpan<T>>(name);
40-
return {
41-
subscribe(subs: Partial<RedisTracingChannelSubscribers<T>>): void {
42-
const userStart = subs.start as SubscriberFn<T> | undefined;
43-
const composed: Record<string, SubscriberFn<T>> = {
44-
start(data) {
45-
data._sentrySpan = transformStart(data);
46-
userStart?.(data);
47-
},
48-
};
49-
for (const event of ['asyncStart', 'asyncEnd', 'end', 'error'] as const) {
50-
const fn = subs[event] as SubscriberFn<T> | undefined;
51-
if (fn) composed[event] = fn;
52-
}
53-
// Native subscribe is typed for the full subscriber set, but only the
54-
// handlers actually present are invoked at runtime.
55-
channel.subscribe(composed as unknown as Parameters<typeof channel.subscribe>[0]);
56-
},
57-
};
58-
};
59-
6021
const _denoRedisIntegration = ((options: DenoRedisIntegrationOptions = {}) => {
6122
return {
6223
name: INTEGRATION_NAME,
6324
setupOnce() {
6425
if (!dc.tracingChannel) {
6526
return;
6627
}
28+
// The span is bound into Deno's AsyncLocalStorage context via the async-context
29+
// strategy's `getTracingChannelBinding`, so the native channel can be passed directly.
6730
setAsyncLocalStorageAsyncContextStrategy();
68-
subscribeRedisDiagnosticChannels(portableTracingChannel, options.responseHook);
31+
subscribeRedisDiagnosticChannels(dc.tracingChannel as RedisTracingChannelFactory, options.responseHook);
6932
},
7033
};
7134
}) satisfies IntegrationFn;

packages/nitro/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"@sentry/bundler-plugin-core": "^5.3.0",
3939
"@sentry/core": "10.60.0",
4040
"@sentry/node": "10.60.0",
41-
"@sentry/opentelemetry": "10.60.0"
41+
"@sentry/server-utils": "10.60.0"
4242
},
4343
"devDependencies": {
4444
"nitro": "^3.0.260415-beta",

packages/nitro/src/runtime/hooks/captureStorageEvents.ts

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1+
import { tracingChannel } from 'node:diagnostics_channel';
12
import {
2-
captureException,
33
flushIfServerless,
44
GLOBAL_OBJ,
55
SEMANTIC_ATTRIBUTE_CACHE_HIT,
66
SEMANTIC_ATTRIBUTE_CACHE_KEY,
77
SEMANTIC_ATTRIBUTE_SENTRY_OP,
88
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
9-
SPAN_STATUS_ERROR,
10-
SPAN_STATUS_OK,
11-
startSpanManual,
9+
startInactiveSpan,
1210
} from '@sentry/core';
13-
import { tracingChannel, type TracingChannelContextWithSpan } from '@sentry/opentelemetry/tracing-channel';
11+
import { bindTracingChannelToSpan } from '@sentry/server-utils';
1412
import type { TraceContext } from 'unstorage/tracing';
1513

1614
const ORIGIN = 'auto.cache.nitro';
@@ -57,11 +55,12 @@ function setupStorageTracingChannel(operation: TracedOperation): void {
5755
const keys = (data: TraceContext): string[] => data.keys ?? [];
5856
const mountBase = (data: TraceContext): string => (data.base ?? '').replace(/:$/, '');
5957

60-
const channel = tracingChannel<TraceContext>(`unstorage.${operation}`, data => {
61-
const cacheKeys = keys(data);
58+
bindTracingChannelToSpan(
59+
tracingChannel<TraceContext>(`unstorage.${operation}`),
60+
data => {
61+
const cacheKeys = keys(data);
6262

63-
return startSpanManual(
64-
{
63+
return startInactiveSpan({
6564
name: cacheKeys.join(', ') || operation,
6665
attributes: {
6766
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `cache.${normalizeMethodName(operation)}`,
@@ -71,34 +70,24 @@ function setupStorageTracingChannel(operation: TracedOperation): void {
7170
'db.collection.name': mountBase(data),
7271
'db.system.name': data.driver?.name ?? 'unknown',
7372
},
74-
},
75-
span => span,
76-
);
77-
});
78-
79-
channel.subscribe({
80-
asyncEnd(data: TracingChannelContextWithSpan<TraceContext & { result?: unknown }>) {
81-
if (data._sentrySpan && CACHE_HIT_OPERATIONS.has(operation)) {
82-
const hit = operation === 'hasItem' ? Boolean(data.result) : isCacheHit(data.keys?.[0], data.result);
83-
data._sentrySpan.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, hit);
84-
}
85-
86-
data._sentrySpan?.setStatus({ code: SPAN_STATUS_OK });
87-
data._sentrySpan?.end();
88-
89-
void flushIfServerless();
90-
},
91-
error(data: TracingChannelContextWithSpan<TraceContext & { error?: unknown }>) {
92-
captureException(data.error, {
93-
mechanism: { handled: false, type: ORIGIN },
9473
});
95-
96-
data._sentrySpan?.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
97-
data._sentrySpan?.end();
98-
99-
void flushIfServerless();
10074
},
101-
});
75+
{
76+
beforeSpanEnd(span, data) {
77+
// Error status is set by the binding; the error itself is captured at the request boundary,
78+
// not here (cache ops aren't an error boundary). Only enrich the success path.
79+
if (!('error' in data)) {
80+
const result = (data as { result?: unknown }).result;
81+
if (CACHE_HIT_OPERATIONS.has(operation)) {
82+
const hit = operation === 'hasItem' ? Boolean(result) : isCacheHit(data.keys?.[0], result);
83+
span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, hit);
84+
}
85+
}
86+
87+
void flushIfServerless();
88+
},
89+
},
90+
);
10291
}
10392

10493
function normalizeMethodName(methodName: string): string {

0 commit comments

Comments
 (0)