1
+ import { deepReadDirSync } from '@sentry/node' ;
2
+ import { hasTracingEnabled } from '@sentry/tracing' ;
3
+ import { Transaction } from '@sentry/types' ;
1
4
import { fill } from '@sentry/utils' ;
2
5
import * as http from 'http' ;
3
6
import { default as createNextServer } from 'next' ;
7
+ import * as path from 'path' ;
4
8
import * as url from 'url' ;
5
9
6
10
import * as Sentry from '../index.server' ;
@@ -10,23 +14,33 @@ type PlainObject<T = any> = { [key: string]: T };
10
14
11
15
interface NextServer {
12
16
server : Server ;
17
+ createServer : ( options : PlainObject ) => Server ;
13
18
}
14
19
15
20
interface Server {
16
21
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
+ } ;
17
34
}
18
35
19
36
type 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 > ;
25
38
type ErrorLogger = ( err : Error ) => void ;
26
39
27
40
// these aliases are purely to make the function signatures more easily understandable
28
41
type WrappedHandlerGetter = HandlerGetter ;
29
42
type WrappedErrorLogger = ErrorLogger ;
43
+ type WrappedReqHandler = ReqHandler ;
30
44
31
45
// TODO is it necessary for this to be an object?
32
46
const closure : PlainObject = { } ;
@@ -61,12 +75,16 @@ function makeWrappedHandlerGetter(origHandlerGetter: HandlerGetter): WrappedHand
61
75
const wrappedHandlerGetter = async function ( this : NextServer ) : Promise < ReqHandler > {
62
76
if ( ! closure . wrappingComplete ) {
63
77
closure . projectRootDir = this . server . dir ;
78
+ closure . server = this . server ;
79
+ closure . publicDir = this . server . publicDir ;
64
80
65
81
const serverPrototype = Object . getPrototypeOf ( this . server ) ;
66
82
67
83
// wrap the logger so we can capture errors in page-level functions like `getServerSideProps`
68
84
fill ( serverPrototype , 'logError' , makeWrappedErrorLogger ) ;
69
85
86
+ fill ( serverPrototype , 'handleRequest' , makeWrappedReqHandler ) ;
87
+
70
88
closure . wrappingComplete = true ;
71
89
}
72
90
@@ -89,3 +107,77 @@ function makeWrappedErrorLogger(origErrorLogger: ErrorLogger): WrappedErrorLogge
89
107
return origErrorLogger . call ( this , err ) ;
90
108
} ;
91
109
}
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