Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref(node): Move request data functions back to@sentry/node #5759

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
);
}
Loading