Skip to content

Commit 23ffed9

Browse files
committed
add normalized request data in http
1 parent 3b121bc commit 23ffed9

File tree

6 files changed

+177
-11
lines changed

6 files changed

+177
-11
lines changed

packages/core/src/integrations/requestdata.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { IntegrationFn } from '@sentry/types';
22
import type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils';
3+
import { addNormalizedRequestDataToEvent } from '@sentry/utils';
34
import { addRequestDataToEvent } from '@sentry/utils';
45
import { defineIntegration } from '../integration';
56

@@ -73,15 +74,24 @@ const _requestDataIntegration = ((options: RequestDataIntegrationOptions = {}) =
7374
// that's happened, it will be easier to add this logic in without worrying about unexpected side effects.)
7475

7576
const { sdkProcessingMetadata = {} } = event;
76-
const req = sdkProcessingMetadata.request;
77+
const { request, normalizedRequest } = sdkProcessingMetadata;
7778

78-
if (!req) {
79-
return event;
79+
const addRequestDataOptions = convertReqDataIntegrationOptsToAddReqDataOpts(_options);
80+
81+
// If this is set, it takes precedence over the plain request object
82+
if (normalizedRequest) {
83+
// Some other data is not available in standard HTTP requests, but can sometimes be augmented by e.g. Express or Next.js
84+
const ipAddress = request ? request.ip || (request.socket && request.socket.remoteAddress) : undefined;
85+
const user = request ? request.user : undefined;
86+
87+
return addNormalizedRequestDataToEvent(event, normalizedRequest, { ipAddress, user }, addRequestDataOptions);
8088
}
8189

82-
const addRequestDataOptions = convertReqDataIntegrationOptsToAddReqDataOpts(_options);
90+
if (!request) {
91+
return event;
92+
}
8393

84-
return addRequestDataToEvent(event, req, addRequestDataOptions);
94+
return addRequestDataToEvent(event, request, addRequestDataOptions);
8595
},
8696
};
8797
}) satisfies IntegrationFn;

packages/node/src/transports/http-module.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export type HTTPModuleRequestOptions = HTTPRequestOptions | HTTPSRequestOptions
1010
export interface HTTPModuleRequestIncomingMessage {
1111
headers: IncomingHttpHeaders;
1212
statusCode?: number;
13-
on(event: 'data' | 'end', listener: () => void): void;
13+
on(event: 'data' | 'end', listener: (chunk: Buffer) => void): void;
14+
off(event: 'data' | 'end', listener: (chunk: Buffer) => void): void;
1415
setEncoding(encoding: string): void;
1516
}
1617

packages/types/src/envelope.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export type ProfileItem = BaseEnvelopeItem<ProfileItemHeaders, Profile>;
101101
export type ProfileChunkItem = BaseEnvelopeItem<ProfileChunkItemHeaders, ProfileChunk>;
102102
export type SpanItem = BaseEnvelopeItem<SpanItemHeaders, Partial<SpanJSON>>;
103103

104-
export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: DynamicSamplingContext };
104+
export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: Partial<DynamicSamplingContext> };
105105
type SessionEnvelopeHeaders = { sent_at: string };
106106
type CheckInEnvelopeHeaders = { trace?: DynamicSamplingContext };
107107
type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders;

packages/types/src/event.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import type { Attachment } from './attachment';
22
import type { Breadcrumb } from './breadcrumb';
33
import type { Contexts } from './context';
44
import type { DebugMeta } from './debugMeta';
5+
import type { DynamicSamplingContext } from './envelope';
56
import type { Exception } from './exception';
67
import type { Extras } from './extra';
78
import type { Measurements } from './measurement';
89
import type { Mechanism } from './mechanism';
910
import type { Primitive } from './misc';
11+
import type { PolymorphicRequest } from './polymorphics';
1012
import type { Request } from './request';
11-
import type { CaptureContext } from './scope';
13+
import type { CaptureContext, Scope } from './scope';
1214
import type { SdkInfo } from './sdkinfo';
1315
import type { SeverityLevel } from './severity';
1416
import type { MetricSummary, SpanJSON } from './span';
@@ -51,7 +53,15 @@ export interface Event {
5153
measurements?: Measurements;
5254
debug_meta?: DebugMeta;
5355
// A place to stash data which is needed at some point in the SDK's event processing pipeline but which shouldn't get sent to Sentry
54-
sdkProcessingMetadata?: { [key: string]: any };
56+
// Note: This is considered internal and is subject to change in minors
57+
sdkProcessingMetadata?: { [key: string]: unknown } & {
58+
request?: PolymorphicRequest;
59+
normalizedRequest?: Request;
60+
dynamicSamplingContext?: Partial<DynamicSamplingContext>;
61+
capturedSpanScope?: Scope;
62+
capturedSpanIsolationScope?: Scope;
63+
spanCountBeforeProcessing?: number;
64+
};
5565
transaction_info?: {
5666
source: TransactionSource;
5767
};

packages/utils/src/requestdata.ts

+122-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
/* eslint-disable max-lines */
12
import type {
23
Event,
34
ExtractedNodeRequestData,
45
PolymorphicRequest,
6+
Request,
57
TransactionSource,
68
WebFetchHeaders,
79
WebFetchRequest,
@@ -12,6 +14,7 @@ import { DEBUG_BUILD } from './debug-build';
1214
import { isPlainObject, isString } from './is';
1315
import { logger } from './logger';
1416
import { normalize } from './normalize';
17+
import { truncate } from './string';
1518
import { stripUrlQueryAndFragment } from './url';
1619
import { getClientIPAddress, ipHeaderNames } from './vendor/getIpAddress';
1720

@@ -228,14 +231,27 @@ export function extractRequestData(
228231
if (method === 'GET' || method === 'HEAD') {
229232
break;
230233
}
234+
// NOTE: As of v8, request is (unless a user sets this manually) ALWAYS a http request
235+
// Which does not have a body by default
236+
// However, in our http instrumentation, we patch the request to capture the body and store it on the
237+
// request as `.body` anyhow
238+
// In v9, we may update requestData to only work with plain http requests
231239
// body data:
232240
// express, koa, nextjs: req.body
233241
//
234242
// when using node by itself, you have to read the incoming stream(see
235243
// https://nodejs.dev/learn/get-http-request-body-data-using-nodejs); if a user is doing that, we can't know
236244
// where they're going to store the final result, so they'll have to capture this data themselves
237-
if (req.body !== undefined) {
238-
requestData.data = isString(req.body) ? req.body : JSON.stringify(normalize(req.body));
245+
const body = req.body;
246+
if (body !== undefined) {
247+
const stringBody: string = isString(body)
248+
? body
249+
: isPlainObject(body)
250+
? JSON.stringify(normalize(body))
251+
: truncate(`${body}`, 1024);
252+
if (stringBody) {
253+
requestData.data = stringBody;
254+
}
239255
}
240256
break;
241257
}
@@ -250,6 +266,62 @@ export function extractRequestData(
250266
return requestData;
251267
}
252268

269+
/**
270+
* Add already normalized request data to an event.
271+
*/
272+
export function addNormalizedRequestDataToEvent(
273+
event: Event,
274+
req: Request,
275+
// This is non-standard data that is not part of the regular HTTP request
276+
additionalData: { ipAddress?: string; user?: Record<string, unknown> },
277+
options: AddRequestDataToEventOptions,
278+
): Event {
279+
const include = {
280+
...DEFAULT_INCLUDES,
281+
...(options && options.include),
282+
};
283+
284+
if (include.request) {
285+
const includeRequest = Array.isArray(include.request) ? [...include.request] : [...DEFAULT_REQUEST_INCLUDES];
286+
if (include.ip) {
287+
includeRequest.push('ip');
288+
}
289+
290+
const extractedRequestData = extractNormalizedRequestData(req, { include: includeRequest });
291+
292+
event.request = {
293+
...event.request,
294+
...extractedRequestData,
295+
};
296+
}
297+
298+
if (include.user) {
299+
const extractedUser =
300+
additionalData.user && isPlainObject(additionalData.user)
301+
? extractUserData(additionalData.user, include.user)
302+
: {};
303+
304+
if (Object.keys(extractedUser).length) {
305+
event.user = {
306+
...event.user,
307+
...extractedUser,
308+
};
309+
}
310+
}
311+
312+
if (include.ip) {
313+
const ip = (req.headers && getClientIPAddress(req.headers)) || additionalData.ipAddress;
314+
if (ip) {
315+
event.user = {
316+
...event.user,
317+
ip_address: ip,
318+
};
319+
}
320+
}
321+
322+
return event;
323+
}
324+
253325
/**
254326
* Add data from the given request to the given event
255327
*
@@ -374,3 +446,51 @@ export function winterCGRequestToRequestData(req: WebFetchRequest): PolymorphicR
374446
headers,
375447
};
376448
}
449+
450+
function extractNormalizedRequestData(normalizedRequest: Request, { include }: { include: string[] }): Request {
451+
const includeKeys = include ? (Array.isArray(include) ? include : DEFAULT_REQUEST_INCLUDES) : [];
452+
453+
const requestData: Request = {};
454+
455+
const { headers } = normalizedRequest;
456+
457+
if (includeKeys.includes('headers')) {
458+
requestData.headers = headers;
459+
460+
// Remove the Cookie header in case cookie data should not be included in the event
461+
if (!include.includes('cookies')) {
462+
delete (headers as { cookie?: string }).cookie;
463+
}
464+
465+
// Remove IP headers in case IP data should not be included in the event
466+
if (!include.includes('ip')) {
467+
ipHeaderNames.forEach(ipHeaderName => {
468+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
469+
delete (headers as Record<string, unknown>)[ipHeaderName];
470+
});
471+
}
472+
}
473+
474+
if (includeKeys.includes('method')) {
475+
requestData.method = normalizedRequest.method;
476+
}
477+
478+
if (includeKeys.includes('url')) {
479+
requestData.url = normalizedRequest.url;
480+
}
481+
482+
if (includeKeys.includes('cookies')) {
483+
const cookies = normalizedRequest.cookies || (headers && headers.cookie ? parseCookie(headers.cookie) : undefined);
484+
requestData.cookies = cookies;
485+
}
486+
487+
if (includeKeys.includes('query_string')) {
488+
requestData.query_string = normalizedRequest.query_string;
489+
}
490+
491+
if (includeKeys.includes('data')) {
492+
requestData.data = normalizedRequest.data;
493+
}
494+
495+
return requestData;
496+
}

yarn.lock

+25
Original file line numberDiff line numberDiff line change
@@ -12964,6 +12964,24 @@ [email protected], body-parser@^1.18.3, body-parser@^1.19.0:
1296412964
type-is "~1.6.18"
1296512965
unpipe "1.0.0"
1296612966

12967+
body-parser@^1.20.3:
12968+
version "1.20.3"
12969+
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6"
12970+
integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==
12971+
dependencies:
12972+
bytes "3.1.2"
12973+
content-type "~1.0.5"
12974+
debug "2.6.9"
12975+
depd "2.0.0"
12976+
destroy "1.2.0"
12977+
http-errors "2.0.0"
12978+
iconv-lite "0.4.24"
12979+
on-finished "2.4.1"
12980+
qs "6.13.0"
12981+
raw-body "2.5.2"
12982+
type-is "~1.6.18"
12983+
unpipe "1.0.0"
12984+
1296712985
body@^5.1.0:
1296812986
version "5.1.0"
1296912987
resolved "https://registry.yarnpkg.com/body/-/body-5.1.0.tgz#e4ba0ce410a46936323367609ecb4e6553125069"
@@ -28287,6 +28305,13 @@ qs@^6.4.0:
2828728305
dependencies:
2828828306
side-channel "^1.0.4"
2828928307

28308+
28309+
version "6.13.0"
28310+
resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
28311+
integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
28312+
dependencies:
28313+
side-channel "^1.0.6"
28314+
2829028315
query-string@^4.2.2:
2829128316
version "4.3.4"
2829228317
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"

0 commit comments

Comments
 (0)