Skip to content

fix(node): Ensure late init works with all integrations #16016

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 9, 2025
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
13 changes: 5 additions & 8 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,19 @@ interface AwsLambdaOptions {
disableAwsContextPropagation?: boolean;
}

export const instrumentAwsLambda = generateInstrumentOnce<AwsLambdaOptions>(
export const instrumentAwsLambda = generateInstrumentOnce(
'AwsLambda',
(_options: AwsLambdaOptions = {}) => {
const options = {
AwsLambdaInstrumentation,
(options: AwsLambdaOptions) => {
return {
disableAwsContextPropagation: true,
..._options,
};

return new AwsLambdaInstrumentation({
...options,
eventContextExtractor,
requestHook(span) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.aws.lambda');
},
});
};
},
);

Expand Down
21 changes: 12 additions & 9 deletions packages/node/src/integrations/node-fetch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, getClient } from '
import { generateInstrumentOnce } from '../../otel/instrument';
import type { NodeClient } from '../../sdk/client';
import type { NodeClientOptions } from '../../types';
import type { SentryNodeFetchInstrumentationOptions } from './SentryNodeFetchInstrumentation';
import { SentryNodeFetchInstrumentation } from './SentryNodeFetchInstrumentation';

const INTEGRATION_NAME = 'NodeFetch';
Expand Down Expand Up @@ -33,14 +32,19 @@ interface NodeFetchOptions {
ignoreOutgoingRequests?: (url: string) => boolean;
}

const instrumentOtelNodeFetch = generateInstrumentOnce<UndiciInstrumentationConfig>(INTEGRATION_NAME, config => {
return new UndiciInstrumentation(config);
});
const instrumentOtelNodeFetch = generateInstrumentOnce(
INTEGRATION_NAME,
UndiciInstrumentation,
(options: NodeFetchOptions) => {
return getConfigWithDefaults(options);
},
);

const instrumentSentryNodeFetch = generateInstrumentOnce<SentryNodeFetchInstrumentationOptions>(
const instrumentSentryNodeFetch = generateInstrumentOnce(
`${INTEGRATION_NAME}.sentry`,
config => {
return new SentryNodeFetchInstrumentation(config);
SentryNodeFetchInstrumentation,
(options: NodeFetchOptions) => {
return options;
},
);

Expand All @@ -52,8 +56,7 @@ const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => {

// This is the "regular" OTEL instrumentation that emits spans
if (instrumentSpans) {
const instrumentationConfig = getConfigWithDefaults(options);
instrumentOtelNodeFetch(instrumentationConfig);
instrumentOtelNodeFetch(options);
}

// This is the Sentry-specific instrumentation that creates breadcrumbs & propagates traces
Expand Down
9 changes: 5 additions & 4 deletions packages/node/src/integrations/tracing/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ interface GraphqlOptions {

const INTEGRATION_NAME = 'Graphql';

export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
export const instrumentGraphql = generateInstrumentOnce(
INTEGRATION_NAME,
(_options: GraphqlOptions = {}) => {
GraphQLInstrumentation,
(_options: GraphqlOptions) => {
const options = getOptionsWithDefaults(_options);

return new GraphQLInstrumentation({
return {
...options,
responseHook(span) {
addOriginToSpan(span, 'auto.graphql.otel.graphql');
Expand Down Expand Up @@ -73,7 +74,7 @@ export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
}
}
},
});
};
},
);

Expand Down
73 changes: 69 additions & 4 deletions packages/node/src/otel/instrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,47 @@ import { type Instrumentation, registerInstrumentations } from '@opentelemetry/i
/** Exported only for tests. */
export const INSTRUMENTED: Record<string, Instrumentation> = {};

/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string };
export function generateInstrumentOnce<
Options = unknown,
InstrumentationInstance extends Instrumentation = Instrumentation,
>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string };
/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<Options>(
name: string,
creatorOrClass: (new (...args: any[]) => Instrumentation) | ((options?: Options) => Instrumentation),
optionsCallback?: (options: Options) => unknown,
): ((options: Options) => Instrumentation) & { id: string } {
if (optionsCallback) {
return _generateInstrumentOnceWithOptions(
name,
creatorOrClass as new (...args: unknown[]) => Instrumentation,
optionsCallback,
);
}

return _generateInstrumentOnce(name, creatorOrClass as (options?: Options) => Instrumentation);
}

// The plain version without handling of options
// Should not be used with custom options that are mutated in the creator!
function _generateInstrumentOnce<Options = unknown, InstrumentationInstance extends Instrumentation = Instrumentation>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string } {
return Object.assign(
(options?: Options) => {
Expand All @@ -38,6 +69,40 @@ export function generateInstrumentOnce<
);
}

// This version handles options properly
function _generateInstrumentOnceWithOptions<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string } {
return Object.assign(
(_options: Options) => {
const options = optionsCallback(_options);

const instrumented = INSTRUMENTED[name] as InstanceType<InstrumentationClass> | undefined;
if (instrumented) {
// Ensure we update options
instrumented.setConfig(options);
return instrumented;
}

const instrumentation = new instrumentationClass(options) as InstanceType<InstrumentationClass>;
INSTRUMENTED[name] = instrumentation;

registerInstrumentations({
instrumentations: [instrumentation],
});

return instrumentation;
},
{ id: name },
);
}

/**
* Ensure a given callback is called when the instrumentation is actually wrapping something.
* This can be used to ensure some logic is only called when the instrumentation is actually active.
Expand Down
20 changes: 11 additions & 9 deletions packages/remix/src/server/integrations/opentelemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,24 @@ import type { RemixOptions } from '../../utils/remixOptions';

const INTEGRATION_NAME = 'Remix';

const instrumentRemix = generateInstrumentOnce<RemixOptions>(
INTEGRATION_NAME,
(_options?: RemixOptions) =>
new RemixInstrumentation({
actionFormDataAttributes: _options?.sendDefaultPii ? _options?.captureActionFormDataKeys : undefined,
}),
);
interface RemixInstrumentationOptions {
actionFormDataAttributes?: Record<string, string | boolean>;
}

const instrumentRemix = generateInstrumentOnce(INTEGRATION_NAME, (options?: RemixInstrumentationOptions) => {
return new RemixInstrumentation(options);
});

const _remixIntegration = (() => {
return {
name: 'Remix',
setupOnce() {
const client = getClient();
const options = client?.getOptions();
const options = client?.getOptions() as RemixOptions | undefined;

instrumentRemix(options);
instrumentRemix({
actionFormDataAttributes: options?.sendDefaultPii ? options?.captureActionFormDataKeys : undefined,
});
},

setup(client: Client) {
Expand Down
Loading