Skip to content

Commit fb4dd7a

Browse files
authored
feat(deno): instrument node:http on versions that support it (#21009)
This adds a default integration for the `node:http` module's diagnostics channels, on Deno versions that support it. Client is enabled in 2.7.13+, Server is instrumented in 2.8.0+. If either is available, the instrumentation is added by default. If neither is available, then the instrumentation no-ops and warns about being pointless. Test verifies that the current Deno version is handled correctly. close: JS-2031 close: #20059
1 parent 00fcc87 commit fb4dd7a

5 files changed

Lines changed: 400 additions & 0 deletions

File tree

packages/deno/src/denoVersion.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { parseSemver } from '@sentry/core';
2+
3+
export const DENO_VERSION = parseSemver(typeof Deno !== 'undefined' ? (Deno.version?.deno ?? '') : '') as {
4+
major: number | undefined;
5+
minor: number | undefined;
6+
patch: number | undefined;
7+
};
8+
9+
/** Exported for testing */
10+
function gte(major: number, minor: number, patch: number): boolean {
11+
const { major: M, minor: m, patch: p } = DENO_VERSION;
12+
if (M === undefined || m === undefined || p === undefined) return false;
13+
if (M !== major) return M > major;
14+
if (m !== minor) return m > minor;
15+
return p >= patch;
16+
}
17+
18+
/** Whether `http.client.request.created` fires (Deno 2.7.13+). */
19+
export const HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED = gte(2, 7, 13);
20+
21+
/** Whether `http.server.request.start` fires (Deno 2.8.0+). */
22+
export const HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED = gte(2, 8, 0);

packages/deno/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ export { DenoClient } from './client';
104104

105105
export { getDefaultIntegrations, init } from './sdk';
106106
export { denoServeIntegration } from './integrations/deno-serve';
107+
export { denoHttpIntegration } from './integrations/http';
108+
export type { DenoHttpIntegrationOptions } from './integrations/http';
107109
export { denoContextIntegration } from './integrations/context';
108110
export { globalHandlersIntegration } from './integrations/globalhandlers';
109111
export { normalizePathsIntegration } from './integrations/normalizepaths';
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { subscribe } from 'node:diagnostics_channel';
2+
import { errorMonitor } from 'node:events';
3+
import type { ClientRequest, RequestOptions } from 'node:http';
4+
import type { HttpIncomingMessage, Integration, IntegrationFn, Span } from '@sentry/core';
5+
import {
6+
debug,
7+
defineIntegration,
8+
getHttpClientSubscriptions,
9+
getHttpServerSubscriptions,
10+
getRequestOptions,
11+
HTTP_ON_CLIENT_REQUEST,
12+
HTTP_ON_SERVER_REQUEST,
13+
} from '@sentry/core';
14+
import { setAsyncLocalStorageAsyncContextStrategy } from '../async';
15+
import {
16+
DENO_VERSION,
17+
HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED,
18+
HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED,
19+
} from '../denoVersion';
20+
21+
const INTEGRATION_NAME = 'DenoHttp';
22+
23+
export interface DenoHttpIntegrationOptions {
24+
/**
25+
* Whether breadcrumbs should be recorded for outgoing requests.
26+
*
27+
* @default `true`
28+
*/
29+
breadcrumbs?: boolean;
30+
31+
/**
32+
* Whether to create spans for incoming and outgoing HTTP requests.
33+
* Defaults to the client's tracing configuration (`hasSpansEnabled`).
34+
*/
35+
spans?: boolean;
36+
37+
/**
38+
* Whether to inject trace propagation headers (sentry-trace, baggage) into outgoing HTTP requests.
39+
*
40+
* When set to `false`, Sentry will not inject any trace propagation headers, but will still create breadcrumbs
41+
* (if `breadcrumbs` is enabled).
42+
*
43+
* @default `true`
44+
*/
45+
tracePropagation?: boolean;
46+
47+
/**
48+
* Whether to automatically ignore common static asset requests (favicon.ico, robots.txt, etc.)
49+
* when creating server spans.
50+
*
51+
* @default `true`
52+
*/
53+
ignoreStaticAssets?: boolean;
54+
55+
/**
56+
* Controls the maximum size of incoming HTTP request bodies attached to events.
57+
*
58+
* @default 'medium'
59+
*/
60+
maxRequestBodySize?: 'none' | 'small' | 'medium' | 'always';
61+
62+
/**
63+
* Do not capture the request body for incoming HTTP requests to URLs where the given callback returns `true`.
64+
*
65+
* The `request` parameter is the incoming `node:http` {@link IncomingMessage} — use `request.url`,
66+
* `request.method`, `request.headers`, etc.
67+
*/
68+
ignoreRequestBody?: (url: string, request: HttpIncomingMessage) => boolean;
69+
70+
/**
71+
* Do not capture server spans for incoming HTTP requests whose URL path makes the given callback return `true`.
72+
*
73+
* The `request` parameter is the incoming `node:http` {@link IncomingMessage} — use `request.url`,
74+
* `request.method`, `request.headers`, etc.
75+
*/
76+
ignoreIncomingRequests?: (urlPath: string, request: HttpIncomingMessage) => boolean;
77+
78+
/**
79+
* Do not capture breadcrumbs, spans, or propagate trace headers for outgoing HTTP requests where the given callback returns `true`.
80+
*
81+
* The `request` parameter is the outgoing {@link RequestOptions} — use `request.hostname`, `request.path`,
82+
* `request.method`, `request.headers`, etc.
83+
*/
84+
ignoreOutgoingRequests?: (url: string, request: RequestOptions) => boolean;
85+
86+
/**
87+
* Hook invoked after the server span is created but before the request is handled.
88+
*/
89+
onIncomingSpanCreated?: (span: Span, request: unknown, response: unknown) => void;
90+
91+
/**
92+
* Hook invoked when the server span ends, before it is recorded.
93+
*/
94+
onIncomingSpanEnd?: (span: Span, request: unknown, response: unknown) => void;
95+
}
96+
97+
const _denoHttpIntegration = ((options: DenoHttpIntegrationOptions = {}) => {
98+
const breadcrumbs = options.breadcrumbs ?? true;
99+
const tracePropagation = options.tracePropagation ?? true;
100+
101+
return {
102+
name: INTEGRATION_NAME,
103+
setupOnce() {
104+
const denoVersion = DENO_VERSION.major !== undefined ? `${Deno.version.deno}` : 'unknown';
105+
106+
// Below 2.7.13 neither channel fires. Warn and bail without touching the ACS.
107+
if (!HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED && !HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED) {
108+
debug.warn(
109+
`denoHttpIntegration requires Deno 2.7.13+ (client) or 2.8.0+ (server) for node:http diagnostics channels; running on Deno ${denoVersion}. The integration is a no-op on this version.`,
110+
);
111+
return;
112+
}
113+
114+
// Wire up Deno's AsyncLocalStorage-backed ACS so the server subscription's
115+
// `withIsolationScope(clone, ...)` actually activates the cloned scope.
116+
// Without this, request isolation and span creation degrade silently.
117+
setAsyncLocalStorageAsyncContextStrategy();
118+
119+
if (HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED) {
120+
const { [HTTP_ON_SERVER_REQUEST]: onHttpServerRequest } = getHttpServerSubscriptions({
121+
// `spans` falls through to the client's tracing config when unset.
122+
spans: options.spans,
123+
ignoreStaticAssets: options.ignoreStaticAssets,
124+
ignoreIncomingRequests: options.ignoreIncomingRequests,
125+
maxRequestBodySize: options.maxRequestBodySize ?? 'medium',
126+
ignoreRequestBody: options.ignoreRequestBody,
127+
onSpanCreated: options.onIncomingSpanCreated,
128+
onSpanEnd: options.onIncomingSpanEnd,
129+
errorMonitor,
130+
sessions: false,
131+
});
132+
subscribe(HTTP_ON_SERVER_REQUEST, onHttpServerRequest);
133+
} else {
134+
debug.log(
135+
`denoHttpIntegration: server-side instrumentation requires Deno 2.8.0+; running on Deno ${denoVersion}. Client-side instrumentation is still active.`,
136+
);
137+
}
138+
139+
if (HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED) {
140+
const { [HTTP_ON_CLIENT_REQUEST]: onHttpClientRequest } = getHttpClientSubscriptions({
141+
spans: options.spans,
142+
breadcrumbs,
143+
propagateTrace: tracePropagation,
144+
ignoreOutgoingRequests: options.ignoreOutgoingRequests
145+
? (url, request) => options.ignoreOutgoingRequests!(url, getRequestOptions(request as ClientRequest))
146+
: undefined,
147+
// Deno doesn't run OTel's http instrumentation, so there's no
148+
// double-wrap to detect; skip the warning to avoid loading the module.
149+
suppressOtelWarning: true,
150+
errorMonitor,
151+
});
152+
subscribe(HTTP_ON_CLIENT_REQUEST, onHttpClientRequest);
153+
}
154+
},
155+
};
156+
}) satisfies IntegrationFn;
157+
158+
/**
159+
* Instruments incoming and outgoing HTTP requests handled via the `node:http` module in Deno.
160+
*
161+
* Listens on Deno's `node:diagnostics_channel` for `http.server.request.start` and
162+
* `http.client.request.created`, then routes them through Sentry core's portable subscription
163+
* helpers (`getHttpServerSubscriptions`, `getHttpClientSubscriptions`) to create root server
164+
* spans, instrument client requests, and propagate distributed trace headers.
165+
*
166+
* For Deno-native `Deno.serve(...)` instrumentation, see {@link denoServeIntegration}.
167+
*/
168+
export const denoHttpIntegration = defineIntegration(_denoHttpIntegration) as (
169+
options?: DenoHttpIntegrationOptions,
170+
) => Integration & { name: 'DenoHttp'; setupOnce: () => void };

packages/deno/src/sdk.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import { DenoClient } from './client';
1616
import { breadcrumbsIntegration } from './integrations/breadcrumbs';
1717
import { denoContextIntegration } from './integrations/context';
1818
import { contextLinesIntegration } from './integrations/contextlines';
19+
import { HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED, HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED } from './denoVersion';
1920
import { denoServeIntegration } from './integrations/deno-serve';
21+
import { denoHttpIntegration } from './integrations/http';
2022
import { globalHandlersIntegration } from './integrations/globalhandlers';
2123
import { normalizePathsIntegration } from './integrations/normalizepaths';
2224
import { setupOpenTelemetryTracer } from './opentelemetry/tracer';
@@ -39,6 +41,12 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
3941
breadcrumbsIntegration(),
4042
denoContextIntegration(),
4143
denoServeIntegration(),
44+
// node:http client diagnostics channels fire on Deno 2.7.13+
45+
// server channels arrive at 2.8.0+
46+
// Include in defaults if at least one is available
47+
...(HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED || HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED
48+
? [denoHttpIntegration()]
49+
: []),
4250
contextLinesIntegration(),
4351
normalizePathsIntegration(),
4452
globalHandlersIntegration(),

0 commit comments

Comments
 (0)