Skip to content

Commit 90d77ae

Browse files
committed
feat(node): WIP Only add span listeners conditionally
1 parent 85e59d3 commit 90d77ae

File tree

3 files changed

+65
-26
lines changed

3 files changed

+65
-26
lines changed

packages/node/src/integrations/tracing/connect.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ function addConnectSpanAttributes(span: Span): void {
104104
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.connect`,
105105
});
106106

107-
// Also update the name, we don't need to "middleware - " prefix
107+
// Also update the name, we don't need the "middleware - " prefix
108108
const name = attributes['connect.name'];
109109
if (typeof name === 'string') {
110110
span.updateName(name);

packages/node/src/integrations/tracing/dataloader.ts

+28-20
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import {
33
SEMANTIC_ATTRIBUTE_SENTRY_OP,
44
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
55
defineIntegration,
6+
getClient,
67
spanToJSON,
78
} from '@sentry/core';
89
import type { IntegrationFn } from '@sentry/core';
9-
import { generateInstrumentOnce } from '../../otel/instrument';
10+
import { callWhenWrapped, generateInstrumentOnce } from '../../otel/instrument';
1011

1112
const INTEGRATION_NAME = 'Dataloader';
1213

@@ -19,31 +20,38 @@ export const instrumentDataloader = generateInstrumentOnce(
1920
);
2021

2122
const _dataloaderIntegration = (() => {
23+
let hookCallback: undefined | (() => void);
24+
2225
return {
2326
name: INTEGRATION_NAME,
2427
setupOnce() {
25-
instrumentDataloader();
26-
},
28+
const instrumentation = instrumentDataloader();
2729

28-
setup(client) {
29-
client.on('spanStart', span => {
30-
const spanJSON = spanToJSON(span);
31-
if (spanJSON.description?.startsWith('dataloader')) {
32-
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.dataloader');
30+
callWhenWrapped(instrumentation, () => {
31+
const client = getClient();
32+
if (hookCallback || !client) {
33+
return;
3334
}
3435

35-
// These are all possible dataloader span descriptions
36-
// Still checking for the future versions
37-
// in case they add support for `clear` and `prime`
38-
if (
39-
spanJSON.description === 'dataloader.load' ||
40-
spanJSON.description === 'dataloader.loadMany' ||
41-
spanJSON.description === 'dataloader.batch'
42-
) {
43-
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'cache.get');
44-
// TODO: We can try adding `key` to the `data` attribute upstream.
45-
// Or alternatively, we can add `requestHook` to the dataloader instrumentation.
46-
}
36+
hookCallback = client.on('spanStart', span => {
37+
const spanJSON = spanToJSON(span);
38+
if (spanJSON.description?.startsWith('dataloader')) {
39+
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.dataloader');
40+
}
41+
42+
// These are all possible dataloader span descriptions
43+
// Still checking for the future versions
44+
// in case they add support for `clear` and `prime`
45+
if (
46+
spanJSON.description === 'dataloader.load' ||
47+
spanJSON.description === 'dataloader.loadMany' ||
48+
spanJSON.description === 'dataloader.batch'
49+
) {
50+
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'cache.get');
51+
// TODO: We can try adding `key` to the `data` attribute upstream.
52+
// Or alternatively, we can add `requestHook` to the dataloader instrumentation.
53+
}
54+
});
4755
});
4856
},
4957
};

packages/node/src/otel/instrument.ts

+36-5
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@ export const INSTRUMENTED: Record<string, Instrumentation> = {};
77
* Instrument an OpenTelemetry instrumentation once.
88
* This will skip running instrumentation again if it was already instrumented.
99
*/
10-
export function generateInstrumentOnce<Options = unknown>(
10+
export function generateInstrumentOnce<
11+
Options = unknown,
12+
InstrumentationInstance extends Instrumentation = Instrumentation,
13+
>(
1114
name: string,
12-
creator: (options?: Options) => Instrumentation,
13-
): ((options?: Options) => void) & { id: string } {
15+
creator: (options?: Options) => InstrumentationInstance,
16+
): ((options?: Options) => InstrumentationInstance) & { id: string } {
1417
return Object.assign(
1518
(options?: Options) => {
16-
const instrumented = INSTRUMENTED[name];
19+
const instrumented = INSTRUMENTED[name] as InstrumentationInstance | undefined;
1720
if (instrumented) {
1821
// If options are provided, ensure we update them
1922
if (options) {
2023
instrumented.setConfig(options);
2124
}
22-
return;
25+
return instrumented;
2326
}
2427

2528
const instrumentation = creator(options);
@@ -28,7 +31,35 @@ export function generateInstrumentOnce<Options = unknown>(
2831
registerInstrumentations({
2932
instrumentations: [instrumentation],
3033
});
34+
35+
return instrumentation;
3136
},
3237
{ id: name },
3338
);
3439
}
40+
41+
/**
42+
* Ensure a given callback is called when the instrumentation is actually wrapping something.
43+
* This can be used to ensure some logic is only called when the instrumentation is actually active.
44+
* This depends on wrapping `_wrap` (inception!). If this is not possible (e.g. the property name is mangled, ...)
45+
* the callback will be called immediately.
46+
*/
47+
export function callWhenWrapped<T extends Instrumentation>(instrumentation: T, callback: () => void): void {
48+
if (!hasWrap(instrumentation)) {
49+
callback();
50+
return;
51+
}
52+
53+
const originalWrap = instrumentation['_wrap'];
54+
55+
instrumentation['_wrap'] = (...args: Parameters<typeof originalWrap>) => {
56+
callback();
57+
return originalWrap(...args);
58+
};
59+
}
60+
61+
function hasWrap<T extends Instrumentation>(
62+
instrumentation: T,
63+
): instrumentation is T & { _wrap: (...args: unknown[]) => unknown } {
64+
return typeof (instrumentation as T & { _wrap?: (...args: unknown[]) => unknown })['_wrap'] === 'function';
65+
}

0 commit comments

Comments
 (0)