Skip to content

Commit ea43514

Browse files
committed
quite a bit more code
1 parent cf36d41 commit ea43514

File tree

4 files changed

+209
-9
lines changed

4 files changed

+209
-9
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ dist/
1111
coverage/
1212
scratch/
1313
*.d.ts
14-
!diagnostics_channel.d.ts
1514
*.js.map
1615
*.pyc
1716
*.tsbuildinfo

packages/node/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!diagnostics_channel.d.ts

packages/node/src/integrations/diagnostics_channel.d.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,21 @@ declare module 'diagnostics_channel' {
166166
*/
167167
public unsubscribe(onMessage: ChannelListener): void;
168168
}
169+
// https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/types/diagnostics-channel.d.ts
170+
interface Request {
171+
origin?: string | URL;
172+
completed: boolean;
173+
// Originally was Dispatcher.HttpMethod, but did not want to vendor that in.
174+
method?: string;
175+
path: string;
176+
headers: string;
177+
addHeader(key: string, value: string): Request;
178+
}
179+
interface Response {
180+
statusCode: number;
181+
statusText: string;
182+
headers: Array<Buffer>;
183+
}
169184
}
170185
declare module 'node:diagnostics_channel' {
171186
export * from 'diagnostics_channel';

packages/node/src/integrations/undici.ts

Lines changed: 193 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,49 @@
1-
import type { Hub } from '@sentry/core';
1+
import type { Hub, Span } from '@sentry/core';
2+
import { stripUrlQueryAndFragment } from '@sentry/core';
23
import type { EventProcessor, Integration } from '@sentry/types';
4+
import { dynamicSamplingContextToSentryBaggageHeader, stringMatchesSomePattern } from '@sentry/utils';
35
import type DiagnosticsChannel from 'diagnostics_channel';
46

7+
import type { NodeClient } from '../client';
8+
import { isSentryRequest } from './utils/http';
9+
10+
enum ChannelName {
11+
// https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md#undicirequestcreate
12+
RequestCreate = 'undici:request:create',
13+
RequestEnd = 'undici:request:headers',
14+
RequestError = 'undici:request:error',
15+
}
16+
17+
interface RequestWithSentry extends DiagnosticsChannel.Request {
18+
__sentry__?: Span;
19+
}
20+
21+
interface RequestCreateMessage {
22+
request: RequestWithSentry;
23+
}
24+
25+
interface RequestEndMessage {
26+
request: RequestWithSentry;
27+
response: DiagnosticsChannel.Response;
28+
}
29+
30+
interface RequestErrorMessage {
31+
request: RequestWithSentry;
32+
error: Error;
33+
}
34+
35+
interface UndiciOptions {
36+
/**
37+
* Whether breadcrumbs should be recorded for requests
38+
* Defaults to true
39+
*/
40+
breadcrumbs: boolean;
41+
}
42+
43+
const DEFAULT_UNDICI_OPTIONS: UndiciOptions = {
44+
breadcrumbs: true,
45+
};
46+
547
/** */
648
export class Undici implements Integration {
749
/**
@@ -17,12 +59,21 @@ export class Undici implements Integration {
1759
// Have to hold all built channels in memory otherwise they get garbage collected
1860
// See: https://github.com/nodejs/node/pull/42714
1961
// This has been fixed in Node 19+
20-
private _channels: Map<string, DiagnosticsChannel.Channel> = new Map();
62+
private _channels = new Set<DiagnosticsChannel.Channel>();
63+
64+
private readonly _options: UndiciOptions;
65+
66+
public constructor(_options: UndiciOptions) {
67+
this._options = {
68+
...DEFAULT_UNDICI_OPTIONS,
69+
..._options,
70+
};
71+
}
2172

2273
/**
2374
* @inheritDoc
2475
*/
25-
public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void {
76+
public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
2677
let ds: typeof DiagnosticsChannel | undefined;
2778
try {
2879
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -35,12 +86,146 @@ export class Undici implements Integration {
3586
return;
3687
}
3788

38-
// https://github.com/nodejs/undici/blob/main/docs/api/DiagnosticsChannel.md
39-
const undiciChannel = ds.channel('undici:request');
89+
// https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md
90+
const requestCreateChannel = this._setupChannel(ds, ChannelName.RequestCreate);
91+
requestCreateChannel.subscribe(message => {
92+
const { request } = message as RequestCreateMessage;
93+
94+
const url = new URL(request.path, request.origin);
95+
const stringUrl = url.toString();
96+
97+
if (isSentryRequest(stringUrl)) {
98+
return;
99+
}
100+
101+
const hub = getCurrentHub();
102+
const client = hub.getClient<NodeClient>();
103+
const scope = hub.getScope();
104+
105+
const activeSpan = scope.getSpan();
106+
107+
if (activeSpan && client) {
108+
const options = client.getOptions();
109+
110+
// eslint-disable-next-line deprecation/deprecation
111+
const shouldCreateSpan = options.shouldCreateSpanForRequest
112+
? // eslint-disable-next-line deprecation/deprecation
113+
options.shouldCreateSpanForRequest(stringUrl)
114+
: true;
115+
116+
if (shouldCreateSpan) {
117+
const span = activeSpan.startChild({
118+
op: 'http.client',
119+
description: `${request.method || 'GET'} ${stripUrlQueryAndFragment(stringUrl)}`,
120+
data: {
121+
'http.query': `?${url.searchParams.toString()}`,
122+
'http.fragment': url.hash,
123+
},
124+
});
125+
request.__sentry__ = span;
126+
127+
// eslint-disable-next-line deprecation/deprecation
128+
const shouldPropagate = options.tracePropagationTargets
129+
? // eslint-disable-next-line deprecation/deprecation
130+
stringMatchesSomePattern(stringUrl, options.tracePropagationTargets)
131+
: true;
132+
133+
if (shouldPropagate) {
134+
// TODO: Only do this based on tracePropagationTargets
135+
request.addHeader('sentry-trace', span.toTraceparent());
136+
if (span.transaction) {
137+
const dynamicSamplingContext = span.transaction.getDynamicSamplingContext();
138+
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
139+
if (sentryBaggageHeader) {
140+
request.addHeader('baggage', sentryBaggageHeader);
141+
}
142+
}
143+
}
144+
}
145+
}
146+
});
147+
148+
const requestEndChannel = this._setupChannel(ds, ChannelName.RequestEnd);
149+
requestEndChannel.subscribe(message => {
150+
const { request, response } = message as RequestEndMessage;
151+
152+
const url = new URL(request.path, request.origin);
153+
const stringUrl = url.toString();
154+
155+
if (isSentryRequest(stringUrl)) {
156+
return;
157+
}
158+
159+
const span = request.__sentry__;
160+
if (span) {
161+
span.setHttpStatus(response.statusCode);
162+
span.finish();
163+
}
164+
165+
if (this._options.breadcrumbs) {
166+
getCurrentHub().addBreadcrumb(
167+
{
168+
category: 'http',
169+
data: {
170+
method: request.method,
171+
status_code: response.statusCode,
172+
url: stringUrl,
173+
},
174+
type: 'http',
175+
},
176+
{
177+
event: 'response',
178+
request,
179+
response,
180+
},
181+
);
182+
}
183+
});
184+
185+
const requestErrorChannel = this._setupChannel(ds, ChannelName.RequestError);
186+
requestErrorChannel.subscribe(message => {
187+
const { request } = message as RequestErrorMessage;
188+
189+
const url = new URL(request.path, request.origin);
190+
const stringUrl = url.toString();
191+
192+
if (isSentryRequest(stringUrl)) {
193+
return;
194+
}
195+
196+
const span = request.__sentry__;
197+
if (span) {
198+
span.setStatus('internal_error');
199+
span.finish();
200+
}
201+
202+
if (this._options.breadcrumbs) {
203+
getCurrentHub().addBreadcrumb(
204+
{
205+
category: 'http',
206+
data: {
207+
method: request.method,
208+
url: stringUrl,
209+
},
210+
level: 'error',
211+
type: 'http',
212+
},
213+
{
214+
event: 'error',
215+
request,
216+
},
217+
);
218+
}
219+
});
40220
}
41221

42-
private _setupChannel(name: Parameters<typeof DiagnosticsChannel.channel>[0]): void {
43-
const channel = DiagnosticsChannel.channel(name);
44-
if (node)
222+
/** */
223+
private _setupChannel(
224+
ds: typeof DiagnosticsChannel,
225+
name: Parameters<typeof DiagnosticsChannel.channel>[0],
226+
): DiagnosticsChannel.Channel {
227+
const channel = ds.channel(name);
228+
this._channels.add(channel);
229+
return channel;
45230
}
46231
}

0 commit comments

Comments
 (0)