Skip to content

Commit 9862a32

Browse files
authored
ref(node): Move request data functions back to@sentry/node (#5759)
As part of the work adding a `RequestData` integration, this moves the `requestdata` functions back into the node SDK. (The dependency injection makes things hard to follow, and soon the original reason for the move (so that they could be used in the `_error` helper in the nextjs SDK, which runs in both browser and node) will no longer apply (once #5729 is merged).) Once these functions are no longer needed, they can be deleted from `@sentry/utils`. More details and a work plan are in #5756.
1 parent 16c121a commit 9862a32

File tree

8 files changed

+339
-108
lines changed

8 files changed

+339
-108
lines changed

packages/node/src/handlers.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ import * as domain from 'domain';
1414
import * as http from 'http';
1515

1616
import { NodeClient } from './client';
17+
import { addRequestDataToEvent, extractRequestData } from './requestdata';
1718
// TODO (v8 / XXX) Remove these imports
1819
import type { ParseRequestOptions } from './requestDataDeprecated';
1920
import { parseRequest } from './requestDataDeprecated';
20-
import { addRequestDataToEvent, extractRequestData, flush, isAutoSessionTrackingEnabled } from './sdk';
21+
import { flush, isAutoSessionTrackingEnabled } from './sdk';
2122

2223
/**
2324
* Express-compatible tracing handler.

packages/node/src/index.ts

+2-11
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,8 @@ export {
4646

4747
export { NodeClient } from './client';
4848
export { makeNodeTransport } from './transports';
49-
export {
50-
addRequestDataToEvent,
51-
extractRequestData,
52-
defaultIntegrations,
53-
init,
54-
defaultStackParser,
55-
lastEventId,
56-
flush,
57-
close,
58-
getSentryRelease,
59-
} from './sdk';
49+
export { defaultIntegrations, init, defaultStackParser, lastEventId, flush, close, getSentryRelease } from './sdk';
50+
export { addRequestDataToEvent, extractRequestData } from './requestdata';
6051
export { deepReadDirSync } from './utils';
6152

6253
import { Integrations as CoreIntegrations } from '@sentry/core';

packages/node/src/requestDataDeprecated.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@
77
/* eslint-disable deprecation/deprecation */
88
/* eslint-disable @typescript-eslint/no-explicit-any */
99
import { Event, ExtractedNodeRequestData, PolymorphicRequest } from '@sentry/types';
10+
1011
import {
1112
addRequestDataToEvent,
1213
AddRequestDataToEventOptions,
1314
extractRequestData as _extractRequestData,
14-
} from '@sentry/utils';
15-
import * as cookie from 'cookie';
16-
import * as url from 'url';
15+
} from './requestdata';
1716

1817
/**
1918
* @deprecated `Handlers.ExpressRequest` is deprecated and will be removed in v8. Use `PolymorphicRequest` instead.
@@ -30,7 +29,7 @@ export type ExpressRequest = PolymorphicRequest;
3029
* @returns An object containing normalized request data
3130
*/
3231
export function extractRequestData(req: { [key: string]: any }, keys?: string[]): ExtractedNodeRequestData {
33-
return _extractRequestData(req, { include: keys, deps: { cookie, url } });
32+
return _extractRequestData(req, { include: keys });
3433
}
3534

3635
/**
@@ -55,5 +54,5 @@ export type ParseRequestOptions = AddRequestDataToEventOptions['include'] & {
5554
* @hidden
5655
*/
5756
export function parseRequest(event: Event, req: ExpressRequest, options: ParseRequestOptions = {}): Event {
58-
return addRequestDataToEvent(event, req, { include: options, deps: { cookie, url } });
57+
return addRequestDataToEvent(event, req, { include: options });
5958
}

packages/node/src/requestdata.ts

+318
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
import { Event, ExtractedNodeRequestData, PolymorphicRequest, Transaction, TransactionSource } from '@sentry/types';
2+
import { isPlainObject, isString, normalize, stripUrlQueryAndFragment } from '@sentry/utils/';
3+
import * as cookie from 'cookie';
4+
import * as url from 'url';
5+
6+
const DEFAULT_INCLUDES = {
7+
ip: false,
8+
request: true,
9+
transaction: true,
10+
user: true,
11+
};
12+
const DEFAULT_REQUEST_INCLUDES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url'];
13+
const DEFAULT_USER_INCLUDES = ['id', 'username', 'email'];
14+
15+
/**
16+
* Options deciding what parts of the request to use when enhancing an event
17+
*/
18+
export interface AddRequestDataToEventOptions {
19+
/** Flags controlling whether each type of data should be added to the event */
20+
include?: {
21+
ip?: boolean;
22+
request?: boolean | Array<typeof DEFAULT_REQUEST_INCLUDES[number]>;
23+
transaction?: boolean | TransactionNamingScheme;
24+
user?: boolean | Array<typeof DEFAULT_USER_INCLUDES[number]>;
25+
};
26+
}
27+
28+
type TransactionNamingScheme = 'path' | 'methodPath' | 'handler';
29+
30+
/**
31+
* Sets parameterized route as transaction name e.g.: `GET /users/:id`
32+
* Also adds more context data on the transaction from the request
33+
*/
34+
export function addRequestDataToTransaction(transaction: Transaction | undefined, req: PolymorphicRequest): void {
35+
if (!transaction) return;
36+
if (!transaction.metadata.source || transaction.metadata.source === 'url') {
37+
// Attempt to grab a parameterized route off of the request
38+
transaction.setName(...extractPathForTransaction(req, { path: true, method: true }));
39+
}
40+
transaction.setData('url', req.originalUrl || req.url);
41+
if (req.baseUrl) {
42+
transaction.setData('baseUrl', req.baseUrl);
43+
}
44+
transaction.setData('query', extractQueryParams(req));
45+
}
46+
47+
/**
48+
* Extracts a complete and parameterized path from the request object and uses it to construct transaction name.
49+
* If the parameterized transaction name cannot be extracted, we fall back to the raw URL.
50+
*
51+
* Additionally, this function determines and returns the transaction name source
52+
*
53+
* eg. GET /mountpoint/user/:id
54+
*
55+
* @param req A request object
56+
* @param options What to include in the transaction name (method, path, or a custom route name to be
57+
* used instead of the request's route)
58+
*
59+
* @returns A tuple of the fully constructed transaction name [0] and its source [1] (can be either 'route' or 'url')
60+
*/
61+
export function extractPathForTransaction(
62+
req: PolymorphicRequest,
63+
options: { path?: boolean; method?: boolean; customRoute?: string } = {},
64+
): [string, TransactionSource] {
65+
const method = req.method && req.method.toUpperCase();
66+
67+
let path = '';
68+
let source: TransactionSource = 'url';
69+
70+
// Check to see if there's a parameterized route we can use (as there is in Express)
71+
if (options.customRoute || req.route) {
72+
path = options.customRoute || `${req.baseUrl || ''}${req.route && req.route.path}`;
73+
source = 'route';
74+
}
75+
76+
// Otherwise, just take the original URL
77+
else if (req.originalUrl || req.url) {
78+
path = stripUrlQueryAndFragment(req.originalUrl || req.url || '');
79+
}
80+
81+
let name = '';
82+
if (options.method && method) {
83+
name += method;
84+
}
85+
if (options.method && options.path) {
86+
name += ' ';
87+
}
88+
if (options.path && path) {
89+
name += path;
90+
}
91+
92+
return [name, source];
93+
}
94+
95+
/** JSDoc */
96+
function extractTransaction(req: PolymorphicRequest, type: boolean | TransactionNamingScheme): string {
97+
switch (type) {
98+
case 'path': {
99+
return extractPathForTransaction(req, { path: true })[0];
100+
}
101+
case 'handler': {
102+
return (req.route && req.route.stack && req.route.stack[0] && req.route.stack[0].name) || '<anonymous>';
103+
}
104+
case 'methodPath':
105+
default: {
106+
return extractPathForTransaction(req, { path: true, method: true })[0];
107+
}
108+
}
109+
}
110+
111+
/** JSDoc */
112+
function extractUserData(
113+
user: {
114+
[key: string]: unknown;
115+
},
116+
keys: boolean | string[],
117+
): { [key: string]: unknown } {
118+
const extractedUser: { [key: string]: unknown } = {};
119+
const attributes = Array.isArray(keys) ? keys : DEFAULT_USER_INCLUDES;
120+
121+
attributes.forEach(key => {
122+
if (user && key in user) {
123+
extractedUser[key] = user[key];
124+
}
125+
});
126+
127+
return extractedUser;
128+
}
129+
130+
/**
131+
* Normalize data from the request object
132+
*
133+
* @param req The request object from which to extract data
134+
* @param options.include An optional array of keys to include in the normalized data. Defaults to
135+
* DEFAULT_REQUEST_INCLUDES if not provided.
136+
* @param options.deps Injected, platform-specific dependencies
137+
*
138+
* @returns An object containing normalized request data
139+
*/
140+
export function extractRequestData(
141+
req: PolymorphicRequest,
142+
options?: {
143+
include?: string[];
144+
},
145+
): ExtractedNodeRequestData {
146+
const { include = DEFAULT_REQUEST_INCLUDES } = options || {};
147+
const requestData: { [key: string]: unknown } = {};
148+
149+
// headers:
150+
// node, express, koa, nextjs: req.headers
151+
const headers = (req.headers || {}) as {
152+
host?: string;
153+
cookie?: string;
154+
};
155+
// method:
156+
// node, express, koa, nextjs: req.method
157+
const method = req.method;
158+
// host:
159+
// express: req.hostname in > 4 and req.host in < 4
160+
// koa: req.host
161+
// node, nextjs: req.headers.host
162+
const host = req.hostname || req.host || headers.host || '<no host>';
163+
// protocol:
164+
// node, nextjs: <n/a>
165+
// express, koa: req.protocol
166+
const protocol = req.protocol === 'https' || (req.socket && req.socket.encrypted) ? 'https' : 'http';
167+
// url (including path and query string):
168+
// node, express: req.originalUrl
169+
// koa, nextjs: req.url
170+
const originalUrl = req.originalUrl || req.url || '';
171+
// absolute url
172+
const absoluteUrl = `${protocol}://${host}${originalUrl}`;
173+
include.forEach(key => {
174+
switch (key) {
175+
case 'headers': {
176+
requestData.headers = headers;
177+
break;
178+
}
179+
case 'method': {
180+
requestData.method = method;
181+
break;
182+
}
183+
case 'url': {
184+
requestData.url = absoluteUrl;
185+
break;
186+
}
187+
case 'cookies': {
188+
// cookies:
189+
// node, express, koa: req.headers.cookie
190+
// vercel, sails.js, express (w/ cookie middleware), nextjs: req.cookies
191+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
192+
requestData.cookies =
193+
// TODO (v8 / #5257): We're only sending the empty object for backwards compatibility, so the last bit can
194+
// come off in v8
195+
req.cookies || (headers.cookie && cookie.parse(headers.cookie)) || {};
196+
break;
197+
}
198+
case 'query_string': {
199+
// query string:
200+
// node: req.url (raw)
201+
// express, koa, nextjs: req.query
202+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
203+
requestData.query_string = extractQueryParams(req);
204+
break;
205+
}
206+
case 'data': {
207+
if (method === 'GET' || method === 'HEAD') {
208+
break;
209+
}
210+
// body data:
211+
// express, koa, nextjs: req.body
212+
//
213+
// when using node by itself, you have to read the incoming stream(see
214+
// https://nodejs.dev/learn/get-http-request-body-data-using-nodejs); if a user is doing that, we can't know
215+
// where they're going to store the final result, so they'll have to capture this data themselves
216+
if (req.body !== undefined) {
217+
requestData.data = isString(req.body) ? req.body : JSON.stringify(normalize(req.body));
218+
}
219+
break;
220+
}
221+
default: {
222+
if ({}.hasOwnProperty.call(req, key)) {
223+
requestData[key] = (req as { [key: string]: unknown })[key];
224+
}
225+
}
226+
}
227+
});
228+
229+
return requestData;
230+
}
231+
232+
/**
233+
* Add data from the given request to the given event
234+
*
235+
* @param event The event to which the request data will be added
236+
* @param req Request object
237+
* @param options.include Flags to control what data is included
238+
*
239+
* @returns The mutated `Event` object
240+
*/
241+
export function addRequestDataToEvent(
242+
event: Event,
243+
req: PolymorphicRequest,
244+
options?: AddRequestDataToEventOptions,
245+
): Event {
246+
const include = {
247+
...DEFAULT_INCLUDES,
248+
...options?.include,
249+
};
250+
251+
if (include.request) {
252+
const extractedRequestData = Array.isArray(include.request)
253+
? extractRequestData(req, { include: include.request })
254+
: extractRequestData(req);
255+
256+
event.request = {
257+
...event.request,
258+
...extractedRequestData,
259+
};
260+
}
261+
262+
if (include.user) {
263+
const extractedUser = req.user && isPlainObject(req.user) ? extractUserData(req.user, include.user) : {};
264+
265+
if (Object.keys(extractedUser).length) {
266+
event.user = {
267+
...event.user,
268+
...extractedUser,
269+
};
270+
}
271+
}
272+
273+
// client ip:
274+
// node, nextjs: req.socket.remoteAddress
275+
// express, koa: req.ip
276+
if (include.ip) {
277+
const ip = req.ip || (req.socket && req.socket.remoteAddress);
278+
if (ip) {
279+
event.user = {
280+
...event.user,
281+
ip_address: ip,
282+
};
283+
}
284+
}
285+
286+
if (include.transaction && !event.transaction) {
287+
// TODO do we even need this anymore?
288+
// TODO make this work for nextjs
289+
event.transaction = extractTransaction(req, include.transaction);
290+
}
291+
292+
return event;
293+
}
294+
295+
function extractQueryParams(req: PolymorphicRequest): string | Record<string, unknown> | undefined {
296+
// url (including path and query string):
297+
// node, express: req.originalUrl
298+
// koa, nextjs: req.url
299+
let originalUrl = req.originalUrl || req.url || '';
300+
301+
if (!originalUrl) {
302+
return;
303+
}
304+
305+
// The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and
306+
// hostname on the beginning. Since the point here is just to grab the query string, it doesn't matter what we use.
307+
if (originalUrl.startsWith('/')) {
308+
originalUrl = `http://dogs.are.great${originalUrl}`;
309+
}
310+
311+
return (
312+
req.query ||
313+
(typeof URL !== undefined && new URL(originalUrl).search.replace('?', '')) ||
314+
// In Node 8, `URL` isn't in the global scope, so we have to use the built-in module from Node
315+
url.parse(originalUrl).query ||
316+
undefined
317+
);
318+
}

0 commit comments

Comments
 (0)