1
- import { trace } from '@sentry/core' ;
1
+ import type { BaseClient } from '@sentry/core' ;
2
+ import { getCurrentHub , trace } from '@sentry/core' ;
3
+ import type { Breadcrumbs , BrowserTracing } from '@sentry/svelte' ;
2
4
import { captureException } from '@sentry/svelte' ;
3
- import { addExceptionMechanism , objectify } from '@sentry/utils' ;
5
+ import type { ClientOptions } from '@sentry/types' ;
6
+ import {
7
+ addExceptionMechanism ,
8
+ addTracingHeadersToFetchRequest ,
9
+ getFetchMethod ,
10
+ getFetchUrl ,
11
+ objectify ,
12
+ stringMatchesSomePattern ,
13
+ stripUrlQueryAndFragment ,
14
+ } from '@sentry/utils' ;
4
15
import type { LoadEvent } from '@sveltejs/kit' ;
5
16
6
17
function sendErrorToSentry ( e : unknown ) : unknown {
@@ -27,7 +38,17 @@ function sendErrorToSentry(e: unknown): unknown {
27
38
}
28
39
29
40
/**
30
- * @inheritdoc
41
+ * Wrap load function with Sentry. This wrapper will
42
+ *
43
+ * - catch errors happening during the execution of `load`
44
+ * - create a load span if performance monitoring is enabled
45
+ * - attach tracing Http headers to `fech` requests if performance monitoring is enabled to get connected traces.
46
+ * - add a fetch breadcrumb for every `fetch` request
47
+ *
48
+ * Note that tracing Http headers are only attached if the url matches the specified `tracePropagationTargets`
49
+ * entries to avoid CORS errors.
50
+ *
51
+ * @param origLoad SvelteKit user defined load function
31
52
*/
32
53
// The liberal generic typing of `T` is necessary because we cannot let T extend `Load`.
33
54
// This function needs to tell TS that it returns exactly the type that it was called with
@@ -40,6 +61,11 @@ export function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T)
40
61
// Type casting here because `T` cannot extend `Load` (see comment above function signature)
41
62
const event = args [ 0 ] as LoadEvent ;
42
63
64
+ const patchedEvent = {
65
+ ...event ,
66
+ fetch : instrumentSvelteKitFetch ( event . fetch ) ,
67
+ } ;
68
+
43
69
const routeId = event . route . id ;
44
70
return trace (
45
71
{
@@ -50,9 +76,175 @@ export function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T)
50
76
source : routeId ? 'route' : 'url' ,
51
77
} ,
52
78
} ,
53
- ( ) => wrappingTarget . apply ( thisArg , args ) ,
79
+ ( ) => wrappingTarget . apply ( thisArg , [ patchedEvent ] ) ,
54
80
sendErrorToSentry ,
55
81
) ;
56
82
} ,
57
83
} ) ;
58
84
}
85
+
86
+ type SvelteKitFetch = LoadEvent [ 'fetch' ] ;
87
+
88
+ /**
89
+ * Instruments SvelteKit's client `fetch` implementation which is passed to the client-side universal `load` functions.
90
+ *
91
+ * We need to instrument this in addition to the native fetch we instrument in BrowserTracing because SvelteKit
92
+ * stores the native fetch implementation before our SDK is initialized.
93
+ *
94
+ * see: https://github.com/sveltejs/kit/blob/master/packages/kit/src/runtime/client/fetcher.js
95
+ *
96
+ * This instrumentation takes the fetch-related options from `BrowserTracing` to determine if we should
97
+ * instrument fetch for perfomance monitoring, create a span for or attach our tracing headers to the given request.
98
+ *
99
+ * To dertermine if breadcrumbs should be recorded, this instrumentation relies on the availability of and the options
100
+ * set in the `BreadCrumbs` integration.
101
+ *
102
+ * @param originalFetch SvelteKit's original fetch implemenetation
103
+ *
104
+ * @returns a proxy of SvelteKit's fetch implementation
105
+ */
106
+ function instrumentSvelteKitFetch ( originalFetch : SvelteKitFetch ) : SvelteKitFetch {
107
+ const client = getCurrentHub ( ) . getClient ( ) as BaseClient < ClientOptions > ;
108
+
109
+ const browserTracingIntegration =
110
+ client . getIntegrationById && ( client . getIntegrationById ( 'BrowserTracing' ) as BrowserTracing | undefined ) ;
111
+ const breadcrumbsIntegration = client . getIntegrationById ( 'BreadCrumbs' ) as Breadcrumbs | undefined ;
112
+
113
+ const browserTracingOptions = browserTracingIntegration && browserTracingIntegration . options ;
114
+
115
+ const shouldTraceFetch = browserTracingOptions && browserTracingOptions . traceFetch ;
116
+ const shouldAddFetchBreadcrumbs = breadcrumbsIntegration && breadcrumbsIntegration . options . fetch ;
117
+
118
+ /* Identical check as in BrowserTracing, just that we also need to verify that BrowserTracing is actually installed */
119
+ const shouldCreateSpan =
120
+ browserTracingOptions && typeof browserTracingOptions . shouldCreateSpanForRequest === 'function'
121
+ ? browserTracingOptions . shouldCreateSpanForRequest
122
+ : ( _ : string ) => shouldTraceFetch ;
123
+
124
+ /* Identical check as in BrowserTracing, just that we also need to verify that BrowserTracing is actually installed */
125
+ const shouldAttachHeaders : ( url : string ) => boolean = url => {
126
+ return (
127
+ ! ! shouldTraceFetch &&
128
+ stringMatchesSomePattern ( url , browserTracingOptions . tracePropagationTargets || [ 'localhost' , / ^ \/ / ] )
129
+ ) ;
130
+ } ;
131
+
132
+ return new Proxy ( originalFetch , {
133
+ apply : ( wrappingTarget , thisArg , args : Parameters < LoadEvent [ 'fetch' ] > ) => {
134
+ const [ input , init ] = args ;
135
+ const rawUrl = getFetchUrl ( args ) ;
136
+ const sanitizedUrl = stripUrlQueryAndFragment ( rawUrl ) ;
137
+ const method = getFetchMethod ( args ) ;
138
+
139
+ // TODO: extract this to a util function (and use it in breadcrumbs integration as well)
140
+ if ( rawUrl . match ( / s e n t r y _ k e y / ) && method === 'POST' ) {
141
+ // We will not create breadcrumbs for fetch requests that contain `sentry_key` (internal sentry requests)
142
+ return wrappingTarget . apply ( thisArg , args ) ;
143
+ }
144
+
145
+ const patchedInit : RequestInit = { ...init } || { } ;
146
+ const activeSpan = getCurrentHub ( ) . getScope ( ) . getSpan ( ) ;
147
+ const activeTransaction = activeSpan && activeSpan . transaction ;
148
+
149
+ const attachHeaders = shouldAttachHeaders ( rawUrl ) ;
150
+ const attachSpan = shouldCreateSpan ( rawUrl ) ;
151
+
152
+ if ( attachHeaders && attachSpan && activeTransaction ) {
153
+ const dsc = activeTransaction . getDynamicSamplingContext ( ) ;
154
+ const headers = addTracingHeadersToFetchRequest (
155
+ input as string | Request ,
156
+ dsc ,
157
+ activeSpan ,
158
+ patchedInit as {
159
+ headers :
160
+ | {
161
+ [ key : string ] : string [ ] | string | undefined ;
162
+ }
163
+ | Request [ 'headers' ] ;
164
+ } ,
165
+ ) as HeadersInit ;
166
+ patchedInit . headers = headers ;
167
+ }
168
+
169
+ let fetchPromise : Promise < Response > ;
170
+
171
+ if ( attachSpan ) {
172
+ fetchPromise = trace (
173
+ {
174
+ name : `${ method } ${ sanitizedUrl } ` , // this will become the description of the span
175
+ op : 'http.client' ,
176
+ data : {
177
+ /* TODO: extract query data (we might actually only do this once we tackle sanitization on the browser-side) */
178
+ } ,
179
+ parentSpanId : activeSpan && activeSpan . spanId ,
180
+ } ,
181
+ async span => {
182
+ const fetchResult : Response = await wrappingTarget . apply ( thisArg , [ input , patchedInit ] ) ;
183
+ if ( span ) {
184
+ span . setHttpStatus ( fetchResult . status ) ;
185
+ }
186
+ return fetchResult ;
187
+ } ,
188
+ ) ;
189
+ } else {
190
+ fetchPromise = wrappingTarget . apply ( thisArg , [ input , patchedInit ] ) ;
191
+ }
192
+
193
+ if ( shouldAddFetchBreadcrumbs ) {
194
+ addFetchBreadcrumbs ( fetchPromise , method , sanitizedUrl , args ) ;
195
+ }
196
+
197
+ return fetchPromise ;
198
+ } ,
199
+ } ) ;
200
+ }
201
+
202
+ /* Adds breadcrumbs for the given fetch result */
203
+ function addFetchBreadcrumbs (
204
+ fetchResult : Promise < Response > ,
205
+ method : string ,
206
+ sanitizedUrl : string ,
207
+ args : Parameters < SvelteKitFetch > ,
208
+ ) : void {
209
+ const breadcrumbStartTimestamp = Date . now ( ) ;
210
+ fetchResult . then (
211
+ response => {
212
+ getCurrentHub ( ) . addBreadcrumb (
213
+ {
214
+ type : 'http' ,
215
+ category : 'fetch' ,
216
+ data : {
217
+ method : method ,
218
+ url : sanitizedUrl ,
219
+ status_code : response . status ,
220
+ } ,
221
+ } ,
222
+ {
223
+ input : args ,
224
+ response,
225
+ startTimestamp : breadcrumbStartTimestamp ,
226
+ endTimestamp : Date . now ( ) ,
227
+ } ,
228
+ ) ;
229
+ } ,
230
+ error => {
231
+ getCurrentHub ( ) . addBreadcrumb (
232
+ {
233
+ type : 'http' ,
234
+ category : 'fetch' ,
235
+ level : 'error' ,
236
+ data : {
237
+ method : method ,
238
+ url : sanitizedUrl ,
239
+ } ,
240
+ } ,
241
+ {
242
+ input : args ,
243
+ data : error ,
244
+ startTimestamp : breadcrumbStartTimestamp ,
245
+ endTimestamp : Date . now ( ) ,
246
+ } ,
247
+ ) ;
248
+ } ,
249
+ ) ;
250
+ }
0 commit comments