Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "fix(node): Ensure httpIntegration propagates traces (#15233)" #15354

Merged
merged 2 commits into from
Feb 10, 2025
Merged
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

This file was deleted.

This file was deleted.

2 changes: 1 addition & 1 deletion packages/core/src/utils-hoist/baggage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function baggageHeaderToObject(baggageHeader: string): Record<string, string> {
* @returns a baggage header string, or `undefined` if the object didn't have any values, since an empty baggage header
* is not spec compliant.
*/
export function objectToBaggageHeader(object: Record<string, string>): string | undefined {
function objectToBaggageHeader(object: Record<string, string>): string | undefined {
if (Object.keys(object).length === 0) {
// An empty baggage header is not spec compliant: We return undefined.
return undefined;
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/utils-hoist/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ export {
baggageHeaderToDynamicSamplingContext,
dynamicSamplingContextToSentryBaggageHeader,
parseBaggageHeader,
objectToBaggageHeader,
} from './baggage';

export { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from './url';
Expand Down
134 changes: 13 additions & 121 deletions packages/node/src/integrations/http/SentryHttpInstrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,33 @@
/* eslint-disable max-lines */
import type * as http from 'node:http';
import type { IncomingMessage, RequestOptions } from 'node:http';
import type * as https from 'node:https';
import type { EventEmitter } from 'node:stream';
/* eslint-disable max-lines */
import { VERSION } from '@opentelemetry/core';
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
import type { AggregationCounts, Client, RequestEventData, SanitizedRequestData, Scope } from '@sentry/core';
import {
LRUMap,
addBreadcrumb,
generateSpanId,
getBreadcrumbLogLevelFromHttpStatusCode,
getClient,
getIsolationScope,
getSanitizedUrlString,
getTraceData,
httpRequestToRequestData,
logger,
objectToBaggageHeader,
parseBaggageHeader,
parseUrl,
stripUrlQueryAndFragment,
withIsolationScope,
withScope,
} from '@sentry/core';
import { shouldPropagateTraceForUrl } from '@sentry/opentelemetry';
import { DEBUG_BUILD } from '../../debug-build';
import { getRequestUrl } from '../../utils/getRequestUrl';
import { getRequestInfo } from './vendor/getRequestInfo';

type Http = typeof http;
type Https = typeof https;

type RequestArgs =
// eslint-disable-next-line @typescript-eslint/ban-types
| [url: string | URL, options?: RequestOptions, callback?: Function]
// eslint-disable-next-line @typescript-eslint/ban-types
| [options: RequestOptions, callback?: Function];

type SentryHttpInstrumentationOptions = InstrumentationConfig & {
/**
* Whether breadcrumbs should be recorded for requests.
Expand Down Expand Up @@ -91,11 +80,8 @@ const MAX_BODY_BYTE_LENGTH = 1024 * 1024;
* https://github.com/open-telemetry/opentelemetry-js/blob/f8ab5592ddea5cba0a3b33bf8d74f27872c0367f/experimental/packages/opentelemetry-instrumentation-http/src/http.ts
*/
export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpInstrumentationOptions> {
private _propagationDecisionMap: LRUMap<string, boolean>;

public constructor(config: SentryHttpInstrumentationOptions = {}) {
super('@sentry/instrumentation-http', VERSION, config);
this._propagationDecisionMap = new LRUMap<string, boolean>(100);
}

/** @inheritdoc */
Expand All @@ -113,7 +99,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
stealthWrap(moduleExports.Server.prototype, 'emit', this._getPatchIncomingRequestFunction());

// Patch outgoing requests for breadcrumbs
const patchedRequest = stealthWrap(moduleExports, 'request', this._getPatchOutgoingRequestFunction('http'));
const patchedRequest = stealthWrap(moduleExports, 'request', this._getPatchOutgoingRequestFunction());
stealthWrap(moduleExports, 'get', this._getPatchOutgoingGetFunction(patchedRequest));

return moduleExports;
Expand All @@ -134,7 +120,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
stealthWrap(moduleExports.Server.prototype, 'emit', this._getPatchIncomingRequestFunction());

// Patch outgoing requests for breadcrumbs
const patchedRequest = stealthWrap(moduleExports, 'request', this._getPatchOutgoingRequestFunction('https'));
const patchedRequest = stealthWrap(moduleExports, 'request', this._getPatchOutgoingRequestFunction());
stealthWrap(moduleExports, 'get', this._getPatchOutgoingGetFunction(patchedRequest));

return moduleExports;
Expand Down Expand Up @@ -211,7 +197,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
/**
* Patch the outgoing request function for breadcrumbs.
*/
private _getPatchOutgoingRequestFunction(component: 'http' | 'https'): (
private _getPatchOutgoingRequestFunction(): (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
original: (...args: any[]) => http.ClientRequest,
) => (options: URL | http.RequestOptions | string, ...args: unknown[]) => http.ClientRequest {
Expand All @@ -222,32 +208,22 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
return function outgoingRequest(this: unknown, ...args: unknown[]): http.ClientRequest {
instrumentation._diag.debug('http instrumentation for outgoing requests');

// Making a copy to avoid mutating the original args array
// We need to access and reconstruct the request options object passed to `ignoreOutgoingRequests`
// so that it matches what Otel instrumentation passes to `ignoreOutgoingRequestHook`.
// @see https://github.com/open-telemetry/opentelemetry-js/blob/7293e69c1e55ca62e15d0724d22605e61bd58952/experimental/packages/opentelemetry-instrumentation-http/src/http.ts#L756-L789
const requestArgs = [...args] as RequestArgs;

let options = requestArgs[0];
const argsCopy = [...args];

// Make sure correct fallback attributes are set on the options object for https before we pass them to the vendored getRequestInfo function.
// Ref: https://github.com/open-telemetry/opentelemetry-js/blob/887ff1cd6e3f795f703e40a9fbe89b3cba7e88c3/experimental/packages/opentelemetry-instrumentation-http/src/http.ts#L390
if (component === 'https' && typeof options === 'object' && options?.constructor?.name !== 'URL') {
options = Object.assign({}, options);
options.protocol = options.protocol || 'https:';
options.port = options.port || 443;
}
const options = argsCopy.shift() as URL | http.RequestOptions | string;

const extraOptions = typeof requestArgs[1] === 'object' ? requestArgs[1] : undefined;
const extraOptions =
typeof argsCopy[0] === 'object' && (typeof options === 'string' || options instanceof URL)
? (argsCopy.shift() as http.RequestOptions)
: undefined;

const { optionsParsed, origin, pathname } = getRequestInfo(instrumentation._diag, options, extraOptions);
const { optionsParsed } = getRequestInfo(instrumentation._diag, options, extraOptions);

const url = getAbsoluteUrl(origin, pathname);

addSentryHeadersToRequestOptions(url, optionsParsed, instrumentation._propagationDecisionMap);

const request = original.apply(this, [optionsParsed, ...requestArgs.slice(1)]) as ReturnType<
typeof http.request
>;
const request = original.apply(this, args) as ReturnType<typeof http.request>;

request.prependListener('response', (response: http.IncomingMessage) => {
const _breadcrumbs = instrumentation.getConfig().breadcrumbs;
Expand Down Expand Up @@ -481,44 +457,6 @@ function patchRequestToCaptureBody(req: IncomingMessage, isolationScope: Scope):
}
}

/**
* Mutates the passed in `options` and adds `sentry-trace` / `baggage` headers, if they are not already set.
*/
function addSentryHeadersToRequestOptions(
url: string,
options: RequestOptions,
propagationDecisionMap: LRUMap<string, boolean>,
): void {
// Manually add the trace headers, if it applies
// Note: We do not use `propagation.inject()` here, because our propagator relies on an active span
// Which we do not have in this case
const tracePropagationTargets = getClient()?.getOptions().tracePropagationTargets;
const addedHeaders = shouldPropagateTraceForUrl(url, tracePropagationTargets, propagationDecisionMap)
? getTraceData()
: undefined;

if (!addedHeaders) {
return;
}

if (!options.headers) {
options.headers = {};
}
const headers = options.headers;

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

// We do not want to overwrite existing header here, if it was already set
if (sentryTrace && !headers['sentry-trace']) {
headers['sentry-trace'] = sentryTrace;
}

// For baggage, we make sure to merge this into a possibly existing header
if (baggage) {
headers['baggage'] = mergeBaggageHeaders(headers['baggage'], baggage);
}
}

/**
* Starts a session and tracks it in the context of a given isolation scope.
* When the passed response is finished, the session is put into a task and is
Expand Down Expand Up @@ -593,49 +531,3 @@ const clientToRequestSessionAggregatesMap = new Map<
Client,
{ [timestampRoundedToSeconds: string]: { exited: number; crashed: number; errored: number } }
>();

function getAbsoluteUrl(origin: string, path: string = '/'): string {
try {
const url = new URL(path, origin);
return url.toString();
} catch {
// fallback: Construct it on our own
const url = `${origin}`;

if (url.endsWith('/') && path.startsWith('/')) {
return `${url}${path.slice(1)}`;
}

if (!url.endsWith('/') && !path.startsWith('/')) {
return `${url}/${path.slice(1)}`;
}

return `${url}${path}`;
}
}

function mergeBaggageHeaders(
existing: string | string[] | number | undefined,
baggage: string,
): string | string[] | number | undefined {
if (!existing) {
return baggage;
}

const existingBaggageEntries = parseBaggageHeader(existing);
const newBaggageEntries = parseBaggageHeader(baggage);

if (!newBaggageEntries) {
return existing;
}

// Existing entries take precedence, ensuring order remains stable for minimal changes
const mergedBaggageEntries = { ...existingBaggageEntries };
Object.entries(newBaggageEntries).forEach(([key, value]) => {
if (!mergedBaggageEntries[key]) {
mergedBaggageEntries[key] = value;
}
});

return objectToBaggageHeader(mergedBaggageEntries);
}
Loading