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