Skip to content

Commit

Permalink
ref(node): Move request data functions back to@sentry/node (#5759)
Browse files Browse the repository at this point in the history
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
lobsterkatie committed Sep 17, 2022
1 parent 16c121a commit 9862a32
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 108 deletions.
3 changes: 2 additions & 1 deletion packages/node/src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import * as domain from 'domain';
import * as http from 'http';

import { NodeClient } from './client';
import { addRequestDataToEvent, extractRequestData } from './requestdata';
// TODO (v8 / XXX) Remove these imports
import type { ParseRequestOptions } from './requestDataDeprecated';
import { parseRequest } from './requestDataDeprecated';
import { addRequestDataToEvent, extractRequestData, flush, isAutoSessionTrackingEnabled } from './sdk';
import { flush, isAutoSessionTrackingEnabled } from './sdk';

/**
* Express-compatible tracing handler.
Expand Down
13 changes: 2 additions & 11 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,8 @@ export {

export { NodeClient } from './client';
export { makeNodeTransport } from './transports';
export {
addRequestDataToEvent,
extractRequestData,
defaultIntegrations,
init,
defaultStackParser,
lastEventId,
flush,
close,
getSentryRelease,
} from './sdk';
export { defaultIntegrations, init, defaultStackParser, lastEventId, flush, close, getSentryRelease } from './sdk';
export { addRequestDataToEvent, extractRequestData } from './requestdata';
export { deepReadDirSync } from './utils';

import { Integrations as CoreIntegrations } from '@sentry/core';
Expand Down
9 changes: 4 additions & 5 deletions packages/node/src/requestDataDeprecated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@
/* eslint-disable deprecation/deprecation */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Event, ExtractedNodeRequestData, PolymorphicRequest } from '@sentry/types';

import {
addRequestDataToEvent,
AddRequestDataToEventOptions,
extractRequestData as _extractRequestData,
} from '@sentry/utils';
import * as cookie from 'cookie';
import * as url from 'url';
} from './requestdata';

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

/**
Expand All @@ -55,5 +54,5 @@ export type ParseRequestOptions = AddRequestDataToEventOptions['include'] & {
* @hidden
*/
export function parseRequest(event: Event, req: ExpressRequest, options: ParseRequestOptions = {}): Event {
return addRequestDataToEvent(event, req, { include: options, deps: { cookie, url } });
return addRequestDataToEvent(event, req, { include: options });
}
318 changes: 318 additions & 0 deletions packages/node/src/requestdata.ts
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
);
}

0 comments on commit 9862a32

Please sign in to comment.