Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .oxlintrc.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,19 @@
"max-lines": "off"
}
},
{
"files": ["**/integrations/node-fetch/vendored/**/*.ts"],
"rules": {
"typescript/consistent-type-imports": "off",
"typescript/no-unnecessary-type-assertion": "off",
"typescript/no-unsafe-member-access": "off",
"typescript/no-explicit-any": "off",
"typescript/prefer-for-of": "off",
"max-lines": "off",
"complexity": "off",
"no-param-reassign": "off"
}
},
{
"files": [
"**/scenarios/**",
Expand Down
7 changes: 4 additions & 3 deletions packages/node-core/src/integrations/node-fetch/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
*/

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

export interface UndiciRequest {
Expand All @@ -24,9 +25,9 @@ export interface UndiciRequest {
path: string;
/**
* Serialized string of headers in the form `name: value\r\n` for v5
* Array of strings v6
* Array of strings `[key1, value1, ...]` for v6 (values may be `string | string[]`)
*/
headers: string | string[];
headers: string | (string | string[])[];
/**
* Helper method to add headers (from v6)
*/
Expand Down
37 changes: 34 additions & 3 deletions packages/node-core/src/utils/outgoingFetchRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ export function addTracePropagationHeadersToFetchRequest(

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

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

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

if (!Array.isArray(request.headers)) {
// For original string request headers, we need to write them back to the request
if (Array.isArray(request.headers)) {
// Replace contents in place so we keep the same array reference undici/fetch still holds.
// `requestHeaders` is already normalized (string pairs only); splice writes them back.
request.headers.splice(0, request.headers.length, ...requestHeaders);
} else {
request.headers = arrayToStringHeaders(requestHeaders);
}
}

/**
* Convert undici’s header array into `[name, value, name, value, ...]` where every value is a string.
*
* Undici v6 uses this shape: `[k1, v1, k2, v2, ...]`. Types allow each `v` to be `string | string[]` when
* that header has multiple values. Sentry’s dedupe/merge helpers expect one string per value slot, so
* multi-value arrays are joined with `', '`. Missing value slots become `''`.
*/
function normalizeUndiciHeaderPairs(headers: (string | string[])[]): string[] {
const out: string[] = [];
for (let i = 0; i < headers.length; i++) {
const entry = headers[i];
if (i % 2 === 0) {
// Header name (should always be a string; coerce defensively).
out.push(typeof entry === 'string' ? entry : String(entry));
} else {
// Header value: flatten `string[]` to a single string for downstream string-only helpers.
out.push(Array.isArray(entry) ? entry.join(', ') : (entry ?? ''));
}
}
return out;
}

function stringToArrayHeaders(requestHeaders: string): string[] {
const headersArray = requestHeaders.split('\r\n');
const headers: string[] = [];
Expand Down
1 change: 0 additions & 1 deletion packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@
"@opentelemetry/instrumentation-pg": "0.66.0",
"@opentelemetry/instrumentation-redis": "0.62.0",
"@opentelemetry/instrumentation-tedious": "0.33.0",
"@opentelemetry/instrumentation-undici": "0.24.0",
"@opentelemetry/resources": "^2.6.1",
"@opentelemetry/sdk-trace-base": "^2.6.1",
"@opentelemetry/semantic-conventions": "^1.40.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { UndiciInstrumentationConfig } from '@opentelemetry/instrumentation-undici';
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';
import type { UndiciInstrumentationConfig } from './vendored/types';
import { UndiciInstrumentation } from './vendored/undici';
import type { IntegrationFn } from '@sentry/core';
import {
defineIntegration,
Expand All @@ -12,7 +12,7 @@ import {
} from '@sentry/core';
import type { NodeClient } from '@sentry/node-core';
import { generateInstrumentOnce, SentryNodeFetchInstrumentation } from '@sentry/node-core';
import type { NodeClientOptions } from '../types';
import type { NodeClientOptions } from '../../types';

const INTEGRATION_NAME = 'NodeFetch';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* NOTICE from the Sentry authors:
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/ed97091c9890dd18e52759f2ea98e9d7593b3ae4/packages/instrumentation-undici
* - Upstream version: @opentelemetry/instrumentation-undici@0.24.0
* - Tracking issue: https://github.com/getsentry/sentry-javascript/issues/20165
*/
/* eslint-disable -- vendored @opentelemetry/instrumentation-undici (#20165) */

import type { UndiciRequest, UndiciResponse } from './types';

export interface ListenerRecord {
name: string;
unsubscribe: () => void;
}

export interface RequestMessage {
request: UndiciRequest;
}

export interface RequestHeadersMessage {
request: UndiciRequest;
socket: any;
}

export interface ResponseHeadersMessage {
request: UndiciRequest;
response: UndiciResponse;
}

export interface RequestTrailersMessage {
request: UndiciRequest;
response: UndiciResponse;
}

export interface RequestErrorMessage {
request: UndiciRequest;
error: Error;
}
92 changes: 92 additions & 0 deletions packages/node/src/integrations/node-fetch/vendored/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* NOTICE from the Sentry authors:
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/ed97091c9890dd18e52759f2ea98e9d7593b3ae4/packages/instrumentation-undici
* - Upstream version: @opentelemetry/instrumentation-undici@0.24.0
* - Tracking issue: https://github.com/getsentry/sentry-javascript/issues/20165
*/
/* eslint-disable -- vendored @opentelemetry/instrumentation-undici (#20165) */

import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
import type { Attributes, Span } from '@opentelemetry/api';

export interface UndiciRequest {
origin: string;
method: string;
path: string;
/**
* Serialized string of headers in the form `name: value\r\n` for v5
* Array of strings `[key1, value1, key2, value2]`, where values are
* `string | string[]` for v6
*/
headers: string | (string | string[])[];
/**
* Helper method to add headers (from v6)
*/
addHeader: (name: string, value: string) => void;
throwOnError: boolean;
completed: boolean;
aborted: boolean;
idempotent: boolean;
contentLength: number | null;
contentType: string | null;
body: any;
}

export interface UndiciResponse {
headers: Buffer[];
statusCode: number;
statusText: string;
}

export interface IgnoreRequestFunction<T = UndiciRequest> {
(request: T): boolean;
}

export interface RequestHookFunction<T = UndiciRequest> {
(span: Span, request: T): void;
}

export interface ResponseHookFunction<RequestType = UndiciRequest, ResponseType = UndiciResponse> {
(span: Span, info: { request: RequestType; response: ResponseType }): void;
}

export interface StartSpanHookFunction<T = UndiciRequest> {
(request: T): Attributes;
}

// This package will instrument HTTP requests made through `undici` or `fetch` global API
// so it seems logical to have similar options than the HTTP instrumentation
export interface UndiciInstrumentationConfig<
RequestType = UndiciRequest,
ResponseType = UndiciResponse,
> extends InstrumentationConfig {
/** Not trace all outgoing requests that matched with custom function */
ignoreRequestHook?: IgnoreRequestFunction<RequestType>;
/** Function for adding custom attributes before request is handled */
requestHook?: RequestHookFunction<RequestType>;
/** Function called once response headers have been received */
responseHook?: ResponseHookFunction<RequestType, ResponseType>;
/** Function for adding custom attributes before a span is started */
startSpanHook?: StartSpanHookFunction<RequestType>;
/** Require parent to create span for outgoing requests */
requireParentforSpans?: boolean;
/** Map the following HTTP headers to span attributes. */
headersToSpanAttributes?: {
requestHeaders?: string[];
responseHeaders?: string[];
};
}
Loading
Loading