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' ;
24import { 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' ;
415import type { LoadEvent } from '@sveltejs/kit' ;
516
617function sendErrorToSentry ( e : unknown ) : unknown {
@@ -27,7 +38,17 @@ function sendErrorToSentry(e: unknown): unknown {
2738}
2839
2940/**
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
3152 */
3253// The liberal generic typing of `T` is necessary because we cannot let T extend `Load`.
3354// 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)
4061 // Type casting here because `T` cannot extend `Load` (see comment above function signature)
4162 const event = args [ 0 ] as LoadEvent ;
4263
64+ const patchedEvent = {
65+ ...event ,
66+ fetch : instrumentSvelteKitFetch ( event . fetch ) ,
67+ } ;
68+
4369 const routeId = event . route . id ;
4470 return trace (
4571 {
@@ -50,9 +76,175 @@ export function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T)
5076 source : routeId ? 'route' : 'url' ,
5177 } ,
5278 } ,
53- ( ) => wrappingTarget . apply ( thisArg , args ) ,
79+ ( ) => wrappingTarget . apply ( thisArg , [ patchedEvent ] ) ,
5480 sendErrorToSentry ,
5581 ) ;
5682 } ,
5783 } ) ;
5884}
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