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