Skip to content

Commit f67c787

Browse files
committed
feat(node): Add tracing without performance to Node http integration
1 parent dae3475 commit f67c787

File tree

2 files changed

+146
-99
lines changed

2 files changed

+146
-99
lines changed

packages/node/src/integrations/http.ts

Lines changed: 126 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import type { Hub } from '@sentry/core';
2-
import { getCurrentHub } from '@sentry/core';
3-
import type { EventProcessor, Integration, SanitizedRequestData, Span, TracePropagationTargets } from '@sentry/types';
4-
import { dynamicSamplingContextToSentryBaggageHeader, fill, logger, stringMatchesSomePattern } from '@sentry/utils';
2+
import { getCurrentHub, getDynamicSamplingContextFromClient } from '@sentry/core';
3+
import type { EventProcessor, Integration, SanitizedRequestData, TracePropagationTargets } from '@sentry/types';
4+
import {
5+
dynamicSamplingContextToSentryBaggageHeader,
6+
fill,
7+
generateSentryTraceHeader,
8+
logger,
9+
stringMatchesSomePattern,
10+
} from '@sentry/utils';
511
import type * as http from 'http';
612
import type * as https from 'https';
713
import { LRUMap } from 'lru_map';
814

915
import type { NodeClient } from '../client';
1016
import { NODE_VERSION } from '../nodeVersion';
11-
import type { RequestMethod, RequestMethodArgs } from './utils/http';
17+
import type { RequestMethod, RequestMethodArgs, RequestOptions } from './utils/http';
1218
import { cleanSpanDescription, extractRawUrl, extractUrl, isSentryRequest, normalizeRequestArgs } from './utils/http';
1319

1420
interface TracingOptions {
@@ -178,6 +184,36 @@ function _createWrappedRequestMethodFactory(
178184
return decision;
179185
};
180186

187+
/**
188+
* Captures Breadcrumb based on provided request/response pair
189+
*/
190+
function addRequestBreadcrumb(
191+
event: string,
192+
requestSpanData: SanitizedRequestData,
193+
req: http.ClientRequest,
194+
res?: http.IncomingMessage,
195+
): void {
196+
if (!getCurrentHub().getIntegration(Http)) {
197+
return;
198+
}
199+
200+
getCurrentHub().addBreadcrumb(
201+
{
202+
category: 'http',
203+
data: {
204+
status_code: res && res.statusCode,
205+
...requestSpanData,
206+
},
207+
type: 'http',
208+
},
209+
{
210+
event,
211+
request: req,
212+
response: res,
213+
},
214+
);
215+
}
216+
181217
return function wrappedRequestMethodFactory(originalRequestMethod: OriginalRequestMethod): WrappedRequestMethod {
182218
return function wrappedMethod(this: unknown, ...args: RequestMethodArgs): http.ClientRequest {
183219
const requestArgs = normalizeRequestArgs(httpModule, args);
@@ -191,74 +227,66 @@ function _createWrappedRequestMethodFactory(
191227
return originalRequestMethod.apply(httpModule, requestArgs);
192228
}
193229

194-
let requestSpan: Span | undefined;
195-
const parentSpan = getCurrentHub().getScope().getSpan();
196-
197-
const method = requestOptions.method || 'GET';
198-
const requestSpanData: SanitizedRequestData = {
199-
url: requestUrl,
200-
'http.method': method,
201-
};
202-
if (requestOptions.hash) {
203-
// strip leading "#"
204-
requestSpanData['http.fragment'] = requestOptions.hash.substring(1);
205-
}
206-
if (requestOptions.search) {
207-
// strip leading "?"
208-
requestSpanData['http.query'] = requestOptions.search.substring(1);
209-
}
230+
const hub = getCurrentHub();
231+
const scope = hub.getScope();
232+
const parentSpan = scope.getSpan();
210233

211-
if (tracingOptions && shouldCreateSpan(rawRequestUrl)) {
212-
if (parentSpan) {
213-
requestSpan = parentSpan.startChild({
214-
description: `${method} ${requestSpanData.url}`,
215-
op: 'http.client',
216-
data: requestSpanData,
217-
});
218-
219-
if (shouldAttachTraceData(rawRequestUrl)) {
220-
const sentryTraceHeader = requestSpan.toTraceparent();
221-
__DEBUG_BUILD__ &&
222-
logger.log(
223-
`[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to "${requestUrl}": `,
224-
);
234+
const data = getRequestSpanData(requestUrl, requestOptions);
225235

236+
const requestSpan = shouldCreateSpan(rawRequestUrl)
237+
? parentSpan?.startChild({
238+
op: 'http.client',
239+
description: `${data['http.method']} ${data.url}`,
240+
data,
241+
})
242+
: undefined;
243+
244+
if (shouldAttachTraceData(rawRequestUrl)) {
245+
if (requestSpan) {
246+
const sentryTraceHeader = requestSpan.toTraceparent();
247+
__DEBUG_BUILD__ &&
248+
logger.log(
249+
`[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to "${requestUrl}": `,
250+
);
251+
const dynamicSamplingContext = requestSpan?.transaction?.getDynamicSamplingContext();
252+
const sentryBaggageHeader = normalizeBaggageHeader(
253+
requestOptions,
254+
dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext),
255+
);
256+
257+
requestOptions.headers = {
258+
...requestOptions.headers,
259+
'sentry-trace': sentryTraceHeader,
260+
// Setting a header to `undefined` will crash in node so we only set the baggage header when it's defined
261+
...(sentryBaggageHeader && { baggage: sentryBaggageHeader }),
262+
};
263+
} else {
264+
const { traceId, sampled, dsc } = scope.getPropagationContext();
265+
const sentryTraceHeader = generateSentryTraceHeader(traceId, undefined, sampled);
266+
__DEBUG_BUILD__ &&
267+
logger.log(
268+
`[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to "${requestUrl}": `,
269+
);
270+
requestOptions.headers = {
271+
...requestOptions.headers,
272+
'sentry-trace': sentryTraceHeader,
273+
};
274+
const client = hub.getClient();
275+
if (client) {
276+
const dynamicSamplingContext = dsc || getDynamicSamplingContextFromClient(traceId, client, scope);
277+
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
226278
requestOptions.headers = {
227279
...requestOptions.headers,
228-
'sentry-trace': sentryTraceHeader,
280+
// Setting a header to `undefined` will crash in node so we only set the baggage header when it's defined
281+
...(sentryBaggageHeader && { baggage: sentryBaggageHeader }),
229282
};
230-
231-
if (parentSpan.transaction) {
232-
const dynamicSamplingContext = parentSpan.transaction.getDynamicSamplingContext();
233-
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
234-
235-
let newBaggageHeaderField;
236-
if (!requestOptions.headers || !requestOptions.headers.baggage) {
237-
newBaggageHeaderField = sentryBaggageHeader;
238-
} else if (!sentryBaggageHeader) {
239-
newBaggageHeaderField = requestOptions.headers.baggage;
240-
} else if (Array.isArray(requestOptions.headers.baggage)) {
241-
newBaggageHeaderField = [...requestOptions.headers.baggage, sentryBaggageHeader];
242-
} else {
243-
// Type-cast explanation:
244-
// Technically this the following could be of type `(number | string)[]` but for the sake of simplicity
245-
// we say this is undefined behaviour, since it would not be baggage spec conform if the user did this.
246-
newBaggageHeaderField = [requestOptions.headers.baggage, sentryBaggageHeader] as string[];
247-
}
248-
249-
requestOptions.headers = {
250-
...requestOptions.headers,
251-
// Setting a hader to `undefined` will crash in node so we only set the baggage header when it's defined
252-
...(newBaggageHeaderField && { baggage: newBaggageHeaderField }),
253-
};
254-
}
255-
} else {
256-
__DEBUG_BUILD__ &&
257-
logger.log(
258-
`[Tracing] Not adding sentry-trace header to outgoing request (${requestUrl}) due to mismatching tracePropagationTargets option.`,
259-
);
260283
}
261284
}
285+
} else {
286+
__DEBUG_BUILD__ &&
287+
logger.log(
288+
`[Tracing] Not adding sentry-trace header to outgoing request (${requestUrl}) due to mismatching tracePropagationTargets option.`,
289+
);
262290
}
263291

264292
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
@@ -268,7 +296,7 @@ function _createWrappedRequestMethodFactory(
268296
// eslint-disable-next-line @typescript-eslint/no-this-alias
269297
const req = this;
270298
if (breadcrumbsEnabled) {
271-
addRequestBreadcrumb('response', requestSpanData, req, res);
299+
addRequestBreadcrumb('response', data, req, res);
272300
}
273301
if (requestSpan) {
274302
if (res.statusCode) {
@@ -283,7 +311,7 @@ function _createWrappedRequestMethodFactory(
283311
const req = this;
284312

285313
if (breadcrumbsEnabled) {
286-
addRequestBreadcrumb('error', requestSpanData, req);
314+
addRequestBreadcrumb('error', data, req);
287315
}
288316
if (requestSpan) {
289317
requestSpan.setHttpStatus(500);
@@ -295,32 +323,37 @@ function _createWrappedRequestMethodFactory(
295323
};
296324
}
297325

298-
/**
299-
* Captures Breadcrumb based on provided request/response pair
300-
*/
301-
function addRequestBreadcrumb(
302-
event: string,
303-
requestSpanData: SanitizedRequestData,
304-
req: http.ClientRequest,
305-
res?: http.IncomingMessage,
306-
): void {
307-
if (!getCurrentHub().getIntegration(Http)) {
308-
return;
326+
function getRequestSpanData(requestUrl: string, requestOptions: RequestOptions): SanitizedRequestData {
327+
const method = requestOptions.method || 'GET';
328+
const data: SanitizedRequestData = {
329+
url: requestUrl,
330+
'http.method': method,
331+
};
332+
if (requestOptions.hash) {
333+
// strip leading "#"
334+
data['http.fragment'] = requestOptions.hash.substring(1);
335+
}
336+
if (requestOptions.search) {
337+
// strip leading "?"
338+
data['http.query'] = requestOptions.search.substring(1);
339+
}
340+
return data;
341+
}
342+
343+
function normalizeBaggageHeader(
344+
requestOptions: RequestOptions,
345+
sentryBaggageHeader: string | undefined,
346+
): string | number | string[] | undefined {
347+
if (!requestOptions.headers || !requestOptions.headers.baggage) {
348+
return sentryBaggageHeader;
349+
} else if (!sentryBaggageHeader) {
350+
return requestOptions.headers.baggage;
351+
} else if (Array.isArray(requestOptions.headers.baggage)) {
352+
return [...requestOptions.headers.baggage, sentryBaggageHeader];
309353
}
310354

311-
getCurrentHub().addBreadcrumb(
312-
{
313-
category: 'http',
314-
data: {
315-
status_code: res && res.statusCode,
316-
...requestSpanData,
317-
},
318-
type: 'http',
319-
},
320-
{
321-
event,
322-
request: req,
323-
response: res,
324-
},
325-
);
355+
// Type-cast explanation:
356+
// Technically this the following could be of type `(number | string)[]` but for the sake of simplicity
357+
// we say this is undefined behaviour, since it would not be baggage spec conform if the user did this.
358+
return [requestOptions.headers.baggage, sentryBaggageHeader] as string[];
326359
}

packages/node/test/integrations/http.test.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ describe('tracing', () => {
272272

273273
// TODO (v8): These can be removed once we remove these properties from client options
274274
describe('as client options', () => {
275-
it("doesn't create span if shouldCreateSpanForRequest returns false", () => {
275+
it('creates span with propagation context if shouldCreateSpanForRequest returns false', () => {
276276
const url = 'http://dogs.are.great/api/v1/index/';
277277
nock(url).get(/.*/).reply(200);
278278

@@ -295,8 +295,15 @@ describe('tracing', () => {
295295
expect(httpSpans.length).toBe(0);
296296

297297
// And headers are not attached without span creation
298-
expect(request.getHeader('sentry-trace')).toBeUndefined();
299-
expect(request.getHeader('baggage')).toBeUndefined();
298+
expect(request.getHeader('sentry-trace')).toBeDefined();
299+
expect(request.getHeader('baggage')).toBeDefined();
300+
301+
const propagationContext = hub.getScope().getPropagationContext();
302+
303+
expect((request.getHeader('sentry-trace') as string).includes(propagationContext.traceId)).toBe(true);
304+
expect(request.getHeader('baggage')).toEqual(
305+
`sentry-environment=production,sentry-release=1.0.0,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=${propagationContext.traceId}`,
306+
);
300307
});
301308

302309
it.each([
@@ -366,7 +373,7 @@ describe('tracing', () => {
366373
});
367374

368375
describe('as Http integration constructor options', () => {
369-
it("doesn't create span if shouldCreateSpanForRequest returns false", () => {
376+
it('creates span with propagation context if shouldCreateSpanForRequest returns false', () => {
370377
const url = 'http://dogs.are.great/api/v1/index/';
371378
nock(url).get(/.*/).reply(200);
372379

@@ -393,8 +400,15 @@ describe('tracing', () => {
393400
expect(httpSpans.length).toBe(0);
394401

395402
// And headers are not attached without span creation
396-
expect(request.getHeader('sentry-trace')).toBeUndefined();
397-
expect(request.getHeader('baggage')).toBeUndefined();
403+
expect(request.getHeader('sentry-trace')).toBeDefined();
404+
expect(request.getHeader('baggage')).toBeDefined();
405+
406+
const propagationContext = hub.getScope().getPropagationContext();
407+
408+
expect((request.getHeader('sentry-trace') as string).includes(propagationContext.traceId)).toBe(true);
409+
expect(request.getHeader('baggage')).toEqual(
410+
`sentry-environment=production,sentry-release=1.0.0,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=${propagationContext.traceId}`,
411+
);
398412
});
399413

400414
it.each([

0 commit comments

Comments
 (0)