Skip to content

Commit 31a85af

Browse files
committed
ref(node): Vendor undici instrumentation
ref #20165 This vendors the instrumentation in with no logic changes as a first step.
1 parent 499f042 commit 31a85af

File tree

10 files changed

+722
-22
lines changed

10 files changed

+722
-22
lines changed

.oxlintrc.base.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@
117117
"max-lines": "off"
118118
}
119119
},
120+
{
121+
"files": ["**/integrations/tracing/node-fetch/vendored/**/*.ts", "**/integrations/node-fetch/vendored/**/*.ts"],
122+
"rules": {
123+
"typescript/consistent-type-imports": "off",
124+
"typescript/no-unnecessary-type-assertion": "off",
125+
"typescript/no-unsafe-member-access": "off",
126+
"typescript/no-explicit-any": "off",
127+
"typescript/prefer-for-of": "off",
128+
"max-lines": "off",
129+
"complexity": "off",
130+
"no-param-reassign": "off"
131+
}
132+
},
120133
{
121134
"files": [
122135
"**/scenarios/**",

packages/node-core/src/integrations/node-fetch/types.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
*/
1616

1717
/**
18-
* Vendored from https://github.com/open-telemetry/opentelemetry-js-contrib/blob/28e209a9da36bc4e1f8c2b0db7360170ed46cb80/plugins/node/instrumentation-undici/src/types.ts
18+
* Aligned with upstream Undici request shape; see `packages/node/.../tracing/node-fetch/vendored/types.ts`
19+
* (vendored from `@opentelemetry/instrumentation-undici`).
1920
*/
2021

2122
export interface UndiciRequest {
@@ -24,9 +25,9 @@ export interface UndiciRequest {
2425
path: string;
2526
/**
2627
* Serialized string of headers in the form `name: value\r\n` for v5
27-
* Array of strings v6
28+
* Array of strings `[key1, value1, ...]` for v6 (values may be `string | string[]`)
2829
*/
29-
headers: string | string[];
30+
headers: string | (string | string[])[];
3031
/**
3132
* Helper method to add headers (from v6)
3233
*/

packages/node-core/src/utils/outgoingFetchRequest.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ export function addTracePropagationHeadersToFetchRequest(
4242

4343
const { 'sentry-trace': sentryTrace, baggage, traceparent } = addedHeaders;
4444

45-
const requestHeaders = Array.isArray(request.headers) ? request.headers : stringToArrayHeaders(request.headers);
45+
// Undici can expose headers either as a raw string (v5-style) or as a flat array of pairs (v6-style).
46+
// In the array form, even indices are header names and odd indices are values; in undici v6 a value
47+
// may be `string | string[]` when a header has multiple values. The helpers below (_deduplicateArrayHeader,
48+
// push, etc.) expect each value slot to be a single string, so we normalize array headers first.
49+
const requestHeaders: string[] = Array.isArray(request.headers)
50+
? normalizeUndiciHeaderPairs(request.headers)
51+
: stringToArrayHeaders(request.headers);
4652

4753
// OTel's UndiciInstrumentation calls propagation.inject() which unconditionally
4854
// appends headers to the request. When the user also sets headers via getTraceData(),
@@ -84,12 +90,37 @@ export function addTracePropagationHeadersToFetchRequest(
8490
}
8591
}
8692

87-
if (!Array.isArray(request.headers)) {
88-
// For original string request headers, we need to write them back to the request
93+
if (Array.isArray(request.headers)) {
94+
// Replace contents in place so we keep the same array reference undici/fetch still holds.
95+
// `requestHeaders` is already normalized (string pairs only); splice writes them back.
96+
request.headers.splice(0, request.headers.length, ...requestHeaders);
97+
} else {
8998
request.headers = arrayToStringHeaders(requestHeaders);
9099
}
91100
}
92101

102+
/**
103+
* Convert undici’s header array into `[name, value, name, value, ...]` where every value is a string.
104+
*
105+
* Undici v6 uses this shape: `[k1, v1, k2, v2, ...]`. Types allow each `v` to be `string | string[]` when
106+
* that header has multiple values. Sentry’s dedupe/merge helpers expect one string per value slot, so
107+
* multi-value arrays are joined with `', '`. Missing value slots become `''`.
108+
*/
109+
function normalizeUndiciHeaderPairs(headers: (string | string[])[]): string[] {
110+
const out: string[] = [];
111+
for (let i = 0; i < headers.length; i++) {
112+
const entry = headers[i];
113+
if (i % 2 === 0) {
114+
// Header name (should always be a string; coerce defensively).
115+
out.push(typeof entry === 'string' ? entry : String(entry));
116+
} else {
117+
// Header value: flatten `string[]` to a single string for downstream string-only helpers.
118+
out.push(Array.isArray(entry) ? entry.join(', ') : (entry ?? ''));
119+
}
120+
}
121+
return out;
122+
}
123+
93124
function stringToArrayHeaders(requestHeaders: string): string[] {
94125
const headersArray = requestHeaders.split('\r\n');
95126
const headers: string[] = [];

packages/node/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@
8989
"@opentelemetry/instrumentation-pg": "0.66.0",
9090
"@opentelemetry/instrumentation-redis": "0.62.0",
9191
"@opentelemetry/instrumentation-tedious": "0.33.0",
92-
"@opentelemetry/instrumentation-undici": "0.24.0",
9392
"@opentelemetry/resources": "^2.6.1",
9493
"@opentelemetry/sdk-trace-base": "^2.6.1",
9594
"@opentelemetry/semantic-conventions": "^1.40.0",

packages/node/src/integrations/node-fetch.ts renamed to packages/node/src/integrations/node-fetch/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { UndiciInstrumentationConfig } from '@opentelemetry/instrumentation-undici';
2-
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';
1+
import type { UndiciInstrumentationConfig } from './vendored/types';
2+
import { UndiciInstrumentation } from './vendored/undici';
33
import type { IntegrationFn } from '@sentry/core';
44
import {
55
defineIntegration,
@@ -12,7 +12,7 @@ import {
1212
} from '@sentry/core';
1313
import type { NodeClient } from '@sentry/node-core';
1414
import { generateInstrumentOnce, SentryNodeFetchInstrumentation } from '@sentry/node-core';
15-
import type { NodeClientOptions } from '../types';
15+
import type { NodeClientOptions } from '../../types';
1616

1717
const INTEGRATION_NAME = 'NodeFetch';
1818

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* NOTICE from the Sentry authors:
17+
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/ed97091c9890dd18e52759f2ea98e9d7593b3ae4/packages/instrumentation-undici
18+
* - Upstream version: @opentelemetry/instrumentation-undici@0.24.0
19+
* - Tracking issue: https://github.com/getsentry/sentry-javascript/issues/20165
20+
*/
21+
/* eslint-disable -- vendored @opentelemetry/instrumentation-undici (#20165) */
22+
23+
import type { UndiciRequest, UndiciResponse } from './types';
24+
25+
export interface ListenerRecord {
26+
name: string;
27+
unsubscribe: () => void;
28+
}
29+
30+
export interface RequestMessage {
31+
request: UndiciRequest;
32+
}
33+
34+
export interface RequestHeadersMessage {
35+
request: UndiciRequest;
36+
socket: any;
37+
}
38+
39+
export interface ResponseHeadersMessage {
40+
request: UndiciRequest;
41+
response: UndiciResponse;
42+
}
43+
44+
export interface RequestTrailersMessage {
45+
request: UndiciRequest;
46+
response: UndiciResponse;
47+
}
48+
49+
export interface RequestErrorMessage {
50+
request: UndiciRequest;
51+
error: Error;
52+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* NOTICE from the Sentry authors:
17+
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/ed97091c9890dd18e52759f2ea98e9d7593b3ae4/packages/instrumentation-undici
18+
* - Upstream version: @opentelemetry/instrumentation-undici@0.24.0
19+
* - Tracking issue: https://github.com/getsentry/sentry-javascript/issues/20165
20+
*/
21+
/* eslint-disable -- vendored @opentelemetry/instrumentation-undici (#20165) */
22+
23+
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
24+
import type { Attributes, Span } from '@opentelemetry/api';
25+
26+
export interface UndiciRequest {
27+
origin: string;
28+
method: string;
29+
path: string;
30+
/**
31+
* Serialized string of headers in the form `name: value\r\n` for v5
32+
* Array of strings `[key1, value1, key2, value2]`, where values are
33+
* `string | string[]` for v6
34+
*/
35+
headers: string | (string | string[])[];
36+
/**
37+
* Helper method to add headers (from v6)
38+
*/
39+
addHeader: (name: string, value: string) => void;
40+
throwOnError: boolean;
41+
completed: boolean;
42+
aborted: boolean;
43+
idempotent: boolean;
44+
contentLength: number | null;
45+
contentType: string | null;
46+
body: any;
47+
}
48+
49+
export interface UndiciResponse {
50+
headers: Buffer[];
51+
statusCode: number;
52+
statusText: string;
53+
}
54+
55+
export interface IgnoreRequestFunction<T = UndiciRequest> {
56+
(request: T): boolean;
57+
}
58+
59+
export interface RequestHookFunction<T = UndiciRequest> {
60+
(span: Span, request: T): void;
61+
}
62+
63+
export interface ResponseHookFunction<RequestType = UndiciRequest, ResponseType = UndiciResponse> {
64+
(span: Span, info: { request: RequestType; response: ResponseType }): void;
65+
}
66+
67+
export interface StartSpanHookFunction<T = UndiciRequest> {
68+
(request: T): Attributes;
69+
}
70+
71+
// This package will instrument HTTP requests made through `undici` or `fetch` global API
72+
// so it seems logical to have similar options than the HTTP instrumentation
73+
export interface UndiciInstrumentationConfig<
74+
RequestType = UndiciRequest,
75+
ResponseType = UndiciResponse,
76+
> extends InstrumentationConfig {
77+
/** Not trace all outgoing requests that matched with custom function */
78+
ignoreRequestHook?: IgnoreRequestFunction<RequestType>;
79+
/** Function for adding custom attributes before request is handled */
80+
requestHook?: RequestHookFunction<RequestType>;
81+
/** Function called once response headers have been received */
82+
responseHook?: ResponseHookFunction<RequestType, ResponseType>;
83+
/** Function for adding custom attributes before a span is started */
84+
startSpanHook?: StartSpanHookFunction<RequestType>;
85+
/** Require parent to create span for outgoing requests */
86+
requireParentforSpans?: boolean;
87+
/** Map the following HTTP headers to span attributes. */
88+
headersToSpanAttributes?: {
89+
requestHeaders?: string[];
90+
responseHeaders?: string[];
91+
};
92+
}

0 commit comments

Comments
 (0)