diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d2b856a7cb37..e5b271a71fff 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,6 +2,7 @@ export type { ClientClass } from './sdk'; export type { AsyncContextStrategy, Carrier, Layer, RunWithAsyncContextOptions } from './hub'; export type { OfflineStore, OfflineTransportOptions } from './transports/offline'; export type { ServerRuntimeClientOptions } from './server-runtime-client'; +export type { RequestDataIntegrationOptions } from './integrations/requestdata'; export * from './tracing'; export { createEventEnvelope } from './envelope'; @@ -56,6 +57,7 @@ export { hasTracingEnabled } from './utils/hasTracingEnabled'; export { isSentryRequestUrl } from './utils/isSentryRequestUrl'; export { DEFAULT_ENVIRONMENT } from './constants'; export { ModuleMetadata } from './integrations/metadata'; +export { RequestData } from './integrations/requestdata'; import * as Integrations from './integrations'; export { Integrations }; diff --git a/packages/node/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts similarity index 94% rename from packages/node/src/integrations/requestdata.ts rename to packages/core/src/integrations/requestdata.ts index 5521345a7b98..4481501d8b8c 100644 --- a/packages/node/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -1,11 +1,6 @@ -// TODO (v8 or v9): Whenever this becomes a default integration for `@sentry/browser`, move this to `@sentry/core`. For -// now, we leave it in `@sentry/integrations` so that it doesn't contribute bytes to our CDN bundles. - import type { Event, EventProcessor, Hub, Integration, PolymorphicRequest, Transaction } from '@sentry/types'; -import { extractPathForTransaction } from '@sentry/utils'; - -import type { AddRequestDataToEventOptions, TransactionNamingScheme } from '../requestdata'; -import { addRequestDataToEvent } from '../requestdata'; +import type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils'; +import { addRequestDataToEvent, extractPathForTransaction } from '@sentry/utils'; export type RequestDataIntegrationOptions = { /** @@ -59,7 +54,7 @@ export class RequestData implements Integration { /** * @inheritDoc */ - public name: string = RequestData.id; + public name: string; /** * Function for adding request data to event. Defaults to `addRequestDataToEvent` from `@sentry/node` for now, but @@ -74,6 +69,7 @@ export class RequestData implements Integration { * @inheritDoc */ public constructor(options: RequestDataIntegrationOptions = {}) { + this.name = RequestData.id; this._addRequestData = addRequestDataToEvent; this._options = { ...DEFAULT_OPTIONS, diff --git a/packages/core/test/lib/integrations/requestdata.test.ts b/packages/core/test/lib/integrations/requestdata.test.ts new file mode 100644 index 000000000000..bfa6a3caf62d --- /dev/null +++ b/packages/core/test/lib/integrations/requestdata.test.ts @@ -0,0 +1,102 @@ +import type { RequestDataIntegrationOptions } from '@sentry/core'; +import { getCurrentHub, Hub, makeMain, RequestData } from '@sentry/core'; +import type { Event, EventProcessor } from '@sentry/types'; +import * as sentryUtils from '@sentry/utils'; +import type { IncomingMessage } from 'http'; + +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; + +const addRequestDataToEventSpy = jest.spyOn(sentryUtils, 'addRequestDataToEvent'); +const requestDataEventProcessor = jest.fn(); + +const headers = { ears: 'furry', nose: 'wet', tongue: 'spotted', cookie: 'favorite=zukes' }; +const method = 'wagging'; +const protocol = 'mutualsniffing'; +const hostname = 'the.dog.park'; +const path = '/by/the/trees/'; +const queryString = 'chase=me&please=thankyou'; + +function initWithRequestDataIntegrationOptions(integrationOptions: RequestDataIntegrationOptions): void { + const setMockEventProcessor = (eventProcessor: EventProcessor) => + requestDataEventProcessor.mockImplementationOnce(eventProcessor); + + const requestDataIntegration = new RequestData({ + ...integrationOptions, + }); + + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', + integrations: [requestDataIntegration], + }), + ); + client.setupIntegrations = () => requestDataIntegration.setupOnce(setMockEventProcessor, getCurrentHub); + client.getIntegration = () => requestDataIntegration as any; + + const hub = new Hub(client); + + makeMain(hub); +} + +describe('`RequestData` integration', () => { + let req: IncomingMessage, event: Event; + + beforeEach(() => { + req = { + headers, + method, + protocol, + hostname, + originalUrl: `${path}?${queryString}`, + } as unknown as IncomingMessage; + event = { sdkProcessingMetadata: { request: req } }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('option conversion', () => { + it('leaves `ip` and `user` at top level of `include`', () => { + initWithRequestDataIntegrationOptions({ include: { ip: false, user: true } }); + + requestDataEventProcessor(event); + + const passedOptions = addRequestDataToEventSpy.mock.calls[0][2]; + + expect(passedOptions?.include).toEqual(expect.objectContaining({ ip: false, user: true })); + }); + + it('moves `transactionNamingScheme` to `transaction` include', () => { + initWithRequestDataIntegrationOptions({ transactionNamingScheme: 'path' }); + + requestDataEventProcessor(event); + + const passedOptions = addRequestDataToEventSpy.mock.calls[0][2]; + + expect(passedOptions?.include).toEqual(expect.objectContaining({ transaction: 'path' })); + }); + + it('moves `true` request keys into `request` include, but omits `false` ones', async () => { + initWithRequestDataIntegrationOptions({ include: { data: true, cookies: false } }); + + requestDataEventProcessor(event); + + const passedOptions = addRequestDataToEventSpy.mock.calls[0][2]; + + expect(passedOptions?.include?.request).toEqual(expect.arrayContaining(['data'])); + expect(passedOptions?.include?.request).not.toEqual(expect.arrayContaining(['cookies'])); + }); + + it('moves `true` user keys into `user` include, but omits `false` ones', async () => { + initWithRequestDataIntegrationOptions({ include: { user: { id: true, email: false } } }); + + requestDataEventProcessor(event); + + const passedOptions = addRequestDataToEventSpy.mock.calls[0][2]; + + expect(passedOptions?.include?.user).toEqual(expect.arrayContaining(['id'])); + expect(passedOptions?.include?.user).not.toEqual(expect.arrayContaining(['email'])); + }); + }); +}); diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index ab6dd6325ce6..6297fc931d02 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -15,6 +15,7 @@ import { addRequestDataToTransaction, dropUndefinedKeys, extractPathForTransaction, + extractRequestData, isString, isThenable, logger, @@ -24,7 +25,6 @@ import { import type * as http from 'http'; import type { NodeClient } from './client'; -import { extractRequestData } from './requestdata'; // TODO (v8 / XXX) Remove this import import type { ParseRequestOptions } from './requestDataDeprecated'; import { isAutoSessionTrackingEnabled } from './sdk'; diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 3c4a28489aa8..04aba567cf2c 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -18,9 +18,8 @@ export type { Transaction, User, } from '@sentry/types'; -export type { AddRequestDataToEventOptions } from '@sentry/utils'; +export type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils'; -export type { TransactionNamingScheme } from './requestdata'; export type { NodeOptions } from './types'; export { @@ -72,7 +71,7 @@ export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing'; export { NodeClient } from './client'; export { makeNodeTransport } from './transports'; export { defaultIntegrations, init, defaultStackParser, getSentryRelease } from './sdk'; -export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from './requestdata'; +export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from '@sentry/utils'; export { deepReadDirSync } from './utils'; export { getModuleFromFilename } from './module'; export { enableAnrDetection } from './anr'; diff --git a/packages/node/src/integrations/index.ts b/packages/node/src/integrations/index.ts index 62e7b58e85b2..8597ffc00a4a 100644 --- a/packages/node/src/integrations/index.ts +++ b/packages/node/src/integrations/index.ts @@ -5,6 +5,6 @@ export { OnUnhandledRejection } from './onunhandledrejection'; export { Modules } from './modules'; export { ContextLines } from './contextlines'; export { Context } from './context'; -export { RequestData } from './requestdata'; +export { RequestData } from '@sentry/core'; export { LocalVariables } from './localvariables'; export { Undici } from './undici'; diff --git a/packages/node/src/requestDataDeprecated.ts b/packages/node/src/requestDataDeprecated.ts index 2a45e71f8e47..74e0a9c98666 100644 --- a/packages/node/src/requestDataDeprecated.ts +++ b/packages/node/src/requestDataDeprecated.ts @@ -7,9 +7,8 @@ /* eslint-disable deprecation/deprecation */ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { Event, ExtractedNodeRequestData, PolymorphicRequest } from '@sentry/types'; - -import type { AddRequestDataToEventOptions } from './requestdata'; -import { addRequestDataToEvent, extractRequestData as _extractRequestData } from './requestdata'; +import type { AddRequestDataToEventOptions } from '@sentry/utils'; +import { addRequestDataToEvent, extractRequestData as _extractRequestData } from '@sentry/utils'; /** * @deprecated `Handlers.ExpressRequest` is deprecated and will be removed in v8. Use `PolymorphicRequest` instead. diff --git a/packages/node/src/requestdata.ts b/packages/node/src/requestdata.ts deleted file mode 100644 index 4d464ba13825..000000000000 --- a/packages/node/src/requestdata.ts +++ /dev/null @@ -1,326 +0,0 @@ -import type { - Event, - ExtractedNodeRequestData, - PolymorphicRequest, - Transaction, - TransactionSource, -} from '@sentry/types'; -import { isPlainObject, isString, normalize, stripUrlQueryAndFragment } from '@sentry/utils'; -import * as url from 'url'; - -import { parseCookie } from './cookie'; - -const DEFAULT_INCLUDES = { - ip: false, - request: true, - transaction: true, - user: true, -}; -const DEFAULT_REQUEST_INCLUDES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url']; -export const DEFAULT_USER_INCLUDES = ['id', 'username', 'email']; - -/** - * Options deciding what parts of the request to use when enhancing an event - */ -export type 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]>; - }; -}; - -export 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) || ''; - } - case 'methodPath': - default: { - // if exist _reconstructedRoute return that path instead of route.path - const customRoute = req._reconstructedRoute ? req._reconstructedRoute : undefined; - return extractPathForTransaction(req, { path: true, method: true, customRoute })[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 || ''; - // protocol: - // node, nextjs: - // 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 = originalUrl.startsWith(protocol) ? originalUrl : `${protocol}://${host}${originalUrl}`; - include.forEach(key => { - switch (key) { - case 'headers': { - requestData.headers = headers; - - // Remove the Cookie header in case cookie data should not be included in the event - if (!include.includes('cookies')) { - delete (requestData.headers as { cookie?: string }).cookie; - } - - 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 - 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 && parseCookie(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 | 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 || new url.URL(originalUrl).search.replace('?', '') || undefined; -} diff --git a/packages/node/test/integrations/requestdata.test.ts b/packages/node/test/integrations/requestdata.test.ts index 52e20c9d6e4b..4a9adc729488 100644 --- a/packages/node/test/integrations/requestdata.test.ts +++ b/packages/node/test/integrations/requestdata.test.ts @@ -1,15 +1,14 @@ -import { getCurrentHub, Hub, makeMain } from '@sentry/core'; +import type { RequestDataIntegrationOptions } from '@sentry/core'; +import { getCurrentHub, Hub, makeMain, RequestData } from '@sentry/core'; import type { Event, EventProcessor, PolymorphicRequest } from '@sentry/types'; +import * as sentryUtils from '@sentry/utils'; import * as http from 'http'; import { NodeClient } from '../../src/client'; import { requestHandler } from '../../src/handlers'; -import type { RequestDataIntegrationOptions } from '../../src/integrations/requestdata'; -import { RequestData } from '../../src/integrations/requestdata'; -import * as requestDataModule from '../../src/requestdata'; import { getDefaultNodeClientOptions } from '../helper/node-client-options'; -const addRequestDataToEventSpy = jest.spyOn(requestDataModule, 'addRequestDataToEvent'); +const addRequestDataToEventSpy = jest.spyOn(sentryUtils, 'addRequestDataToEvent'); const requestDataEventProcessor = jest.fn(); const headers = { ears: 'furry', nose: 'wet', tongue: 'spotted', cookie: 'favorite=zukes' }; @@ -59,50 +58,6 @@ describe('`RequestData` integration', () => { jest.clearAllMocks(); }); - describe('option conversion', () => { - it('leaves `ip` and `user` at top level of `include`', () => { - initWithRequestDataIntegrationOptions({ include: { ip: false, user: true } }); - - requestDataEventProcessor(event); - - const passedOptions = addRequestDataToEventSpy.mock.calls[0][2]; - - expect(passedOptions?.include).toEqual(expect.objectContaining({ ip: false, user: true })); - }); - - it('moves `transactionNamingScheme` to `transaction` include', () => { - initWithRequestDataIntegrationOptions({ transactionNamingScheme: 'path' }); - - requestDataEventProcessor(event); - - const passedOptions = addRequestDataToEventSpy.mock.calls[0][2]; - - expect(passedOptions?.include).toEqual(expect.objectContaining({ transaction: 'path' })); - }); - - it('moves `true` request keys into `request` include, but omits `false` ones', async () => { - initWithRequestDataIntegrationOptions({ include: { data: true, cookies: false } }); - - requestDataEventProcessor(event); - - const passedOptions = addRequestDataToEventSpy.mock.calls[0][2]; - - expect(passedOptions?.include?.request).toEqual(expect.arrayContaining(['data'])); - expect(passedOptions?.include?.request).not.toEqual(expect.arrayContaining(['cookies'])); - }); - - it('moves `true` user keys into `user` include, but omits `false` ones', async () => { - initWithRequestDataIntegrationOptions({ include: { user: { id: true, email: false } } }); - - requestDataEventProcessor(event); - - const passedOptions = addRequestDataToEventSpy.mock.calls[0][2]; - - expect(passedOptions?.include?.user).toEqual(expect.arrayContaining(['id'])); - expect(passedOptions?.include?.user).not.toEqual(expect.arrayContaining(['email'])); - }); - }); - describe('usage with express request handler and GCP wrapper', () => { it('uses options from Express request handler', async () => { const sentryRequestMiddleware = requestHandler({ include: { transaction: 'methodPath' } }); diff --git a/packages/node/test/requestdata.test.ts b/packages/node/test/requestdata.test.ts index b73b5de2d985..989aa680e2d9 100644 --- a/packages/node/test/requestdata.test.ts +++ b/packages/node/test/requestdata.test.ts @@ -1,16 +1,16 @@ /* eslint-disable deprecation/deprecation */ -// TODO (v8 / #5257): Remove everything related to the deprecated functions +// TODO (v8 / #5257): Remove everything related to the deprecated functions and move tests into `@sentry/utils` import type { Event, PolymorphicRequest, TransactionSource, User } from '@sentry/types'; -import type * as net from 'net'; - -import type { AddRequestDataToEventOptions } from '../src/requestdata'; +import type { AddRequestDataToEventOptions } from '@sentry/utils'; import { addRequestDataToEvent, extractPathForTransaction, extractRequestData as newExtractRequestData, -} from '../src/requestdata'; +} from '@sentry/utils'; +import type * as net from 'net'; + import type { ExpressRequest } from '../src/requestDataDeprecated'; import { extractRequestData as oldExtractRequestData, parseRequest } from '../src/requestDataDeprecated'; diff --git a/packages/node/src/cookie.ts b/packages/utils/src/cookie.ts similarity index 100% rename from packages/node/src/cookie.ts rename to packages/utils/src/cookie.ts diff --git a/packages/utils/src/requestdata.ts b/packages/utils/src/requestdata.ts index 5c8216759025..f5c39292bd54 100644 --- a/packages/utils/src/requestdata.ts +++ b/packages/utils/src/requestdata.ts @@ -1,19 +1,3 @@ -// TODO: Remove this file once equivalent integration is used everywhere - -/* eslint-disable complexity */ -/** - * The functions here, which enrich an event with request data, are mostly for use in Node, but are safe for use in a - * browser context. They live here in `@sentry/utils` rather than in `@sentry/node` so that they can be used in - * frameworks (like nextjs), which, because of SSR, run the same code in both Node and browser contexts. - * - * TODO (v8 / #5257): Remove the note below - * Note that for now, the tests for this code have to live in `@sentry/node`, since they test both these functions and - * the backwards-compatibility-preserving wrappers which still live in `handlers.ts` there. - */ - -/* eslint-disable max-lines */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - import type { Event, ExtractedNodeRequestData, @@ -22,6 +6,7 @@ import type { TransactionSource, } from '@sentry/types'; +import { parseCookie } from './cookie'; import { isPlainObject, isString } from './is'; import { normalize } from './normalize'; import { stripUrlQueryAndFragment } from './url'; @@ -33,7 +18,7 @@ const DEFAULT_INCLUDES = { user: true, }; const DEFAULT_REQUEST_INCLUDES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url']; -const DEFAULT_USER_INCLUDES = ['id', 'username', 'email']; +export const DEFAULT_USER_INCLUDES = ['id', 'username', 'email']; type InjectedNodeDeps = { cookie: { @@ -46,6 +31,33 @@ type InjectedNodeDeps = { }; }; +/** + * Options deciding what parts of the request to use when enhancing an event + */ +export type 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]>; + }; + + /** Injected platform-specific dependencies */ + deps?: { + cookie: { + parse: (cookieStr: string) => Record; + }; + url: { + parse: (urlStr: string) => { + query: string | null; + }; + }; + }; +}; + +export 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 @@ -115,8 +127,6 @@ export function extractPathForTransaction( return [name, source]; } -type TransactionNamingScheme = 'path' | 'methodPath' | 'handler'; - /** JSDoc */ function extractTransaction(req: PolymorphicRequest, type: boolean | TransactionNamingScheme): string { switch (type) { @@ -128,7 +138,9 @@ function extractTransaction(req: PolymorphicRequest, type: boolean | Transaction } case 'methodPath': default: { - return extractPathForTransaction(req, { path: true, method: true })[0]; + // if exist _reconstructedRoute return that path instead of route.path + const customRoute = req._reconstructedRoute ? req._reconstructedRoute : undefined; + return extractPathForTransaction(req, { path: true, method: true, customRoute })[0]; } } } @@ -136,11 +148,11 @@ function extractTransaction(req: PolymorphicRequest, type: boolean | Transaction /** JSDoc */ function extractUserData( user: { - [key: string]: any; + [key: string]: unknown; }, keys: boolean | string[], -): { [key: string]: any } { - const extractedUser: { [key: string]: any } = {}; +): { [key: string]: unknown } { + const extractedUser: { [key: string]: unknown } = {}; const attributes = Array.isArray(keys) ? keys : DEFAULT_USER_INCLUDES; attributes.forEach(key => { @@ -194,11 +206,17 @@ export function extractRequestData( // koa, nextjs: req.url const originalUrl = req.originalUrl || req.url || ''; // absolute url - const absoluteUrl = `${protocol}://${host}${originalUrl}`; + const absoluteUrl = originalUrl.startsWith(protocol) ? originalUrl : `${protocol}://${host}${originalUrl}`; include.forEach(key => { switch (key) { case 'headers': { requestData.headers = headers; + + // Remove the Cookie header in case cookie data should not be included in the event + if (!include.includes('cookies')) { + delete (requestData.headers as { cookie?: string }).cookie; + } + break; } case 'method': { @@ -213,11 +231,10 @@ export function extractRequestData( // 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 && deps && deps.cookie && deps.cookie.parse(headers.cookie)) || {}; + req.cookies || (headers.cookie && parseCookie(headers.cookie)) || {}; break; } case 'query_string': { @@ -245,7 +262,7 @@ export function extractRequestData( } default: { if ({}.hasOwnProperty.call(req, key)) { - requestData[key] = (req as { [key: string]: any })[key]; + requestData[key] = (req as { [key: string]: unknown })[key]; } } } @@ -254,31 +271,6 @@ export function extractRequestData( return requestData; } -/** - * 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 | string[]; - transaction?: boolean | TransactionNamingScheme; - user?: boolean | string[]; - }; - - /** Injected platform-specific dependencies */ - deps?: { - cookie: { - parse: (cookieStr: string) => Record; - }; - url: { - parse: (urlStr: string) => { - query: string | null; - }; - }; - }; -} - /** * Add data from the given request to the given event * @@ -286,7 +278,7 @@ export interface AddRequestDataToEventOptions { * @param req Request object * @param options.include Flags to control what data is included * @param options.deps Injected platform-specific dependencies - * @hidden + * @returns The mutated `Event` object */ export function addRequestDataToEvent( event: Event, diff --git a/packages/node/test/cookie.test.ts b/packages/utils/test/cookie.test.ts similarity index 100% rename from packages/node/test/cookie.test.ts rename to packages/utils/test/cookie.test.ts