Skip to content

Commit a6e2642

Browse files
authored
ref(sveltekit): Remove custom client fetch instrumentation and use default instrumentation (#8802)
Remove our custom SvelteKit client fetch instrumentation which we created when we initially worked on the SDK. Back then I didn't think that it's possible to use our default fetch instrumentation from `BrowserTracing`, due to timing issues where Kit would store away `window.fetch` (and use the stored version in `load` functions) before our SDK was initialized. After receiving some [hints](sveltejs/kit#9530 (comment)) how it might be possible, we now have a way to instrument `fetch` everywhere on the client (including the one in `load`) functions. This works in two parts: 1. During the initial page load request, our server-side `handle` wrapper injects a script into the returned HTML that wraps `window.fetch` and adds a proxy handler (`window._sentryFetchProxy`) that at this time just forwards the fetch call to the original fetch. After this script is evaluated by the browser, at some point, SvelteKit loads its initial client-side bundle that stores away `window.fetch`. Kit also patches `window.fetch` itself at this time. Sometime later, the code from the `hooks.client.js` file is evaluated in the browser, including our `Sentry.init` call: 2. During `Sentry.init` we now swap `window.fetch` with `window._sentryFetchProxy` which will make our `BrowserTracing` integration patch our proxy with our default fetch instrumentation. After the init, we swap the two fetches back and we're done.
1 parent 490631e commit a6e2642

File tree

11 files changed

+146
-761
lines changed

11 files changed

+146
-761
lines changed

packages/sveltekit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"build:types": "tsc -p tsconfig.types.json",
4545
"build:watch": "run-p build:transpile:watch build:types:watch",
4646
"build:dev:watch": "yarn build:watch",
47-
"build:transpile:watch": "rollup -c rollup.npm.config.js --watch",
47+
"build:transpile:watch": "rollup -c rollup.npm.config.js --bundleConfigAsCjs --watch",
4848
"build:types:watch": "tsc -p tsconfig.types.json --watch",
4949
"build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build",
5050
"circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts",

packages/sveltekit/src/client/load.ts

Lines changed: 2 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,10 @@
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';
1+
import { trace } from '@sentry/core';
52
import { captureException } from '@sentry/svelte';
6-
import type { Client, ClientOptions, SanitizedRequestData, Span } from '@sentry/types';
7-
import {
8-
addExceptionMechanism,
9-
addNonEnumerableProperty,
10-
getSanitizedUrlString,
11-
objectify,
12-
parseFetchArgs,
13-
parseUrl,
14-
stringMatchesSomePattern,
15-
} from '@sentry/utils';
3+
import { addExceptionMechanism, addNonEnumerableProperty, objectify } from '@sentry/utils';
164
import type { LoadEvent } from '@sveltejs/kit';
175

186
import type { SentryWrappedFlag } from '../common/utils';
197
import { isRedirect } from '../common/utils';
20-
import { isRequestCached } from './vendor/lookUpCache';
218

229
type PatchedLoadEvent = LoadEvent & Partial<SentryWrappedFlag>;
2310

@@ -80,7 +67,6 @@ export function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T)
8067

8168
const patchedEvent: PatchedLoadEvent = {
8269
...event,
83-
fetch: instrumentSvelteKitFetch(event.fetch),
8470
};
8571

8672
addNonEnumerableProperty(patchedEvent as unknown as Record<string, unknown>, '__sentry_wrapped__', true);
@@ -101,182 +87,3 @@ export function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T)
10187
},
10288
});
10389
}
104-
105-
type SvelteKitFetch = LoadEvent['fetch'];
106-
107-
/**
108-
* Instruments SvelteKit's client `fetch` implementation which is passed to the client-side universal `load` functions.
109-
*
110-
* We need to instrument this in addition to the native fetch we instrument in BrowserTracing because SvelteKit
111-
* stores the native fetch implementation before our SDK is initialized.
112-
*
113-
* see: https://github.com/sveltejs/kit/blob/master/packages/kit/src/runtime/client/fetcher.js
114-
*
115-
* This instrumentation takes the fetch-related options from `BrowserTracing` to determine if we should
116-
* instrument fetch for perfomance monitoring, create a span for or attach our tracing headers to the given request.
117-
*
118-
* To dertermine if breadcrumbs should be recorded, this instrumentation relies on the availability of and the options
119-
* set in the `BreadCrumbs` integration.
120-
*
121-
* @param originalFetch SvelteKit's original fetch implemenetation
122-
*
123-
* @returns a proxy of SvelteKit's fetch implementation
124-
*/
125-
function instrumentSvelteKitFetch(originalFetch: SvelteKitFetch): SvelteKitFetch {
126-
const client = getCurrentHub().getClient();
127-
128-
if (!isValidClient(client)) {
129-
return originalFetch;
130-
}
131-
132-
const options = client.getOptions();
133-
134-
const browserTracingIntegration = client.getIntegrationById('BrowserTracing') as BrowserTracing | undefined;
135-
const breadcrumbsIntegration = client.getIntegrationById('Breadcrumbs') as Breadcrumbs | undefined;
136-
137-
const browserTracingOptions = browserTracingIntegration && browserTracingIntegration.options;
138-
139-
const shouldTraceFetch = browserTracingOptions && browserTracingOptions.traceFetch;
140-
const shouldAddFetchBreadcrumb = breadcrumbsIntegration && breadcrumbsIntegration.options.fetch;
141-
142-
/* Identical check as in BrowserTracing, just that we also need to verify that BrowserTracing is actually installed */
143-
const shouldCreateSpan =
144-
browserTracingOptions && typeof browserTracingOptions.shouldCreateSpanForRequest === 'function'
145-
? browserTracingOptions.shouldCreateSpanForRequest
146-
: (_: string) => shouldTraceFetch;
147-
148-
/* Identical check as in BrowserTracing, just that we also need to verify that BrowserTracing is actually installed */
149-
const shouldAttachHeaders: (url: string) => boolean = url => {
150-
return (
151-
!!shouldTraceFetch &&
152-
stringMatchesSomePattern(
153-
url,
154-
options.tracePropagationTargets || browserTracingOptions.tracePropagationTargets || ['localhost', /^\//],
155-
)
156-
);
157-
};
158-
159-
return new Proxy(originalFetch, {
160-
apply: (wrappingTarget, thisArg, args: Parameters<LoadEvent['fetch']>) => {
161-
const [input, init] = args;
162-
163-
if (isRequestCached(input, init)) {
164-
return wrappingTarget.apply(thisArg, args);
165-
}
166-
167-
const { url: rawUrl, method } = parseFetchArgs(args);
168-
169-
// TODO: extract this to a util function (and use it in breadcrumbs integration as well)
170-
if (rawUrl.match(/sentry_key/)) {
171-
// We don't create spans or breadcrumbs for fetch requests that contain `sentry_key` (internal sentry requests)
172-
return wrappingTarget.apply(thisArg, args);
173-
}
174-
175-
const urlObject = parseUrl(rawUrl);
176-
177-
const requestData: SanitizedRequestData = {
178-
url: getSanitizedUrlString(urlObject),
179-
'http.method': method,
180-
...(urlObject.search && { 'http.query': urlObject.search.substring(1) }),
181-
...(urlObject.hash && { 'http.hash': urlObject.hash.substring(1) }),
182-
};
183-
184-
const patchedInit: RequestInit = { ...init };
185-
const hub = getCurrentHub();
186-
const scope = hub.getScope();
187-
const client = hub.getClient();
188-
189-
let fetchPromise: Promise<Response>;
190-
191-
function callFetchTarget(span?: Span): Promise<Response> {
192-
if (client && shouldAttachHeaders(rawUrl)) {
193-
const headers = addTracingHeadersToFetchRequest(input as string | Request, client, scope, patchedInit, span);
194-
patchedInit.headers = headers as HeadersInit;
195-
}
196-
const patchedFetchArgs = [input, patchedInit];
197-
return wrappingTarget.apply(thisArg, patchedFetchArgs);
198-
}
199-
200-
if (shouldCreateSpan(rawUrl)) {
201-
fetchPromise = trace(
202-
{
203-
name: `${method} ${requestData.url}`, // this will become the description of the span
204-
op: 'http.client',
205-
data: requestData,
206-
},
207-
span => {
208-
const promise = callFetchTarget(span);
209-
if (span) {
210-
promise.then(res => span.setHttpStatus(res.status)).catch(_ => span.setStatus('internal_error'));
211-
}
212-
return promise;
213-
},
214-
);
215-
} else {
216-
fetchPromise = callFetchTarget();
217-
}
218-
219-
if (shouldAddFetchBreadcrumb) {
220-
addFetchBreadcrumb(fetchPromise, requestData, args);
221-
}
222-
223-
return fetchPromise;
224-
},
225-
});
226-
}
227-
228-
/* Adds a breadcrumb for the given fetch result */
229-
function addFetchBreadcrumb(
230-
fetchResult: Promise<Response>,
231-
requestData: SanitizedRequestData,
232-
args: Parameters<SvelteKitFetch>,
233-
): void {
234-
const breadcrumbStartTimestamp = Date.now();
235-
fetchResult.then(
236-
response => {
237-
getCurrentHub().addBreadcrumb(
238-
{
239-
type: 'http',
240-
category: 'fetch',
241-
data: {
242-
...requestData,
243-
status_code: response.status,
244-
},
245-
},
246-
{
247-
input: args,
248-
response,
249-
startTimestamp: breadcrumbStartTimestamp,
250-
endTimestamp: Date.now(),
251-
},
252-
);
253-
},
254-
error => {
255-
getCurrentHub().addBreadcrumb(
256-
{
257-
type: 'http',
258-
category: 'fetch',
259-
level: 'error',
260-
data: requestData,
261-
},
262-
{
263-
input: args,
264-
data: error,
265-
startTimestamp: breadcrumbStartTimestamp,
266-
endTimestamp: Date.now(),
267-
},
268-
);
269-
},
270-
);
271-
}
272-
273-
type MaybeClientWithGetIntegrationsById =
274-
| (Client & { getIntegrationById?: BaseClient<ClientOptions>['getIntegrationById'] })
275-
| undefined;
276-
277-
type ClientWithGetIntegrationById = Required<MaybeClientWithGetIntegrationsById> &
278-
Exclude<MaybeClientWithGetIntegrationsById, undefined>;
279-
280-
function isValidClient(client: MaybeClientWithGetIntegrationsById): client is ClientWithGetIntegrationById {
281-
return !!client && typeof client.getIntegrationById === 'function';
282-
}

packages/sveltekit/src/client/sdk.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { hasTracingEnabled } from '@sentry/core';
22
import type { BrowserOptions } from '@sentry/svelte';
3-
import { BrowserTracing, configureScope, init as initSvelteSdk } from '@sentry/svelte';
3+
import { BrowserTracing, configureScope, init as initSvelteSdk, WINDOW } from '@sentry/svelte';
44
import { addOrUpdateIntegration } from '@sentry/utils';
55

66
import { applySdkMetadata } from '../common/metadata';
77
import { svelteKitRoutingInstrumentation } from './router';
88

9+
type WindowWithSentryFetchProxy = typeof WINDOW & {
10+
_sentryFetchProxy?: typeof fetch;
11+
};
12+
913
// Treeshakable guard to remove all code related to tracing
1014
declare const __SENTRY_TRACING__: boolean;
1115

@@ -19,8 +23,17 @@ export function init(options: BrowserOptions): void {
1923

2024
addClientIntegrations(options);
2125

26+
// 1. Switch window.fetch to our fetch proxy we injected earlier
27+
const actualFetch = switchToFetchProxy();
28+
29+
// 2. Initialize the SDK which will instrument our proxy
2230
initSvelteSdk(options);
2331

32+
// 3. Restore the original fetch now that our proxy is instrumented
33+
if (actualFetch) {
34+
restoreFetch(actualFetch);
35+
}
36+
2437
configureScope(scope => {
2538
scope.setTag('runtime', 'browser');
2639
});
@@ -45,3 +58,43 @@ function addClientIntegrations(options: BrowserOptions): void {
4558

4659
options.integrations = integrations;
4760
}
61+
62+
/**
63+
* During server-side page load, we injected a <script> that wraps `window.fetch` so that
64+
* before a `fetch` call is forwarded to the original `window.fetch`, a function we control
65+
* is also invoked. This function is put onto the global object (`window._sentryFetchProxy`)
66+
* so that we can access it here.
67+
*
68+
* In this function we briefly set our fetch proxy as `window.fetch` so that the SDK can
69+
* instrument it.
70+
*
71+
* After initializing the SDK, `restoreFetch` must be called to put back whatever was on `window.fetch` before.
72+
*
73+
* @see ../server/handle.ts (https://github.com/getsentry/sentry-javascript/blob/8d92180c900c2ac98fd127d53703a415c1f191dd/packages/sveltekit/src/server/handle.ts#L60-L81)
74+
*
75+
* @returns the function that was previously on `window.fetch`.
76+
*/
77+
function switchToFetchProxy(): typeof fetch | undefined {
78+
const globalWithSentryFetchProxy: WindowWithSentryFetchProxy = WINDOW as WindowWithSentryFetchProxy;
79+
80+
// eslint-disable-next-line @typescript-eslint/unbound-method
81+
const actualFetch = globalWithSentryFetchProxy.fetch;
82+
83+
if (globalWithSentryFetchProxy._sentryFetchProxy && actualFetch) {
84+
globalWithSentryFetchProxy.fetch = globalWithSentryFetchProxy._sentryFetchProxy;
85+
return actualFetch;
86+
}
87+
return undefined;
88+
}
89+
90+
/**
91+
* Restores the function @param actualFetch to `window.fetch`
92+
* and puts our fetch proxy back onto `window._sentryFetchProxy`.
93+
*/
94+
function restoreFetch(actualFetch: typeof fetch): void {
95+
const globalWithSentryFetchProxy: WindowWithSentryFetchProxy = WINDOW as WindowWithSentryFetchProxy;
96+
97+
// eslint-disable-next-line @typescript-eslint/unbound-method
98+
globalWithSentryFetchProxy._sentryFetchProxy = globalWithSentryFetchProxy.fetch;
99+
globalWithSentryFetchProxy.fetch = actualFetch;
100+
}

packages/sveltekit/src/client/vendor/buildSelector.ts

Lines changed: 0 additions & 57 deletions
This file was deleted.

0 commit comments

Comments
 (0)