1+ import { deepReadDirSync } from '@sentry/node' ;
2+ import { hasTracingEnabled } from '@sentry/tracing' ;
3+ import { Transaction } from '@sentry/types' ;
14import { fill } from '@sentry/utils' ;
25import * as http from 'http' ;
36import { default as createNextServer } from 'next' ;
7+ import * as path from 'path' ;
48import * as url from 'url' ;
59
610import * as Sentry from '../index.server' ;
@@ -10,23 +14,33 @@ type PlainObject<T = any> = { [key: string]: T };
1014
1115interface NextServer {
1216 server : Server ;
17+ createServer : ( options : PlainObject ) => Server ;
1318}
1419
1520interface Server {
1621 dir : string ;
22+ publicDir : string ;
23+ }
24+
25+ interface NextRequest extends http . IncomingMessage {
26+ cookies : Record < string , string > ;
27+ url : string ;
28+ }
29+
30+ interface NextResponse extends http . ServerResponse {
31+ __sentry__ : {
32+ transaction ?: Transaction ;
33+ } ;
1734}
1835
1936type HandlerGetter = ( ) => Promise < ReqHandler > ;
20- type ReqHandler = (
21- req : http . IncomingMessage ,
22- res : http . ServerResponse ,
23- parsedUrl ?: url . UrlWithParsedQuery ,
24- ) => Promise < void > ;
37+ type ReqHandler = ( req : NextRequest , res : NextResponse , parsedUrl ?: url . UrlWithParsedQuery ) => Promise < void > ;
2538type ErrorLogger = ( err : Error ) => void ;
2639
2740// these aliases are purely to make the function signatures more easily understandable
2841type WrappedHandlerGetter = HandlerGetter ;
2942type WrappedErrorLogger = ErrorLogger ;
43+ type WrappedReqHandler = ReqHandler ;
3044
3145// TODO is it necessary for this to be an object?
3246const closure : PlainObject = { } ;
@@ -61,12 +75,16 @@ function makeWrappedHandlerGetter(origHandlerGetter: HandlerGetter): WrappedHand
6175 const wrappedHandlerGetter = async function ( this : NextServer ) : Promise < ReqHandler > {
6276 if ( ! closure . wrappingComplete ) {
6377 closure . projectRootDir = this . server . dir ;
78+ closure . server = this . server ;
79+ closure . publicDir = this . server . publicDir ;
6480
6581 const serverPrototype = Object . getPrototypeOf ( this . server ) ;
6682
6783 // wrap the logger so we can capture errors in page-level functions like `getServerSideProps`
6884 fill ( serverPrototype , 'logError' , makeWrappedErrorLogger ) ;
6985
86+ fill ( serverPrototype , 'handleRequest' , makeWrappedReqHandler ) ;
87+
7088 closure . wrappingComplete = true ;
7189 }
7290
@@ -89,3 +107,77 @@ function makeWrappedErrorLogger(origErrorLogger: ErrorLogger): WrappedErrorLogge
89107 return origErrorLogger . call ( this , err ) ;
90108 } ;
91109}
110+
111+ /**
112+ * Wrap the server's request handler to be able to create request transactions.
113+ *
114+ * @param origReqHandler The original request handler from the `Server` class
115+ * @returns A wrapped version of that handler
116+ */
117+ function makeWrappedReqHandler ( origReqHandler : ReqHandler ) : WrappedReqHandler {
118+ const liveServer = closure . server as Server ;
119+
120+ // inspired by
121+ // https://github.com/vercel/next.js/blob/4443d6f3d36b107e833376c2720c1e206eee720d/packages/next/next-server/server/next-server.ts#L1166
122+ const publicDirFiles = new Set (
123+ deepReadDirSync ( liveServer . publicDir ) . map ( p =>
124+ encodeURI (
125+ // switch any backslashes in the path to regular slashes
126+ p . replace ( / \\ / g, '/' ) ,
127+ ) ,
128+ ) ,
129+ ) ;
130+
131+ // add transaction start and stop to the normal request handling
132+ const wrappedReqHandler = async function (
133+ this : Server ,
134+ req : NextRequest ,
135+ res : NextResponse ,
136+ parsedUrl ?: url . UrlWithParsedQuery ,
137+ ) : Promise < void > {
138+ // We only want to record page and API requests
139+ if ( hasTracingEnabled ( ) && shouldTraceRequest ( req . url , publicDirFiles ) ) {
140+ const transaction = Sentry . startTransaction ( {
141+ name : `${ ( req . method || 'GET' ) . toUpperCase ( ) } ${ req . url } ` ,
142+ op : 'http.server' ,
143+ } ) ;
144+ Sentry . getCurrentHub ( )
145+ . getScope ( )
146+ ?. setSpan ( transaction ) ;
147+
148+ res . __sentry__ = { } ;
149+ res . __sentry__ . transaction = transaction ;
150+ }
151+
152+ res . once ( 'finish' , ( ) => {
153+ const transaction = res . __sentry__ ?. transaction ;
154+ if ( transaction ) {
155+ // Push `transaction.finish` to the next event loop so open spans have a chance to finish before the transaction
156+ // closes
157+ setImmediate ( ( ) => {
158+ // TODO
159+ // addExpressReqToTransaction(transaction, req);
160+ transaction . setHttpStatus ( res . statusCode ) ;
161+ transaction . finish ( ) ;
162+ } ) ;
163+ }
164+ } ) ;
165+
166+ return origReqHandler . call ( this , req , res , parsedUrl ) ;
167+ } ;
168+
169+ return wrappedReqHandler ;
170+ }
171+
172+ /**
173+ * Determine if the request should be traced, by filtering out requests for internal next files and static resources.
174+ *
175+ * @param url The URL of the request
176+ * @param publicDirFiles A set containing relative paths to all available static resources (note that this does not
177+ * include static *pages*, but rather images and the like)
178+ * @returns false if the URL is for an internal or static resource
179+ */
180+ function shouldTraceRequest ( url : string , publicDirFiles : Set < string > ) : boolean {
181+ // `static` is a deprecated but still-functional location for static resources
182+ return ! url . startsWith ( '/_next/' ) && ! url . startsWith ( '/static/' ) && ! publicDirFiles . has ( url ) ;
183+ }
0 commit comments