-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
16c121a
commit 9862a32
Showing
8 changed files
with
339 additions
and
108 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,318 @@ | ||
import { Event, ExtractedNodeRequestData, PolymorphicRequest, Transaction, TransactionSource } from '@sentry/types'; | ||
import { isPlainObject, isString, normalize, stripUrlQueryAndFragment } from '@sentry/utils/'; | ||
import * as cookie from 'cookie'; | ||
import * as url from 'url'; | ||
|
||
const DEFAULT_INCLUDES = { | ||
ip: false, | ||
request: true, | ||
transaction: true, | ||
user: true, | ||
}; | ||
const DEFAULT_REQUEST_INCLUDES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url']; | ||
const DEFAULT_USER_INCLUDES = ['id', 'username', 'email']; | ||
|
||
/** | ||
* Options deciding what parts of the request to use when enhancing an event | ||
*/ | ||
export interface AddRequestDataToEventOptions { | ||
/** Flags controlling whether each type of data should be added to the event */ | ||
include?: { | ||
ip?: boolean; | ||
request?: boolean | Array<typeof DEFAULT_REQUEST_INCLUDES[number]>; | ||
transaction?: boolean | TransactionNamingScheme; | ||
user?: boolean | Array<typeof DEFAULT_USER_INCLUDES[number]>; | ||
}; | ||
} | ||
|
||
type TransactionNamingScheme = 'path' | 'methodPath' | 'handler'; | ||
|
||
/** | ||
* Sets parameterized route as transaction name e.g.: `GET /users/:id` | ||
* Also adds more context data on the transaction from the request | ||
*/ | ||
export function addRequestDataToTransaction(transaction: Transaction | undefined, req: PolymorphicRequest): void { | ||
if (!transaction) return; | ||
if (!transaction.metadata.source || transaction.metadata.source === 'url') { | ||
// Attempt to grab a parameterized route off of the request | ||
transaction.setName(...extractPathForTransaction(req, { path: true, method: true })); | ||
} | ||
transaction.setData('url', req.originalUrl || req.url); | ||
if (req.baseUrl) { | ||
transaction.setData('baseUrl', req.baseUrl); | ||
} | ||
transaction.setData('query', extractQueryParams(req)); | ||
} | ||
|
||
/** | ||
* Extracts a complete and parameterized path from the request object and uses it to construct transaction name. | ||
* If the parameterized transaction name cannot be extracted, we fall back to the raw URL. | ||
* | ||
* Additionally, this function determines and returns the transaction name source | ||
* | ||
* eg. GET /mountpoint/user/:id | ||
* | ||
* @param req A request object | ||
* @param options What to include in the transaction name (method, path, or a custom route name to be | ||
* used instead of the request's route) | ||
* | ||
* @returns A tuple of the fully constructed transaction name [0] and its source [1] (can be either 'route' or 'url') | ||
*/ | ||
export function extractPathForTransaction( | ||
req: PolymorphicRequest, | ||
options: { path?: boolean; method?: boolean; customRoute?: string } = {}, | ||
): [string, TransactionSource] { | ||
const method = req.method && req.method.toUpperCase(); | ||
|
||
let path = ''; | ||
let source: TransactionSource = 'url'; | ||
|
||
// Check to see if there's a parameterized route we can use (as there is in Express) | ||
if (options.customRoute || req.route) { | ||
path = options.customRoute || `${req.baseUrl || ''}${req.route && req.route.path}`; | ||
source = 'route'; | ||
} | ||
|
||
// Otherwise, just take the original URL | ||
else if (req.originalUrl || req.url) { | ||
path = stripUrlQueryAndFragment(req.originalUrl || req.url || ''); | ||
} | ||
|
||
let name = ''; | ||
if (options.method && method) { | ||
name += method; | ||
} | ||
if (options.method && options.path) { | ||
name += ' '; | ||
} | ||
if (options.path && path) { | ||
name += path; | ||
} | ||
|
||
return [name, source]; | ||
} | ||
|
||
/** JSDoc */ | ||
function extractTransaction(req: PolymorphicRequest, type: boolean | TransactionNamingScheme): string { | ||
switch (type) { | ||
case 'path': { | ||
return extractPathForTransaction(req, { path: true })[0]; | ||
} | ||
case 'handler': { | ||
return (req.route && req.route.stack && req.route.stack[0] && req.route.stack[0].name) || '<anonymous>'; | ||
} | ||
case 'methodPath': | ||
default: { | ||
return extractPathForTransaction(req, { path: true, method: true })[0]; | ||
} | ||
} | ||
} | ||
|
||
/** JSDoc */ | ||
function extractUserData( | ||
user: { | ||
[key: string]: unknown; | ||
}, | ||
keys: boolean | string[], | ||
): { [key: string]: unknown } { | ||
const extractedUser: { [key: string]: unknown } = {}; | ||
const attributes = Array.isArray(keys) ? keys : DEFAULT_USER_INCLUDES; | ||
|
||
attributes.forEach(key => { | ||
if (user && key in user) { | ||
extractedUser[key] = user[key]; | ||
} | ||
}); | ||
|
||
return extractedUser; | ||
} | ||
|
||
/** | ||
* Normalize data from the request object | ||
* | ||
* @param req The request object from which to extract data | ||
* @param options.include An optional array of keys to include in the normalized data. Defaults to | ||
* DEFAULT_REQUEST_INCLUDES if not provided. | ||
* @param options.deps Injected, platform-specific dependencies | ||
* | ||
* @returns An object containing normalized request data | ||
*/ | ||
export function extractRequestData( | ||
req: PolymorphicRequest, | ||
options?: { | ||
include?: string[]; | ||
}, | ||
): ExtractedNodeRequestData { | ||
const { include = DEFAULT_REQUEST_INCLUDES } = options || {}; | ||
const requestData: { [key: string]: unknown } = {}; | ||
|
||
// headers: | ||
// node, express, koa, nextjs: req.headers | ||
const headers = (req.headers || {}) as { | ||
host?: string; | ||
cookie?: string; | ||
}; | ||
// method: | ||
// node, express, koa, nextjs: req.method | ||
const method = req.method; | ||
// host: | ||
// express: req.hostname in > 4 and req.host in < 4 | ||
// koa: req.host | ||
// node, nextjs: req.headers.host | ||
const host = req.hostname || req.host || headers.host || '<no host>'; | ||
// protocol: | ||
// node, nextjs: <n/a> | ||
// express, koa: req.protocol | ||
const protocol = req.protocol === 'https' || (req.socket && req.socket.encrypted) ? 'https' : 'http'; | ||
// url (including path and query string): | ||
// node, express: req.originalUrl | ||
// koa, nextjs: req.url | ||
const originalUrl = req.originalUrl || req.url || ''; | ||
// absolute url | ||
const absoluteUrl = `${protocol}://${host}${originalUrl}`; | ||
include.forEach(key => { | ||
switch (key) { | ||
case 'headers': { | ||
requestData.headers = headers; | ||
break; | ||
} | ||
case 'method': { | ||
requestData.method = method; | ||
break; | ||
} | ||
case 'url': { | ||
requestData.url = absoluteUrl; | ||
break; | ||
} | ||
case 'cookies': { | ||
// cookies: | ||
// node, express, koa: req.headers.cookie | ||
// vercel, sails.js, express (w/ cookie middleware), nextjs: req.cookies | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
requestData.cookies = | ||
// TODO (v8 / #5257): We're only sending the empty object for backwards compatibility, so the last bit can | ||
// come off in v8 | ||
req.cookies || (headers.cookie && cookie.parse(headers.cookie)) || {}; | ||
break; | ||
} | ||
case 'query_string': { | ||
// query string: | ||
// node: req.url (raw) | ||
// express, koa, nextjs: req.query | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
requestData.query_string = extractQueryParams(req); | ||
break; | ||
} | ||
case 'data': { | ||
if (method === 'GET' || method === 'HEAD') { | ||
break; | ||
} | ||
// body data: | ||
// express, koa, nextjs: req.body | ||
// | ||
// when using node by itself, you have to read the incoming stream(see | ||
// https://nodejs.dev/learn/get-http-request-body-data-using-nodejs); if a user is doing that, we can't know | ||
// where they're going to store the final result, so they'll have to capture this data themselves | ||
if (req.body !== undefined) { | ||
requestData.data = isString(req.body) ? req.body : JSON.stringify(normalize(req.body)); | ||
} | ||
break; | ||
} | ||
default: { | ||
if ({}.hasOwnProperty.call(req, key)) { | ||
requestData[key] = (req as { [key: string]: unknown })[key]; | ||
} | ||
} | ||
} | ||
}); | ||
|
||
return requestData; | ||
} | ||
|
||
/** | ||
* Add data from the given request to the given event | ||
* | ||
* @param event The event to which the request data will be added | ||
* @param req Request object | ||
* @param options.include Flags to control what data is included | ||
* | ||
* @returns The mutated `Event` object | ||
*/ | ||
export function addRequestDataToEvent( | ||
event: Event, | ||
req: PolymorphicRequest, | ||
options?: AddRequestDataToEventOptions, | ||
): Event { | ||
const include = { | ||
...DEFAULT_INCLUDES, | ||
...options?.include, | ||
}; | ||
|
||
if (include.request) { | ||
const extractedRequestData = Array.isArray(include.request) | ||
? extractRequestData(req, { include: include.request }) | ||
: extractRequestData(req); | ||
|
||
event.request = { | ||
...event.request, | ||
...extractedRequestData, | ||
}; | ||
} | ||
|
||
if (include.user) { | ||
const extractedUser = req.user && isPlainObject(req.user) ? extractUserData(req.user, include.user) : {}; | ||
|
||
if (Object.keys(extractedUser).length) { | ||
event.user = { | ||
...event.user, | ||
...extractedUser, | ||
}; | ||
} | ||
} | ||
|
||
// client ip: | ||
// node, nextjs: req.socket.remoteAddress | ||
// express, koa: req.ip | ||
if (include.ip) { | ||
const ip = req.ip || (req.socket && req.socket.remoteAddress); | ||
if (ip) { | ||
event.user = { | ||
...event.user, | ||
ip_address: ip, | ||
}; | ||
} | ||
} | ||
|
||
if (include.transaction && !event.transaction) { | ||
// TODO do we even need this anymore? | ||
// TODO make this work for nextjs | ||
event.transaction = extractTransaction(req, include.transaction); | ||
} | ||
|
||
return event; | ||
} | ||
|
||
function extractQueryParams(req: PolymorphicRequest): string | Record<string, unknown> | undefined { | ||
// url (including path and query string): | ||
// node, express: req.originalUrl | ||
// koa, nextjs: req.url | ||
let originalUrl = req.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}`; | ||
} | ||
|
||
return ( | ||
req.query || | ||
(typeof URL !== undefined && new URL(originalUrl).search.replace('?', '')) || | ||
// In Node 8, `URL` isn't in the global scope, so we have to use the built-in module from Node | ||
url.parse(originalUrl).query || | ||
undefined | ||
); | ||
} |
Oops, something went wrong.