Skip to content

Commit 2ba6e06

Browse files
nicohrubecclaude
andauthored
ref(server-utils): Extract shared safeChannelCallback helper (#22435)
Extracts the three identical `safe` try/catch wrappers in the firebase, graphql, and aws-sdk tracing-channel integrations into a single shared `safeChannelCallback` helper in `tracing-channel.ts`. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2d404f5 commit 2ba6e06

4 files changed

Lines changed: 26 additions & 56 deletions

File tree

packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
import { DEBUG_BUILD } from '../../../debug-build';
1818
import { CHANNELS } from '../../../orchestrion/channels';
1919
import type { TracingChannelLifeCycleOptions } from '../../../tracing-channel';
20-
import { bindTracingChannelToSpan } from '../../../tracing-channel';
20+
import { bindTracingChannelToSpan, safeChannelCallback } from '../../../tracing-channel';
2121
import { AWS_SDK_ORIGIN } from './constants';
2222
import { ServicesExtensions } from './services';
2323
import type { NormalizedRequest, NormalizedResponse, RequestMetadata } from './types';
@@ -49,16 +49,6 @@ interface AwsV3Command {
4949
constructor?: { name?: string };
5050
}
5151

52-
/** Runs a span-building callback so a throw inside it can never break the user's aws-sdk call. */
53-
function safe<T>(fn: () => T): T | undefined {
54-
try {
55-
return fn();
56-
} catch (error) {
57-
DEBUG_BUILD && debug.warn('[orchestrion:aws-sdk] error building span', error);
58-
return undefined;
59-
}
60-
}
61-
6252
// `metadata` is smithy's `ResponseMetadata`, read off the untyped channel result/error (`any` for the
6353
// same reason as `CommandInput`, see types.ts).
6454
function setMetadataAttributes(span: Span, metadata: Record<string, any> | undefined): void {
@@ -91,7 +81,7 @@ const _awsChannelIntegration = (() => {
9181
}
9282

9383
const getSpan = (data: AwsSendChannelContext): Span | undefined =>
94-
safe(() => {
84+
safeChannelCallback(() => {
9585
const command = data.arguments[0] as AwsV3Command | undefined;
9686
const commandName = command?.constructor?.name;
9787
if (!command || !commandName) {
@@ -140,7 +130,7 @@ const _awsChannelIntegration = (() => {
140130
// so `cloud.region` cannot be lost when `send` settles first (e.g. an early failure).
141131
//
142132
// The provider call is guarded separately: the span is already started, so a synchronous
143-
// throw bubbling into the enclosing `safe` would discard it without ending it (a leaked
133+
// throw bubbling into the enclosing `safeChannelCallback` would discard it without ending it (a leaked
144134
// open span).
145135
let regionResult: string | Promise<string> | undefined;
146136
try {
@@ -169,7 +159,7 @@ const _awsChannelIntegration = (() => {
169159

170160
// Inject trace-propagation headers into outgoing messages (SQS/SNS/Lambda). Runs before
171161
// `send` proceeds, so the mutated `commandInput` is used to build the request.
172-
safe(() => servicesExtensions.requestPostSpanHook(normalizedRequest, span));
162+
safeChannelCallback(() => servicesExtensions.requestPostSpanHook(normalizedRequest, span));
173163

174164
return span;
175165
});
@@ -186,7 +176,7 @@ const _awsChannelIntegration = (() => {
186176

187177
// The channel `result`/`error` are untyped; the `$metadata` casts below name smithy's
188178
// `ResponseMetadata` shape (`any`-valued, see `setMetadataAttributes`).
189-
safe(() => {
179+
safeChannelCallback(() => {
190180
if (failed) {
191181
const err = data.error as
192182
| { $metadata?: Record<string, any>; RequestId?: string; extendedRequestId?: string }

packages/server-utils/src/integrations/tracing-channel/firebase/instrumentation.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import * as diagnosticsChannel from 'node:diagnostics_channel';
2-
import { debug } from '@sentry/core';
3-
import { DEBUG_BUILD } from '../../../debug-build';
42
import { CHANNELS } from '../../../orchestrion/channels';
5-
import { bindTracingChannelToSpan } from '../../../tracing-channel';
3+
import { bindTracingChannelToSpan, safeChannelCallback } from '../../../tracing-channel';
64
import type { FirestoreReference } from './firestore-types';
75
import { startFirestoreSpan } from './firestore';
86
import { wrapFunctionsRegistration } from './functions';
@@ -43,24 +41,10 @@ const FUNCTIONS_TRIGGERS: Array<{ channel: string; triggerType: string }> = [
4341

4442
const NOOP = (): void => {};
4543

46-
/**
47-
* Runs a span-building callback so a throw inside it can never break the user's firebase call: these run
48-
* inside the `tracingChannel(...)` machinery wrapping the real function, where an unguarded throw would
49-
* propagate into the traced call.
50-
*/
51-
function safe<T>(fn: () => T): T | undefined {
52-
try {
53-
return fn();
54-
} catch (error) {
55-
DEBUG_BUILD && debug.warn('[orchestrion:firebase] error handling channel event', error);
56-
return undefined;
57-
}
58-
}
59-
6044
export function instrumentFirebase() {
6145
for (const { channel, spanName, useParent } of FIRESTORE_OPERATIONS) {
6246
bindTracingChannelToSpan(diagnosticsChannel.tracingChannel<FirestoreChannelContext>(channel), data =>
63-
safe(() => {
47+
safeChannelCallback(() => {
6448
const reference = data.arguments[0] as FirestoreReference | undefined;
6549
if (!reference) {
6650
return undefined;
@@ -76,7 +60,8 @@ export function instrumentFirebase() {
7660
// registration call, so we only rewrap the handler argument here (in `start`) and open the
7761
// span inside that wrapper. The other lifecycle events are irrelevant, so no-op them.
7862
diagnosticsChannel.tracingChannel(channel).subscribe({
79-
start: data => void safe(() => wrapFunctionsRegistration(data as { arguments: unknown[] }, triggerType)),
63+
start: data =>
64+
void safeChannelCallback(() => wrapFunctionsRegistration(data as { arguments: unknown[] }, triggerType)),
8065
end: NOOP,
8166
asyncStart: NOOP,
8267
asyncEnd: NOOP,

packages/server-utils/src/integrations/tracing-channel/graphql/index.ts

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import * as diagnosticsChannel from 'node:diagnostics_channel';
22
import type { IntegrationFn } from '@sentry/core';
3-
import { debug, defineIntegration, extendIntegration, waitForTracingChannelBinding } from '@sentry/core';
4-
import { DEBUG_BUILD } from '../../../debug-build';
3+
import { defineIntegration, extendIntegration, waitForTracingChannelBinding } from '@sentry/core';
54
import { graphqlIntegration as graphqlNativeIntegration } from '../../../graphql';
65
import type { GraphqlDiagnosticChannelsOptions } from '../../../graphql/graphql-dc-subscriber';
76
import { CHANNELS } from '../../../orchestrion/channels';
8-
import { bindTracingChannelToSpan } from '../../../tracing-channel';
7+
import { bindTracingChannelToSpan, safeChannelCallback } from '../../../tracing-channel';
98
import {
109
finalizeExecuteSpan,
1110
finalizeValidateSpan,
@@ -35,20 +34,6 @@ function getOptionsWithDefaults(options: GraphqlDiagnosticChannelsOptions): Grap
3534
};
3635
}
3736

38-
/**
39-
* Runs a span-building callback so a throw inside it can never break the user's graphql call: these
40-
* run inside the `tracingChannel(...).trace*` machinery wrapping the real function (as the `getSpan`
41-
* producer / `beforeSpanEnd` handler), where an unguarded throw would propagate into the traced call.
42-
*/
43-
function safe<T>(fn: () => T): T | undefined {
44-
try {
45-
return fn();
46-
} catch (error) {
47-
DEBUG_BUILD && debug.warn('[orchestrion:graphql] error building span', error);
48-
return undefined;
49-
}
50-
}
51-
5237
const _graphqlChannelIntegration = ((options: GraphqlDiagnosticChannelsOptions = {}) => {
5338
const config = getOptionsWithDefaults(options);
5439
const getConfig = (): GraphqlResolvedConfig => config;
@@ -62,19 +47,19 @@ const _graphqlChannelIntegration = ((options: GraphqlDiagnosticChannelsOptions =
6247

6348
waitForTracingChannelBinding(() => {
6449
bindTracingChannelToSpan(diagnosticsChannel.tracingChannel<GraphqlChannelContext>(CHANNELS.GRAPHQL_PARSE), () =>
65-
safe(() => startParseSpan()),
50+
safeChannelCallback(() => startParseSpan()),
6651
);
6752

6853
bindTracingChannelToSpan(
6954
diagnosticsChannel.tracingChannel<GraphqlChannelContext>(CHANNELS.GRAPHQL_VALIDATE),
70-
data => safe(() => startValidateSpan(data.arguments[1])),
71-
{ beforeSpanEnd: (span, data) => void safe(() => finalizeValidateSpan(span, data.result)) },
55+
data => safeChannelCallback(() => startValidateSpan(data.arguments[1])),
56+
{ beforeSpanEnd: (span, data) => void safeChannelCallback(() => finalizeValidateSpan(span, data.result)) },
7257
);
7358

7459
bindTracingChannelToSpan(
7560
diagnosticsChannel.tracingChannel<GraphqlChannelContext>(CHANNELS.GRAPHQL_EXECUTE),
76-
data => safe(() => startExecuteSpan(data.arguments, data.self, config, getConfig)),
77-
{ beforeSpanEnd: (span, data) => void safe(() => finalizeExecuteSpan(span, data.result)) },
61+
data => safeChannelCallback(() => startExecuteSpan(data.arguments, data.self, config, getConfig)),
62+
{ beforeSpanEnd: (span, data) => void safeChannelCallback(() => finalizeExecuteSpan(span, data.result)) },
7863
);
7964
});
8065
},

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ export interface TracingChannelBindingHandle<TData extends object = object> {
9090

9191
const NOOP = (): void => {};
9292

93+
/** Runs a span-building callback so a throw inside it can never break the user's traced call. */
94+
export function safeChannelCallback<T>(fn: () => T): T | undefined {
95+
try {
96+
return fn();
97+
} catch (error) {
98+
DEBUG_BUILD && debug.warn('[orchestrion] error handling channel event', error);
99+
return undefined;
100+
}
101+
}
102+
93103
/**
94104
* Bind a span and its lifecycle to a tracing channel so the span becomes the active async context
95105
* for the traced operation and is ended when the operation completes.

0 commit comments

Comments
 (0)