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