Skip to content

Commit 41489e7

Browse files
committed
feat(node): Add tracing without performance to Node Undici
1 parent 99a0864 commit 41489e7

File tree

3 files changed

+268
-199
lines changed

3 files changed

+268
-199
lines changed
Lines changed: 184 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import type { Hub } from '@sentry/core';
2-
import type { EventProcessor, Integration } from '@sentry/types';
1+
import { getCurrentHub, getDynamicSamplingContextFromClient } from '@sentry/core';
2+
import type { EventProcessor, Integration, Span } from '@sentry/types';
33
import {
44
dynamicRequire,
55
dynamicSamplingContextToSentryBaggageHeader,
6+
generateSentryTraceHeader,
67
getSanitizedUrlString,
78
parseUrl,
89
stringMatchesSomePattern,
@@ -12,7 +13,13 @@ import { LRUMap } from 'lru_map';
1213
import type { NodeClient } from '../../client';
1314
import { NODE_VERSION } from '../../nodeVersion';
1415
import { isSentryRequest } from '../utils/http';
15-
import type { DiagnosticsChannel, RequestCreateMessage, RequestEndMessage, RequestErrorMessage } from './types';
16+
import type {
17+
DiagnosticsChannel,
18+
RequestCreateMessage,
19+
RequestEndMessage,
20+
RequestErrorMessage,
21+
RequestWithSentry,
22+
} from './types';
1623

1724
export enum ChannelName {
1825
// https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md#undicirequestcreate
@@ -81,7 +88,7 @@ export class Undici implements Integration {
8188
/**
8289
* @inheritDoc
8390
*/
84-
public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
91+
public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void): void {
8592
// Requires Node 16+ to use the diagnostics_channel API.
8693
if (NODE_VERSION.major && NODE_VERSION.major < 16) {
8794
return;
@@ -99,169 +106,205 @@ export class Undici implements Integration {
99106
return;
100107
}
101108

102-
const shouldCreateSpan = (url: string): boolean => {
103-
if (this._options.shouldCreateSpanForRequest === undefined) {
109+
// https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md
110+
ds.subscribe(ChannelName.RequestCreate, this._onRequestCreate);
111+
ds.subscribe(ChannelName.RequestEnd, this._onRequestEnd);
112+
ds.subscribe(ChannelName.RequestError, this._onRequestError);
113+
}
114+
115+
/** Helper that wraps shouldCreateSpanForRequest option */
116+
private _shouldCreateSpan(url: string): boolean {
117+
if (this._options.shouldCreateSpanForRequest === undefined) {
118+
return true;
119+
}
120+
121+
const cachedDecision = this._createSpanUrlMap.get(url);
122+
if (cachedDecision !== undefined) {
123+
return cachedDecision;
124+
}
125+
126+
const decision = this._options.shouldCreateSpanForRequest(url);
127+
this._createSpanUrlMap.set(url, decision);
128+
return decision;
129+
}
130+
131+
private _onRequestCreate = (message: unknown): void => {
132+
const hub = getCurrentHub();
133+
if (!hub.getIntegration(Undici)) {
134+
return;
135+
}
136+
137+
const { request } = message as RequestCreateMessage;
138+
139+
const stringUrl = request.origin ? request.origin.toString() + request.path : request.path;
140+
141+
if (isSentryRequest(stringUrl) || request.__sentry_span__ !== undefined) {
142+
return;
143+
}
144+
145+
const client = hub.getClient<NodeClient>();
146+
if (!client) {
147+
return;
148+
}
149+
150+
const clientOptions = client.getOptions();
151+
const scope = hub.getScope();
152+
153+
const parentSpan = scope.getSpan();
154+
155+
const span = this._shouldCreateSpan(stringUrl) ? createRequestSpan(parentSpan, request, stringUrl) : undefined;
156+
if (span) {
157+
request.__sentry_span__ = span;
158+
}
159+
160+
const shouldAttachTraceData = (url: string): boolean => {
161+
if (clientOptions.tracePropagationTargets === undefined) {
104162
return true;
105163
}
106164

107-
const cachedDecision = this._createSpanUrlMap.get(url);
165+
const cachedDecision = this._headersUrlMap.get(url);
108166
if (cachedDecision !== undefined) {
109167
return cachedDecision;
110168
}
111169

112-
const decision = this._options.shouldCreateSpanForRequest(url);
113-
this._createSpanUrlMap.set(url, decision);
170+
const decision = stringMatchesSomePattern(url, clientOptions.tracePropagationTargets);
171+
this._headersUrlMap.set(url, decision);
114172
return decision;
115173
};
116174

117-
// https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md
118-
ds.subscribe(ChannelName.RequestCreate, message => {
119-
const hub = getCurrentHub();
120-
if (!hub.getIntegration(Undici)) {
121-
return;
175+
if (shouldAttachTraceData(stringUrl)) {
176+
if (span) {
177+
const dynamicSamplingContext = span?.transaction?.getDynamicSamplingContext();
178+
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
179+
180+
setHeadersOnRequest(request, span.toTraceparent(), sentryBaggageHeader);
181+
} else {
182+
const { traceId, sampled, dsc } = scope.getPropagationContext();
183+
const sentryTrace = generateSentryTraceHeader(traceId, undefined, sampled);
184+
const dynamicSamplingContext = dsc || getDynamicSamplingContextFromClient(traceId, client, scope);
185+
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
186+
setHeadersOnRequest(request, sentryTrace, sentryBaggageHeader);
122187
}
188+
}
189+
};
123190

124-
const { request } = message as RequestCreateMessage;
191+
private _onRequestEnd = (message: unknown): void => {
192+
const hub = getCurrentHub();
193+
if (!hub.getIntegration(Undici)) {
194+
return;
195+
}
125196

126-
const stringUrl = request.origin ? request.origin.toString() + request.path : request.path;
127-
const url = parseUrl(stringUrl);
197+
const { request, response } = message as RequestEndMessage;
128198

129-
if (isSentryRequest(stringUrl) || request.__sentry__ !== undefined) {
130-
return;
131-
}
199+
const stringUrl = request.origin ? request.origin.toString() + request.path : request.path;
132200

133-
const client = hub.getClient<NodeClient>();
134-
const scope = hub.getScope();
135-
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-
}
184-
}
185-
}
186-
});
201+
if (isSentryRequest(stringUrl)) {
202+
return;
203+
}
187204

188-
ds.subscribe(ChannelName.RequestEnd, message => {
189-
const hub = getCurrentHub();
190-
if (!hub.getIntegration(Undici)) {
191-
return;
192-
}
205+
const span = request.__sentry_span__;
206+
if (span) {
207+
span.setHttpStatus(response.statusCode);
208+
span.finish();
209+
}
193210

194-
const { request, response } = message as RequestEndMessage;
211+
if (this._options.breadcrumbs) {
212+
hub.addBreadcrumb(
213+
{
214+
category: 'http',
215+
data: {
216+
method: request.method,
217+
status_code: response.statusCode,
218+
url: stringUrl,
219+
},
220+
type: 'http',
221+
},
222+
{
223+
event: 'response',
224+
request,
225+
response,
226+
},
227+
);
228+
}
229+
};
195230

196-
const stringUrl = request.origin ? request.origin.toString() + request.path : request.path;
231+
private _onRequestError = (message: unknown): void => {
232+
const hub = getCurrentHub();
233+
if (!hub.getIntegration(Undici)) {
234+
return;
235+
}
197236

198-
if (isSentryRequest(stringUrl)) {
199-
return;
200-
}
237+
const { request } = message as RequestErrorMessage;
201238

202-
const span = request.__sentry__;
203-
if (span) {
204-
span.setHttpStatus(response.statusCode);
205-
span.finish();
206-
}
239+
const stringUrl = request.origin ? request.origin.toString() + request.path : request.path;
207240

208-
if (this._options.breadcrumbs) {
209-
hub.addBreadcrumb(
210-
{
211-
category: 'http',
212-
data: {
213-
method: request.method,
214-
status_code: response.statusCode,
215-
url: stringUrl,
216-
},
217-
type: 'http',
218-
},
219-
{
220-
event: 'response',
221-
request,
222-
response,
223-
},
224-
);
225-
}
226-
});
241+
if (isSentryRequest(stringUrl)) {
242+
return;
243+
}
227244

228-
ds.subscribe(ChannelName.RequestError, message => {
229-
const hub = getCurrentHub();
230-
if (!hub.getIntegration(Undici)) {
231-
return;
232-
}
245+
const span = request.__sentry_span__;
246+
if (span) {
247+
span.setStatus('internal_error');
248+
span.finish();
249+
}
233250

234-
const { request } = message as RequestErrorMessage;
251+
if (this._options.breadcrumbs) {
252+
hub.addBreadcrumb(
253+
{
254+
category: 'http',
255+
data: {
256+
method: request.method,
257+
url: stringUrl,
258+
},
259+
level: 'error',
260+
type: 'http',
261+
},
262+
{
263+
event: 'error',
264+
request,
265+
},
266+
);
267+
}
268+
};
269+
}
235270

236-
const stringUrl = request.origin ? request.origin.toString() + request.path : request.path;
271+
function setHeadersOnRequest(
272+
request: RequestWithSentry,
273+
sentryTrace: string,
274+
sentryBaggageHeader: string | undefined,
275+
): void {
276+
if (request.__sentry_has_headers__) {
277+
return;
278+
}
237279

238-
if (isSentryRequest(stringUrl)) {
239-
return;
240-
}
280+
request.addHeader('sentry-trace', sentryTrace);
281+
if (sentryBaggageHeader) {
282+
request.addHeader('baggage', sentryBaggageHeader);
283+
}
241284

242-
const span = request.__sentry__;
243-
if (span) {
244-
span.setStatus('internal_error');
245-
span.finish();
246-
}
285+
request.__sentry_has_headers__ = true;
286+
}
247287

248-
if (this._options.breadcrumbs) {
249-
hub.addBreadcrumb(
250-
{
251-
category: 'http',
252-
data: {
253-
method: request.method,
254-
url: stringUrl,
255-
},
256-
level: 'error',
257-
type: 'http',
258-
},
259-
{
260-
event: 'error',
261-
request,
262-
},
263-
);
264-
}
265-
});
288+
function createRequestSpan(
289+
activeSpan: Span | undefined,
290+
request: RequestWithSentry,
291+
stringUrl: string,
292+
): Span | undefined {
293+
const url = parseUrl(stringUrl);
294+
295+
const method = request.method || 'GET';
296+
const data: Record<string, unknown> = {
297+
'http.method': method,
298+
};
299+
if (url.search) {
300+
data['http.query'] = url.search;
301+
}
302+
if (url.hash) {
303+
data['http.fragment'] = url.hash;
266304
}
305+
return activeSpan?.startChild({
306+
op: 'http.client',
307+
description: `${method} ${getSanitizedUrlString(url)}`,
308+
data,
309+
});
267310
}

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 {

0 commit comments

Comments
 (0)