Skip to content

Commit 0314949

Browse files
committed
feat(sveltekit): Add instrumentation for client-side fetch
properly sanitize url, add url data apply data to breadcrumbs cleanup set span status add tests sorry fix authority url "fix" authority regex again cleanup move addTracingHeadersToFetchRequest back to tracing-internal cleanup apply suggestions re-activate request object test adjust types after exporting
1 parent 8ccb82d commit 0314949

File tree

11 files changed

+631
-63
lines changed

11 files changed

+631
-63
lines changed

packages/node/src/integrations/http.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Hub } from '@sentry/core';
22
import { getCurrentHub } from '@sentry/core';
3-
import type { EventProcessor, Integration, Span, TracePropagationTargets } from '@sentry/types';
3+
import type { EventProcessor, Integration, SanitizedRequestData, Span, TracePropagationTargets } from '@sentry/types';
44
import { dynamicSamplingContextToSentryBaggageHeader, fill, logger, stringMatchesSomePattern } from '@sentry/utils';
55
import type * as http from 'http';
66
import type * as https from 'https';
@@ -122,16 +122,6 @@ type OriginalRequestMethod = RequestMethod;
122122
type WrappedRequestMethod = RequestMethod;
123123
type WrappedRequestMethodFactory = (original: OriginalRequestMethod) => WrappedRequestMethod;
124124

125-
/**
126-
* See https://develop.sentry.dev/sdk/data-handling/#structuring-data
127-
*/
128-
type RequestSpanData = {
129-
url: string;
130-
method: string;
131-
'http.fragment'?: string;
132-
'http.query'?: string;
133-
};
134-
135125
/**
136126
* Function which creates a function which creates wrapped versions of internal `request` and `get` calls within `http`
137127
* and `https` modules. (NB: Not a typo - this is a creator^2!)
@@ -197,7 +187,7 @@ function _createWrappedRequestMethodFactory(
197187

198188
const scope = getCurrentHub().getScope();
199189

200-
const requestSpanData: RequestSpanData = {
190+
const requestSpanData: SanitizedRequestData = {
201191
url: requestUrl,
202192
method: requestOptions.method || 'GET',
203193
};
@@ -304,7 +294,7 @@ function _createWrappedRequestMethodFactory(
304294
*/
305295
function addRequestBreadcrumb(
306296
event: string,
307-
requestSpanData: RequestSpanData,
297+
requestSpanData: SanitizedRequestData,
308298
req: http.ClientRequest,
309299
res?: http.IncomingMessage,
310300
): void {
@@ -316,7 +306,6 @@ function addRequestBreadcrumb(
316306
{
317307
category: 'http',
318308
data: {
319-
method: req.method,
320309
status_code: res && res.statusCode,
321310
...requestSpanData,
322311
},

packages/sveltekit/src/client/load.ts

Lines changed: 200 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
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';
25
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';
415
import type { LoadEvent } from '@sveltejs/kit';
516

617
import { 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(/sentry_key/)) {
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

Comments
 (0)