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' ;
25import { 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' ;
415import type { LoadEvent } from '@sveltejs/kit' ;
516
617import { isRedirect } from '../common/utils' ;
@@ -34,7 +45,17 @@ function sendErrorToSentry(e: unknown): unknown {
3445}
3546
3647/**
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
3859 */
3960// The liberal generic typing of `T` is necessary because we cannot let T extend `Load`.
4061// 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)
4768 // Type casting here because `T` cannot extend `Load` (see comment above function signature)
4869 const event = args [ 0 ] as LoadEvent ;
4970
71+ const patchedEvent = {
72+ ...event ,
73+ fetch : instrumentSvelteKitFetch ( event . fetch ) ,
74+ } ;
75+
5076 const routeId = event . route . id ;
5177 return trace (
5278 {
@@ -57,9 +83,179 @@ export function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T)
5783 source : routeId ? 'route' : 'url' ,
5884 } ,
5985 } ,
60- ( ) => wrappingTarget . apply ( thisArg , args ) ,
86+ ( ) => wrappingTarget . apply ( thisArg , [ patchedEvent ] ) ,
6187 sendErrorToSentry ,
6288 ) ;
6389 } ,
6490 } ) ;
6591}
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