Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Revert "fix(node): Ensure `httpIntegration` propagates traces (#15233)" by lforst · Pull Request #15354 · getsentry/sentry-javascript · GitHub
Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Revert "fix(node): Ensure `httpIntegration` propagates traces (#15233)" by lforst · Pull Request #15354 · getsentry/sentry-javascript · GitHub
Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Revert "fix(node): Ensure `httpIntegration` propagates traces (#15233)" by lforst · Pull Request #15354 · getsentry/sentry-javascript · GitHub
Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Revert "fix(node): Ensure `httpIntegration` propagates traces (#15233)" by lforst · Pull Request #15354 · getsentry/sentry-javascript · GitHub
Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Revert "fix(node): Ensure `httpIntegration` propagates traces (#15233)" by lforst · Pull Request #15354 · getsentry/sentry-javascript · GitHub
Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Revert "fix(node): Ensure `httpIntegration` propagates traces (#15233)" by lforst · Pull Request #15354 · getsentry/sentry-javascript · GitHub
Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
}
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Revert "fix(node): Ensure `httpIntegration` propagates traces (#15233)" by lforst · Pull Request #15354 · getsentry/sentry-javascript · GitHub
Skip to content
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
}