Skip to content

Commit

Permalink
fix after rebase
Browse files Browse the repository at this point in the history
  • Loading branch information
mydea committed Oct 28, 2024
1 parent c570e39 commit 3ad5459
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 3 deletions.
153 changes: 151 additions & 2 deletions packages/node/src/integrations/http/SentryHttpInstrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import type * as http from 'node:http';
import type { RequestOptions } from 'node:http';
import type { IncomingMessage, RequestOptions } from 'node:http';
import type * as https from 'node:https';
import { VERSION } from '@opentelemetry/core';
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
import { getRequestInfo } from '@opentelemetry/instrumentation-http';
import { addBreadcrumb, getClient, getIsolationScope, withIsolationScope } from '@sentry/core';
import type { SanitizedRequestData } from '@sentry/types';
import type { PolymorphicRequest, Request, SanitizedRequestData } from '@sentry/types';
import {
getBreadcrumbLogLevelFromHttpStatusCode,
getSanitizedUrlString,
logger,
parseUrl,
stripUrlQueryAndFragment,
} from '@sentry/utils';
import { DEBUG_BUILD } from '../../debug-build';
import type { NodeClient } from '../../sdk/client';
import { getRequestUrl } from '../../utils/getRequestUrl';

Expand Down Expand Up @@ -128,6 +130,28 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns

const isolationScope = getIsolationScope().clone();

const headers = request.headers;
const host = headers.host || '<no host>';
const protocol = request.socket && (request.socket as { encrypted?: boolean }).encrypted ? 'https' : 'http';
const originalUrl = request.url || '';
const absoluteUrl = originalUrl.startsWith(protocol) ? originalUrl : `${protocol}://${host}${originalUrl}`;

// This is non-standard, but may be set on e.g. Next.js or Express requests
const cookies = (request as PolymorphicRequest).cookies;

const normalizedRequest: Request = {
url: absoluteUrl,
method: request.method,
query_string: extractQueryParams(request),
headers: headersToDict(request.headers),
cookies,
};

patchRequestToCaptureBody(request, normalizedRequest);

// Update the isolation scope, isolate this request
isolationScope.setSDKProcessingMetadata({ request, normalizedRequest });

// Update the isolation scope, isolate this request
isolationScope.setSDKProcessingMetadata({ request });

Expand Down Expand Up @@ -316,3 +340,128 @@ function getBreadcrumbData(request: http.ClientRequest): Partial<SanitizedReques
return {};
}
}

/**
* This method patches the request object to capture the body.
* Instead of actually consuming the streamed body ourselves, which has potential side effects,
* we monkey patch `req.on('data')` to intercept the body chunks.
* This way, we only read the body if the user also consumes the body, ensuring we do not change any behavior in unexpected ways.
*/
function patchRequestToCaptureBody(req: IncomingMessage, normalizedRequest: Request): void {
const chunks: Buffer[] = [];

/**
* We need to keep track of the original callbacks, in order to be able to remove listeners again.
* Since `off` depends on having the exact same function reference passed in, we need to be able to map
* original listeners to our wrapped ones.
*/
const callbackMap = new WeakMap();

try {
// eslint-disable-next-line @typescript-eslint/unbound-method
req.on = new Proxy(req.on, {
apply: (target, thisArg, args: Parameters<typeof req.on>) => {
const [event, listener, ...restArgs] = args;

if (event === 'data') {
const callback = new Proxy(listener, {
apply: (target, thisArg, args: Parameters<typeof listener>) => {
const chunk = args[0];
chunks.push(chunk);
return Reflect.apply(target, thisArg, args);
},
});

callbackMap.set(listener, callback);

return Reflect.apply(target, thisArg, [event, callback, ...restArgs]);
}

if (event === 'end') {
const callback = new Proxy(listener, {
apply: (target, thisArg, args) => {
try {
const body = Buffer.concat(chunks).toString('utf-8');

// We mutate the passed in normalizedRequest and add the body to it
if (body) {
normalizedRequest.data = body;
}
} catch {
// ignore errors here
}

return Reflect.apply(target, thisArg, args);
},
});

callbackMap.set(listener, callback);

return Reflect.apply(target, thisArg, [event, callback, ...restArgs]);
}

return Reflect.apply(target, thisArg, args);
},
});

// Ensure we also remove callbacks correctly
// eslint-disable-next-line @typescript-eslint/unbound-method
req.off = new Proxy(req.off, {
apply: (target, thisArg, args: Parameters<typeof req.off>) => {
const [, listener] = args;

const callback = callbackMap.get(listener);
if (callback) {
callbackMap.delete(listener);

const modifiedArgs = args.slice();
modifiedArgs[1] = callback;
return Reflect.apply(target, thisArg, modifiedArgs);
}

return Reflect.apply(target, thisArg, args);
},
});
} catch {
// ignore errors if we can't patch stuff
}
}

function extractQueryParams(req: IncomingMessage): string | undefined {
// url (including path and query string):
let originalUrl = req.url || '';

if (!originalUrl) {
return;
}

// The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and
// hostname on the beginning. Since the point here is just to grab the query string, it doesn't matter what we use.
if (originalUrl.startsWith('/')) {
originalUrl = `http://dogs.are.great${originalUrl}`;
}

try {
const queryParams = new URL(originalUrl).search.slice(1);
return queryParams.length ? queryParams : undefined;
} catch {
return undefined;
}
}

function headersToDict(reqHeaders: Record<string, string | string[] | undefined>): Record<string, string> {
const headers: Record<string, string> = {};

try {
Object.entries(reqHeaders).forEach(([key, value]) => {
if (typeof value === 'string') {
headers[key] = value;
}
});
} catch (e) {
DEBUG_BUILD &&
logger.warn('Sentry failed extracting headers from a request object. If you see this, please file an issue.');
}

return headers;
}
5 changes: 4 additions & 1 deletion packages/types/src/request.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
/** Request data included in an event as sent to Sentry */
/**
* Request data included in an event as sent to Sentry.
* TODO(v9): Rename this to avoid confusion, because Request is also a native type.
*/
export interface Request {
url?: string;
method?: string;
Expand Down

0 comments on commit 3ad5459

Please sign in to comment.