Skip to content

Commit fb3ff50

Browse files
committed
feat(node): Add tracing without performance to Node Undici
fix type force sampling decision log this out remove console log
1 parent 86ffdf4 commit fb3ff50

File tree

3 files changed

+125
-63
lines changed

3 files changed

+125
-63
lines changed

packages/node/src/integrations/undici/index.ts

Lines changed: 93 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { Hub } from '@sentry/core';
2-
import type { EventProcessor, Integration } from '@sentry/types';
2+
import { getDynamicSamplingContextFromClient } from '@sentry/core';
3+
import type { EventProcessor, Integration, Span } from '@sentry/types';
34
import {
45
dynamicRequire,
56
dynamicSamplingContextToSentryBaggageHeader,
7+
generateSentryTraceHeader,
68
getSanitizedUrlString,
79
parseUrl,
810
stringMatchesSomePattern,
@@ -12,7 +14,13 @@ import { LRUMap } from 'lru_map';
1214
import type { NodeClient } from '../../client';
1315
import { NODE_VERSION } from '../../nodeVersion';
1416
import { isSentryRequest } from '../utils/http';
15-
import type { DiagnosticsChannel, RequestCreateMessage, RequestEndMessage, RequestErrorMessage } from './types';
17+
import type {
18+
DiagnosticsChannel,
19+
RequestCreateMessage,
20+
RequestEndMessage,
21+
RequestErrorMessage,
22+
RequestWithSentry,
23+
} from './types';
1624

1725
export enum ChannelName {
1826
// https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md#undicirequestcreate
@@ -124,63 +132,53 @@ export class Undici implements Integration {
124132
const { request } = message as RequestCreateMessage;
125133

126134
const stringUrl = request.origin ? request.origin.toString() + request.path : request.path;
127-
const url = parseUrl(stringUrl);
128135

129-
if (isSentryRequest(stringUrl) || request.__sentry__ !== undefined) {
136+
if (isSentryRequest(stringUrl) || request.__sentry_span__ !== undefined) {
130137
return;
131138
}
132139

133140
const client = hub.getClient<NodeClient>();
141+
if (!client) {
142+
return;
143+
}
144+
145+
const clientOptions = client.getOptions();
134146
const scope = hub.getScope();
135147

136-
const activeSpan = scope.getSpan();
137-
138-
if (activeSpan && client) {
139-
const clientOptions = client.getOptions();
140-
141-
if (shouldCreateSpan(stringUrl)) {
142-
const method = request.method || 'GET';
143-
const data: Record<string, unknown> = {
144-
'http.method': method,
145-
};
146-
if (url.search) {
147-
data['http.query'] = url.search;
148-
}
149-
if (url.hash) {
150-
data['http.fragment'] = url.hash;
151-
}
152-
const span = activeSpan.startChild({
153-
op: 'http.client',
154-
description: `${method} ${getSanitizedUrlString(url)}`,
155-
data,
156-
});
157-
request.__sentry__ = span;
158-
159-
const shouldAttachTraceData = (url: string): boolean => {
160-
if (clientOptions.tracePropagationTargets === undefined) {
161-
return true;
162-
}
163-
164-
const cachedDecision = this._headersUrlMap.get(url);
165-
if (cachedDecision !== undefined) {
166-
return cachedDecision;
167-
}
168-
169-
const decision = stringMatchesSomePattern(url, clientOptions.tracePropagationTargets);
170-
this._headersUrlMap.set(url, decision);
171-
return decision;
172-
};
173-
174-
if (shouldAttachTraceData(stringUrl)) {
175-
request.addHeader('sentry-trace', span.toTraceparent());
176-
if (span.transaction) {
177-
const dynamicSamplingContext = span.transaction.getDynamicSamplingContext();
178-
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
179-
if (sentryBaggageHeader) {
180-
request.addHeader('baggage', sentryBaggageHeader);
181-
}
182-
}
183-
}
148+
const parentSpan = scope.getSpan();
149+
150+
const span = shouldCreateSpan(stringUrl) ? createRequestSpan(parentSpan, request, stringUrl) : undefined;
151+
if (span) {
152+
request.__sentry_span__ = span;
153+
}
154+
155+
const shouldAttachTraceData = (url: string): boolean => {
156+
if (clientOptions.tracePropagationTargets === undefined) {
157+
return true;
158+
}
159+
160+
const cachedDecision = this._headersUrlMap.get(url);
161+
if (cachedDecision !== undefined) {
162+
return cachedDecision;
163+
}
164+
165+
const decision = stringMatchesSomePattern(url, clientOptions.tracePropagationTargets);
166+
this._headersUrlMap.set(url, decision);
167+
return decision;
168+
};
169+
170+
if (shouldAttachTraceData(stringUrl)) {
171+
if (span) {
172+
const dynamicSamplingContext = span?.transaction?.getDynamicSamplingContext();
173+
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
174+
175+
setHeadersOnRequest(request, span.toTraceparent(), sentryBaggageHeader);
176+
} else {
177+
const { traceId, sampled, dsc } = scope.getPropagationContext();
178+
const sentryTrace = generateSentryTraceHeader(traceId, undefined, sampled);
179+
const dynamicSamplingContext = dsc || getDynamicSamplingContextFromClient(traceId, client, scope);
180+
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
181+
setHeadersOnRequest(request, sentryTrace, sentryBaggageHeader);
184182
}
185183
}
186184
});
@@ -199,7 +197,7 @@ export class Undici implements Integration {
199197
return;
200198
}
201199

202-
const span = request.__sentry__;
200+
const span = request.__sentry_span__;
203201
if (span) {
204202
span.setHttpStatus(response.statusCode);
205203
span.finish();
@@ -239,7 +237,7 @@ export class Undici implements Integration {
239237
return;
240238
}
241239

242-
const span = request.__sentry__;
240+
const span = request.__sentry_span__;
243241
if (span) {
244242
span.setStatus('internal_error');
245243
span.finish();
@@ -265,3 +263,44 @@ export class Undici implements Integration {
265263
});
266264
}
267265
}
266+
267+
function setHeadersOnRequest(
268+
request: RequestWithSentry,
269+
sentryTrace: string,
270+
sentryBaggageHeader: string | undefined,
271+
): void {
272+
if (request.__sentry_has_headers__) {
273+
return;
274+
}
275+
276+
request.addHeader('sentry-trace', sentryTrace);
277+
if (sentryBaggageHeader) {
278+
request.addHeader('baggage', sentryBaggageHeader);
279+
}
280+
281+
request.__sentry_has_headers__ = true;
282+
}
283+
284+
function createRequestSpan(
285+
activeSpan: Span | undefined,
286+
request: RequestWithSentry,
287+
stringUrl: string,
288+
): Span | undefined {
289+
const url = parseUrl(stringUrl);
290+
291+
const method = request.method || 'GET';
292+
const data: Record<string, unknown> = {
293+
'http.method': method,
294+
};
295+
if (url.search) {
296+
data['http.query'] = url.search;
297+
}
298+
if (url.hash) {
299+
data['http.fragment'] = url.hash;
300+
}
301+
return activeSpan?.startChild({
302+
op: 'http.client',
303+
description: `${method} ${getSanitizedUrlString(url)}`,
304+
data,
305+
});
306+
}

packages/node/src/integrations/undici/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,8 @@ export interface UndiciResponse {
234234
}
235235

236236
export interface RequestWithSentry extends UndiciRequest {
237-
__sentry__?: Span;
237+
__sentry_span__?: Span;
238+
__sentry_has_headers__?: boolean;
238239
}
239240

240241
export interface RequestCreateMessage {

packages/node/test/integrations/undici.test.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ beforeAll(async () => {
2828

2929
const DEFAULT_OPTIONS = getDefaultNodeClientOptions({
3030
dsn: SENTRY_DSN,
31-
tracesSampleRate: 1,
31+
tracesSampler: () => true,
3232
integrations: [new Undici()],
3333
});
3434

@@ -193,7 +193,7 @@ conditionalTest({ min: 16 })('Undici integration', () => {
193193
undoPatch();
194194
});
195195

196-
it('attaches the sentry trace and baggage headers', async () => {
196+
it('attaches the sentry trace and baggage headers if there is an active span', async () => {
197197
const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction;
198198
hub.getScope().setSpan(transaction);
199199

@@ -208,22 +208,44 @@ conditionalTest({ min: 16 })('Undici integration', () => {
208208
);
209209
});
210210

211-
it('does not attach headers if `shouldCreateSpanForRequest` does not create a span', async () => {
211+
it('attaches the sentry trace and baggage headers if there is no active span', async () => {
212+
await fetch('http://localhost:18099', { method: 'POST' });
213+
214+
const propagationContext = hub.getScope().getPropagationContext();
215+
216+
expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(true);
217+
expect(requestHeaders['baggage']).toEqual(
218+
`sentry-environment=production,sentry-public_key=0,sentry-trace_id=${propagationContext.traceId}`,
219+
);
220+
});
221+
222+
it('attaches headers if `shouldCreateSpanForRequest` does not create a span using propagation context', async () => {
212223
const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction;
213-
hub.getScope().setSpan(transaction);
224+
const scope = hub.getScope();
225+
const propagationContext = scope.getPropagationContext();
226+
227+
scope.setSpan(transaction);
214228

215229
const undoPatch = patchUndici(hub, { shouldCreateSpanForRequest: url => url.includes('yes') });
216230

217231
await fetch('http://localhost:18099/no', { method: 'POST' });
218232

219-
expect(requestHeaders['sentry-trace']).toBeUndefined();
220-
expect(requestHeaders['baggage']).toBeUndefined();
233+
expect(requestHeaders['sentry-trace']).toBeDefined();
234+
expect(requestHeaders['baggage']).toBeDefined();
235+
236+
expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(true);
237+
const firstSpanId = requestHeaders['sentry-trace'].split('-')[1];
221238

222239
await fetch('http://localhost:18099/yes', { method: 'POST' });
223240

224241
expect(requestHeaders['sentry-trace']).toBeDefined();
225242
expect(requestHeaders['baggage']).toBeDefined();
226243

244+
expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(false);
245+
246+
const secondSpanId = requestHeaders['sentry-trace'].split('-')[1];
247+
expect(firstSpanId).not.toBe(secondSpanId);
248+
227249
undoPatch();
228250
});
229251

@@ -351,10 +373,10 @@ function setupTestServer() {
351373
res.end();
352374

353375
// also terminate socket because keepalive hangs connection a bit
354-
res.connection.end();
376+
res.connection?.end();
355377
});
356378

357-
testServer.listen(18099, 'localhost');
379+
testServer?.listen(18099, 'localhost');
358380

359381
return new Promise(resolve => {
360382
testServer?.on('listening', resolve);

0 commit comments

Comments
 (0)