Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { DEBUG_BUILD } from '../../../debug-build';
import { CHANNELS } from '../../../orchestrion/channels';
import type { TracingChannelLifeCycleOptions } from '../../../tracing-channel';
import { bindTracingChannelToSpan } from '../../../tracing-channel';
import { bindTracingChannelToSpan, safeChannelCallback } from '../../../tracing-channel';
import { AWS_SDK_ORIGIN } from './constants';
import { ServicesExtensions } from './services';
import type { NormalizedRequest, NormalizedResponse, RequestMetadata } from './types';
Expand Down Expand Up @@ -49,16 +49,6 @@ interface AwsV3Command {
constructor?: { name?: string };
}

/** Runs a span-building callback so a throw inside it can never break the user's aws-sdk call. */
function safe<T>(fn: () => T): T | undefined {
try {
return fn();
} catch (error) {
DEBUG_BUILD && debug.warn('[orchestrion:aws-sdk] error building span', error);
return undefined;
}
}

// `metadata` is smithy's `ResponseMetadata`, read off the untyped channel result/error (`any` for the
// same reason as `CommandInput`, see types.ts).
function setMetadataAttributes(span: Span, metadata: Record<string, any> | undefined): void {
Expand Down Expand Up @@ -91,7 +81,7 @@ const _awsChannelIntegration = (() => {
}

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

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

return span;
});
Expand All @@ -186,7 +176,7 @@ const _awsChannelIntegration = (() => {

// The channel `result`/`error` are untyped; the `$metadata` casts below name smithy's
// `ResponseMetadata` shape (`any`-valued, see `setMetadataAttributes`).
safe(() => {
safeChannelCallback(() => {
if (failed) {
const err = data.error as
| { $metadata?: Record<string, any>; RequestId?: string; extendedRequestId?: string }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import * as diagnosticsChannel from 'node:diagnostics_channel';
import { debug } from '@sentry/core';
import { DEBUG_BUILD } from '../../../debug-build';
import { CHANNELS } from '../../../orchestrion/channels';
import { bindTracingChannelToSpan } from '../../../tracing-channel';
import { bindTracingChannelToSpan, safeChannelCallback } from '../../../tracing-channel';
import type { FirestoreReference } from './firestore-types';
import { startFirestoreSpan } from './firestore';
import { wrapFunctionsRegistration } from './functions';
Expand Down Expand Up @@ -43,24 +41,10 @@ const FUNCTIONS_TRIGGERS: Array<{ channel: string; triggerType: string }> = [

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

/**
* Runs a span-building callback so a throw inside it can never break the user's firebase call: these run
* inside the `tracingChannel(...)` machinery wrapping the real function, where an unguarded throw would
* propagate into the traced call.
*/
function safe<T>(fn: () => T): T | undefined {
try {
return fn();
} catch (error) {
DEBUG_BUILD && debug.warn('[orchestrion:firebase] error handling channel event', error);
return undefined;
}
}

export function instrumentFirebase() {
for (const { channel, spanName, useParent } of FIRESTORE_OPERATIONS) {
bindTracingChannelToSpan(diagnosticsChannel.tracingChannel<FirestoreChannelContext>(channel), data =>
safe(() => {
safeChannelCallback(() => {
const reference = data.arguments[0] as FirestoreReference | undefined;
if (!reference) {
return undefined;
Expand All @@ -76,7 +60,8 @@ export function instrumentFirebase() {
// registration call, so we only rewrap the handler argument here (in `start`) and open the
// span inside that wrapper. The other lifecycle events are irrelevant, so no-op them.
diagnosticsChannel.tracingChannel(channel).subscribe({
start: data => void safe(() => wrapFunctionsRegistration(data as { arguments: unknown[] }, triggerType)),
start: data =>
void safeChannelCallback(() => wrapFunctionsRegistration(data as { arguments: unknown[] }, triggerType)),
end: NOOP,
asyncStart: NOOP,
asyncEnd: NOOP,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import * as diagnosticsChannel from 'node:diagnostics_channel';
import type { IntegrationFn } from '@sentry/core';
import { debug, defineIntegration, extendIntegration, waitForTracingChannelBinding } from '@sentry/core';
import { DEBUG_BUILD } from '../../../debug-build';
import { defineIntegration, extendIntegration, waitForTracingChannelBinding } from '@sentry/core';
import { graphqlIntegration as graphqlNativeIntegration } from '../../../graphql';
import type { GraphqlDiagnosticChannelsOptions } from '../../../graphql/graphql-dc-subscriber';
import { CHANNELS } from '../../../orchestrion/channels';
import { bindTracingChannelToSpan } from '../../../tracing-channel';
import { bindTracingChannelToSpan, safeChannelCallback } from '../../../tracing-channel';
import {
finalizeExecuteSpan,
finalizeValidateSpan,
Expand Down Expand Up @@ -35,20 +34,6 @@ function getOptionsWithDefaults(options: GraphqlDiagnosticChannelsOptions): Grap
};
}

/**
* Runs a span-building callback so a throw inside it can never break the user's graphql call: these
* run inside the `tracingChannel(...).trace*` machinery wrapping the real function (as the `getSpan`
* producer / `beforeSpanEnd` handler), where an unguarded throw would propagate into the traced call.
*/
function safe<T>(fn: () => T): T | undefined {
try {
return fn();
} catch (error) {
DEBUG_BUILD && debug.warn('[orchestrion:graphql] error building span', error);
return undefined;
}
}

const _graphqlChannelIntegration = ((options: GraphqlDiagnosticChannelsOptions = {}) => {
const config = getOptionsWithDefaults(options);
const getConfig = (): GraphqlResolvedConfig => config;
Expand All @@ -62,19 +47,19 @@ const _graphqlChannelIntegration = ((options: GraphqlDiagnosticChannelsOptions =

waitForTracingChannelBinding(() => {
bindTracingChannelToSpan(diagnosticsChannel.tracingChannel<GraphqlChannelContext>(CHANNELS.GRAPHQL_PARSE), () =>
safe(() => startParseSpan()),
safeChannelCallback(() => startParseSpan()),
);

bindTracingChannelToSpan(
diagnosticsChannel.tracingChannel<GraphqlChannelContext>(CHANNELS.GRAPHQL_VALIDATE),
data => safe(() => startValidateSpan(data.arguments[1])),
{ beforeSpanEnd: (span, data) => void safe(() => finalizeValidateSpan(span, data.result)) },
data => safeChannelCallback(() => startValidateSpan(data.arguments[1])),
{ beforeSpanEnd: (span, data) => void safeChannelCallback(() => finalizeValidateSpan(span, data.result)) },
);

bindTracingChannelToSpan(
diagnosticsChannel.tracingChannel<GraphqlChannelContext>(CHANNELS.GRAPHQL_EXECUTE),
data => safe(() => startExecuteSpan(data.arguments, data.self, config, getConfig)),
{ beforeSpanEnd: (span, data) => void safe(() => finalizeExecuteSpan(span, data.result)) },
data => safeChannelCallback(() => startExecuteSpan(data.arguments, data.self, config, getConfig)),
{ beforeSpanEnd: (span, data) => void safeChannelCallback(() => finalizeExecuteSpan(span, data.result)) },
);
});
},
Expand Down
10 changes: 10 additions & 0 deletions packages/server-utils/src/tracing-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ export interface TracingChannelBindingHandle<TData extends object = object> {

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

/** Runs a span-building callback so a throw inside it can never break the user's traced call. */
export function safeChannelCallback<T>(fn: () => T): T | undefined {
try {
return fn();
} catch (error) {
DEBUG_BUILD && debug.warn('[orchestrion] error handling channel event', error);
return undefined;
}
}

/**
* Bind a span and its lifecycle to a tracing channel so the span becomes the active async context
* for the traced operation and is ended when the operation completes.
Expand Down
Loading