11import { deepReadDirSync } from '@sentry/node' ;
2- import { hasTracingEnabled } from '@sentry/tracing' ;
3- import { Transaction } from '@sentry/types' ;
2+ import { getActiveTransaction , hasTracingEnabled } from '@sentry/tracing' ;
3+ import { Event as SentryEvent , Transaction } from '@sentry/types' ;
44import { fill , logger } from '@sentry/utils' ;
55import * as domain from 'domain' ;
66import * as http from 'http' ;
77import { default as createNextServer } from 'next' ;
8+ import * as querystring from 'querystring' ;
89import * as url from 'url' ;
910
1011import * as Sentry from '../index.server' ;
@@ -29,6 +30,8 @@ interface Server {
2930interface NextRequest extends http . IncomingMessage {
3031 cookies : Record < string , string > ;
3132 url : string ;
33+ query : { [ key : string ] : string } ;
34+ headers : { [ key : string ] : string } ;
3235}
3336
3437interface NextResponse extends http . ServerResponse {
@@ -40,11 +43,19 @@ interface NextResponse extends http.ServerResponse {
4043type HandlerGetter = ( ) => Promise < ReqHandler > ;
4144type ReqHandler = ( req : NextRequest , res : NextResponse , parsedUrl ?: url . UrlWithParsedQuery ) => Promise < void > ;
4245type ErrorLogger = ( err : Error ) => void ;
46+ type ApiPageEnsurer = ( path : string ) => Promise < void > ;
47+ type PageComponentFinder = (
48+ pathname : string ,
49+ query : querystring . ParsedUrlQuery ,
50+ params : { [ key : string ] : any } | null ,
51+ ) => Promise < { [ key : string ] : any } | null > ;
4352
4453// these aliases are purely to make the function signatures more easily understandable
4554type WrappedHandlerGetter = HandlerGetter ;
4655type WrappedErrorLogger = ErrorLogger ;
4756type WrappedReqHandler = ReqHandler ;
57+ type WrappedApiPageEnsurer = ApiPageEnsurer ;
58+ type WrappedPageComponentFinder = PageComponentFinder ;
4859
4960// TODO is it necessary for this to be an object?
5061const closure : PlainObject = { } ;
@@ -125,6 +136,12 @@ function makeWrappedHandlerGetter(origHandlerGetter: HandlerGetter): WrappedHand
125136 // to the appropriate handlers)
126137 fill ( serverPrototype , 'handleRequest' , makeWrappedReqHandler ) ;
127138
139+ // Wrap as a way to grab the parameterized request URL to use as the transaction name for API requests and page
140+ // requests, respectively. These methods are chosen because they're the first spot in the request-handling process
141+ // where the parameterized path is provided as an argument, so it's easy to grab.
142+ fill ( serverPrototype , 'ensureApiPage' , makeWrappedMethodForGettingParameterizedPath ) ;
143+ fill ( serverPrototype , 'findPageComponents' , makeWrappedMethodForGettingParameterizedPath ) ;
144+
128145 sdkSetupComplete = true ;
129146 }
130147
@@ -182,40 +199,80 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler {
182199 // local.on('error', Sentry.captureException);
183200
184201 local . run ( ( ) => {
185- // We only want to record page and API requests
186- if ( hasTracingEnabled ( ) && shouldTraceRequest ( req . url , publicDirFiles ) ) {
187- const transaction = Sentry . startTransaction ( {
188- name : `${ ( req . method || 'GET' ) . toUpperCase ( ) } ${ req . url } ` ,
189- op : 'http.server' ,
190- } ) ;
191- Sentry . getCurrentHub ( )
192- . getScope ( )
193- ?. setSpan ( transaction ) ;
194-
195- res . __sentry__ = { transaction } ;
196-
197- res . once ( 'finish' , ( ) => {
198- const transaction = res . __sentry__ ?. transaction ;
199- if ( transaction ) {
200- // Push `transaction.finish` to the next event loop so open spans have a chance to finish before the transaction
201- // closes
202- setImmediate ( ( ) => {
203- // TODO
204- // addExpressReqToTransaction(transaction, req);
202+ const currentScope = Sentry . getCurrentHub ( ) . getScope ( ) ;
203+
204+ if ( currentScope ) {
205+ currentScope . addEventProcessor ( event => addRequestDataToEvent ( event , req ) ) ;
206+
207+ // We only want to record page and API requests
208+ if ( hasTracingEnabled ( ) && shouldTraceRequest ( req . url , publicDirFiles ) ) {
209+ // pull off query string, if any
210+ const reqPath = req . url . split ( '?' ) [ 0 ] ;
211+
212+ // requests for pages will only ever be GET requests, so don't bother to include the method in the transaction
213+ // name; requests to API routes could be GET, POST, PUT, etc, so do include it there
214+ const namePrefix = req . url . startsWith ( '/api' ) ? `${ ( req . method || 'GET' ) . toUpperCase ( ) } ` : '' ;
215+
216+ const transaction = Sentry . startTransaction ( {
217+ name : `${ namePrefix } ${ reqPath } ` ,
218+ op : 'http.server' ,
219+ metadata : { requestPath : req . url . split ( '?' ) [ 0 ] } ,
220+ } ) ;
221+
222+ currentScope . setSpan ( transaction ) ;
223+
224+ res . once ( 'finish' , ( ) => {
225+ const transaction = getActiveTransaction ( ) ;
226+ if ( transaction ) {
205227 transaction . setHttpStatus ( res . statusCode ) ;
206- transaction . finish ( ) ;
207- } ) ;
208- }
209- } ) ;
210228
211- return origReqHandler . call ( this , req , res , parsedUrl ) ;
229+ // we'll collect this data in a more targeted way in the event processor we added above,
230+ // `addRequestDataToEvent`
231+ delete transaction . metadata . requestPath ;
232+
233+ // Push `transaction.finish` to the next event loop so open spans have a chance to finish before the
234+ // transaction closes
235+ setImmediate ( ( ) => {
236+ transaction . finish ( ) ;
237+ } ) ;
238+ }
239+ } ) ;
240+ }
212241 }
242+
243+ return origReqHandler . call ( this , req , res , parsedUrl ) ;
213244 } ) ;
214245 } ;
215246
216247 return wrappedReqHandler ;
217248}
218249
250+ /**
251+ * Wrap the given method in order to use the parameterized path passed to it in the transaction name.
252+ *
253+ * @param origMethod Either `ensureApiPage` (called for every API request) or `findPageComponents` (called for every
254+ * page request), both from the `Server` class
255+ * @returns A wrapped version of the given method
256+ */
257+ function makeWrappedMethodForGettingParameterizedPath (
258+ origMethod : ApiPageEnsurer | PageComponentFinder ,
259+ ) : WrappedApiPageEnsurer | WrappedPageComponentFinder {
260+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
261+ const wrappedMethod = async function ( this : Server , parameterizedPath : string , ...args : any [ ] ) : Promise < any > {
262+ const transaction = getActiveTransaction ( ) ;
263+
264+ // replace specific URL with parameterized version
265+ if ( transaction && transaction . metadata . requestPath ) {
266+ const origPath = transaction . metadata . requestPath ;
267+ transaction . name = transaction . name . replace ( origPath , parameterizedPath ) ;
268+ }
269+
270+ return origMethod . call ( this , parameterizedPath , ...args ) ;
271+ } ;
272+
273+ return wrappedMethod ;
274+ }
275+
219276/**
220277 * Determine if the request should be traced, by filtering out requests for internal next files and static resources.
221278 *
@@ -228,3 +285,17 @@ function shouldTraceRequest(url: string, publicDirFiles: Set<string>): boolean {
228285 // `static` is a deprecated but still-functional location for static resources
229286 return ! url . startsWith ( '/_next/' ) && ! url . startsWith ( '/static/' ) && ! publicDirFiles . has ( url ) ;
230287}
288+
289+ function addRequestDataToEvent ( event : SentryEvent , req : NextRequest ) : SentryEvent {
290+ event . request = {
291+ ...event . request ,
292+ // TODO body/data
293+ url : req . url . split ( '?' ) [ 0 ] ,
294+ cookies : req . cookies ,
295+ headers : req . headers ,
296+ method : req . method ,
297+ query_string : req . query ,
298+ } ;
299+
300+ return event ;
301+ }
0 commit comments