1- /* eslint-disable max-lines */
21/* eslint-disable @typescript-eslint/no-explicit-any */
32import { captureException , getCurrentHub , startTransaction , withScope } from '@sentry/core' ;
4- import { Event , ExtractedNodeRequestData , Span , Transaction } from '@sentry/types' ;
3+ import { Event , Span } from '@sentry/types' ;
54import {
5+ AddRequestDataToEventOptions ,
6+ addRequestDataToTransaction ,
7+ extractPathForTransaction ,
68 extractTraceparentData ,
7- isPlainObject ,
89 isString ,
910 logger ,
10- normalize ,
1111 parseBaggageSetMutability ,
12- stripUrlQueryAndFragment ,
1312} from '@sentry/utils' ;
14- import * as cookie from 'cookie' ;
1513import * as domain from 'domain' ;
1614import * as http from 'http' ;
17- import * as url from 'url' ;
1815
1916import { NodeClient } from './client' ;
20- import { flush , isAutoSessionTrackingEnabled } from './sdk' ;
21-
22- export interface ExpressRequest {
23- baseUrl ?: string ;
24- connection ?: {
25- remoteAddress ?: string ;
26- } ;
27- ip ?: string ;
28- method ?: string ;
29- originalUrl ?: string ;
30- route ?: {
31- path : string ;
32- stack : [
33- {
34- name : string ;
35- } ,
36- ] ;
37- } ;
38- query ?: {
39- // It can be: undefined | string | string[] | ParsedQs | ParsedQs[] (from `qs` package), but we dont want to pull it.
40- [ key : string ] : unknown ;
41- } ;
42- url ?: string ;
43- user ?: {
44- [ key : string ] : any ;
45- } ;
46- }
17+ import { addRequestDataToEvent , extractRequestData , flush , isAutoSessionTrackingEnabled } from './sdk' ;
4718
4819/**
4920 * Express-compatible tracing handler.
@@ -68,7 +39,7 @@ export function tracingHandler(): (
6839
6940 const transaction = startTransaction (
7041 {
71- name : extractExpressTransactionName ( req , { path : true , method : true } ) ,
42+ name : extractPathForTransaction ( req , { path : true , method : true } ) ,
7243 op : 'http.server' ,
7344 ...traceparentData ,
7445 metadata : { baggage : baggage } ,
@@ -91,7 +62,7 @@ export function tracingHandler(): (
9162 // Push `transaction.finish` to the next event loop so open spans have a chance to finish before the transaction
9263 // closes
9364 setImmediate ( ( ) => {
94- addExpressReqToTransaction ( transaction , req ) ;
65+ addRequestDataToTransaction ( transaction , req ) ;
9566 transaction . setHttpStatus ( res . statusCode ) ;
9667 transaction . finish ( ) ;
9768 } ) ;
@@ -101,263 +72,7 @@ export function tracingHandler(): (
10172 } ;
10273}
10374
104- /**
105- * Set parameterized as transaction name e.g.: `GET /users/:id`
106- * Also adds more context data on the transaction from the request
107- */
108- function addExpressReqToTransaction ( transaction : Transaction | undefined , req : ExpressRequest ) : void {
109- if ( ! transaction ) return ;
110- transaction . name = extractExpressTransactionName ( req , { path : true , method : true } ) ;
111- transaction . setData ( 'url' , req . originalUrl ) ;
112- transaction . setData ( 'baseUrl' , req . baseUrl ) ;
113- transaction . setData ( 'query' , req . query ) ;
114- }
115-
116- /**
117- * Extracts complete generalized path from the request object and uses it to construct transaction name.
118- *
119- * eg. GET /mountpoint/user/:id
120- *
121- * @param req The ExpressRequest object
122- * @param options What to include in the transaction name (method, path, or both)
123- *
124- * @returns The fully constructed transaction name
125- */
126- function extractExpressTransactionName (
127- req : ExpressRequest ,
128- options : { path ?: boolean ; method ?: boolean } = { } ,
129- ) : string {
130- const method = req . method ?. toUpperCase ( ) ;
131-
132- let path = '' ;
133- if ( req . route ) {
134- path = `${ req . baseUrl || '' } ${ req . route . path } ` ;
135- } else if ( req . originalUrl || req . url ) {
136- path = stripUrlQueryAndFragment ( req . originalUrl || req . url || '' ) ;
137- }
138-
139- let info = '' ;
140- if ( options . method && method ) {
141- info += method ;
142- }
143- if ( options . method && options . path ) {
144- info += ' ' ;
145- }
146- if ( options . path && path ) {
147- info += path ;
148- }
149-
150- return info ;
151- }
152-
153- type TransactionNamingScheme = 'path' | 'methodPath' | 'handler' ;
154-
155- /** JSDoc */
156- function extractTransaction ( req : ExpressRequest , type : boolean | TransactionNamingScheme ) : string {
157- switch ( type ) {
158- case 'path' : {
159- return extractExpressTransactionName ( req , { path : true } ) ;
160- }
161- case 'handler' : {
162- return req . route ?. stack [ 0 ] . name || '<anonymous>' ;
163- }
164- case 'methodPath' :
165- default : {
166- return extractExpressTransactionName ( req , { path : true , method : true } ) ;
167- }
168- }
169- }
170-
171- /** Default user keys that'll be used to extract data from the request */
172- const DEFAULT_USER_KEYS = [ 'id' , 'username' , 'email' ] ;
173-
174- /** JSDoc */
175- function extractUserData (
176- user : {
177- [ key : string ] : any ;
178- } ,
179- keys : boolean | string [ ] ,
180- ) : { [ key : string ] : any } {
181- const extractedUser : { [ key : string ] : any } = { } ;
182- const attributes = Array . isArray ( keys ) ? keys : DEFAULT_USER_KEYS ;
183-
184- attributes . forEach ( key => {
185- if ( user && key in user ) {
186- extractedUser [ key ] = user [ key ] ;
187- }
188- } ) ;
189-
190- return extractedUser ;
191- }
192-
193- /** Default request keys that'll be used to extract data from the request */
194- const DEFAULT_REQUEST_KEYS = [ 'cookies' , 'data' , 'headers' , 'method' , 'query_string' , 'url' ] ;
195-
196- /**
197- * Normalizes data from the request object, accounting for framework differences.
198- *
199- * @param req The request object from which to extract data
200- * @param keys An optional array of keys to include in the normalized data. Defaults to DEFAULT_REQUEST_KEYS if not
201- * provided.
202- * @returns An object containing normalized request data
203- */
204- export function extractRequestData (
205- req : { [ key : string ] : any } ,
206- keys : string [ ] = DEFAULT_REQUEST_KEYS ,
207- ) : ExtractedNodeRequestData {
208- const requestData : { [ key : string ] : any } = { } ;
209-
210- // headers:
211- // node, express, nextjs: req.headers
212- // koa: req.header
213- const headers = ( req . headers || req . header || { } ) as {
214- host ?: string ;
215- cookie ?: string ;
216- } ;
217- // method:
218- // node, express, koa, nextjs: req.method
219- const method = req . method ;
220- // host:
221- // express: req.hostname in > 4 and req.host in < 4
222- // koa: req.host
223- // node, nextjs: req.headers.host
224- const host = req . hostname || req . host || headers . host || '<no host>' ;
225- // protocol:
226- // node, nextjs: <n/a>
227- // express, koa: req.protocol
228- const protocol =
229- req . protocol === 'https' || req . secure || ( ( req . socket || { } ) as { encrypted ?: boolean } ) . encrypted
230- ? 'https'
231- : 'http' ;
232- // url (including path and query string):
233- // node, express: req.originalUrl
234- // koa, nextjs: req.url
235- const originalUrl = ( req . originalUrl || req . url || '' ) as string ;
236- // absolute url
237- const absoluteUrl = `${ protocol } ://${ host } ${ originalUrl } ` ;
238-
239- keys . forEach ( key => {
240- switch ( key ) {
241- case 'headers' :
242- requestData . headers = headers ;
243- break ;
244- case 'method' :
245- requestData . method = method ;
246- break ;
247- case 'url' :
248- requestData . url = absoluteUrl ;
249- break ;
250- case 'cookies' :
251- // cookies:
252- // node, express, koa: req.headers.cookie
253- // vercel, sails.js, express (w/ cookie middleware), nextjs: req.cookies
254- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
255- requestData . cookies = req . cookies || cookie . parse ( headers . cookie || '' ) ;
256- break ;
257- case 'query_string' :
258- // query string:
259- // node: req.url (raw)
260- // express, koa, nextjs: req.query
261- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
262- requestData . query_string = req . query || url . parse ( originalUrl || '' , false ) . query ;
263- break ;
264- case 'data' :
265- if ( method === 'GET' || method === 'HEAD' ) {
266- break ;
267- }
268- // body data:
269- // express, koa, nextjs: req.body
270- //
271- // when using node by itself, you have to read the incoming stream(see
272- // https://nodejs.dev/learn/get-http-request-body-data-using-nodejs); if a user is doing that, we can't know
273- // where they're going to store the final result, so they'll have to capture this data themselves
274- if ( req . body !== undefined ) {
275- requestData . data = isString ( req . body ) ? req . body : JSON . stringify ( normalize ( req . body ) ) ;
276- }
277- break ;
278- default :
279- if ( { } . hasOwnProperty . call ( req , key ) ) {
280- requestData [ key ] = ( req as { [ key : string ] : any } ) [ key ] ;
281- }
282- }
283- } ) ;
284-
285- return requestData ;
286- }
287-
288- /**
289- * Options deciding what parts of the request to use when enhancing an event
290- */
291- export interface ParseRequestOptions {
292- ip ?: boolean ;
293- request ?: boolean | string [ ] ;
294- transaction ?: boolean | TransactionNamingScheme ;
295- user ?: boolean | string [ ] ;
296- }
297-
298- /**
299- * Enriches passed event with request data.
300- *
301- * @param event Will be mutated and enriched with req data
302- * @param req Request object
303- * @param options object containing flags to enable functionality
304- * @hidden
305- */
306- export function parseRequest ( event : Event , req : ExpressRequest , options ?: ParseRequestOptions ) : Event {
307- // eslint-disable-next-line no-param-reassign
308- options = {
309- ip : false ,
310- request : true ,
311- transaction : true ,
312- user : true ,
313- ...options ,
314- } ;
315-
316- if ( options . request ) {
317- // if the option value is `true`, use the default set of keys by not passing anything to `extractRequestData()`
318- const extractedRequestData = Array . isArray ( options . request )
319- ? extractRequestData ( req , options . request )
320- : extractRequestData ( req ) ;
321- event . request = {
322- ...event . request ,
323- ...extractedRequestData ,
324- } ;
325- }
326-
327- if ( options . user ) {
328- const extractedUser = req . user && isPlainObject ( req . user ) ? extractUserData ( req . user , options . user ) : { } ;
329-
330- if ( Object . keys ( extractedUser ) ) {
331- event . user = {
332- ...event . user ,
333- ...extractedUser ,
334- } ;
335- }
336- }
337-
338- // client ip:
339- // node, nextjs: req.connection.remoteAddress
340- // express, koa: req.ip
341- if ( options . ip ) {
342- const ip = req . ip || ( req . connection && req . connection . remoteAddress ) ;
343- if ( ip ) {
344- event . user = {
345- ...event . user ,
346- ip_address : ip ,
347- } ;
348- }
349- }
350-
351- if ( options . transaction && ! event . transaction ) {
352- // TODO do we even need this anymore?
353- // TODO make this work for nextjs
354- event . transaction = extractTransaction ( req , options . transaction ) ;
355- }
356-
357- return event ;
358- }
359-
360- export type RequestHandlerOptions = ParseRequestOptions & {
75+ export type RequestHandlerOptions = AddRequestDataToEventOptions & {
36176 flushTimeout ?: number ;
36277} ;
36378
@@ -409,7 +124,7 @@ export function requestHandler(
409124 const currentHub = getCurrentHub ( ) ;
410125
411126 currentHub . configureScope ( scope => {
412- scope . addEventProcessor ( ( event : Event ) => parseRequest ( event , req , options ) ) ;
127+ scope . addEventProcessor ( ( event : Event ) => addRequestDataToEvent ( event , req , options ) ) ;
413128 const client = currentHub . getClient < NodeClient > ( ) ;
414129 if ( isAutoSessionTrackingEnabled ( client ) ) {
415130 const scope = currentHub . getScope ( ) ;
0 commit comments