Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(sveltekit): Add partial instrumentation for client-side fetch#7626
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,17 @@ | ||
| import { trace } from '@sentry/core'; | ||
| import { addTracingHeadersToFetchRequest } from '@sentry-internal/tracing'; | ||
| import type { BaseClient } from '@sentry/core'; | ||
| import { getCurrentHub, trace } from '@sentry/core'; | ||
| import type { Breadcrumbs, BrowserTracing } from '@sentry/svelte'; | ||
| import { captureException } from '@sentry/svelte'; | ||
| import { addExceptionMechanism, objectify } from '@sentry/utils'; | ||
| import type { ClientOptions, SanitizedRequestData } from '@sentry/types'; | ||
| import { | ||
| addExceptionMechanism, | ||
| getSanitizedUrlString, | ||
| objectify, | ||
| parseFetchArgs, | ||
| parseUrl, | ||
| stringMatchesSomePattern, | ||
| } from '@sentry/utils'; | ||
| import type { LoadEvent } from '@sveltejs/kit'; | ||
| import { isRedirect } from '../common/utils'; | ||
| @@ -34,7 +45,17 @@ function sendErrorToSentry(e: unknown): unknown { | ||
| } | ||
| /** | ||
| * @inheritdoc | ||
| * Wrap load function with Sentry. This wrapper will | ||
| * | ||
| * - catch errors happening during the execution of `load` | ||
| * - create a load span if performance monitoring is enabled | ||
| * - attach tracing Http headers to `fech` requests if performance monitoring is enabled to get connected traces. | ||
| * - add a fetch breadcrumb for every `fetch` request | ||
| * | ||
| * Note that tracing Http headers are only attached if the url matches the specified `tracePropagationTargets` | ||
| * entries to avoid CORS errors. | ||
| * | ||
| * @param origLoad SvelteKit user defined load function | ||
| */ | ||
| // The liberal generic typing of `T` is necessary because we cannot let T extend `Load`. | ||
| // 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) | ||
| // Type casting here because `T` cannot extend `Load` (see comment above function signature) | ||
| const event = args[0] as LoadEvent; | ||
| const patchedEvent = { | ||
| ...event, | ||
| fetch: instrumentSvelteKitFetch(event.fetch), | ||
Lms24 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| }; | ||
| const routeId = event.route.id; | ||
| return trace( | ||
| { | ||
| @@ -57,9 +83,179 @@ export function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T) | ||
| source: routeId ? 'route' : 'url', | ||
| }, | ||
| }, | ||
| () => wrappingTarget.apply(thisArg, args), | ||
| () => wrappingTarget.apply(thisArg, [patchedEvent]), | ||
| sendErrorToSentry, | ||
| ); | ||
| }, | ||
| }); | ||
| } | ||
| type SvelteKitFetch = LoadEvent['fetch']; | ||
| /** | ||
| * Instruments SvelteKit's client `fetch` implementation which is passed to the client-side universal `load` functions. | ||
| * | ||
| * We need to instrument this in addition to the native fetch we instrument in BrowserTracing because SvelteKit | ||
| * stores the native fetch implementation before our SDK is initialized. | ||
| * | ||
| * see: https://github.com/sveltejs/kit/blob/master/packages/kit/src/runtime/client/fetcher.js | ||
| * | ||
| * This instrumentation takes the fetch-related options from `BrowserTracing` to determine if we should | ||
| * instrument fetch for perfomance monitoring, create a span for or attach our tracing headers to the given request. | ||
| * | ||
| * To dertermine if breadcrumbs should be recorded, this instrumentation relies on the availability of and the options | ||
| * set in the `BreadCrumbs` integration. | ||
| * | ||
| * @param originalFetch SvelteKit's original fetch implemenetation | ||
| * | ||
| * @returns a proxy of SvelteKit's fetch implementation | ||
| */ | ||
| function instrumentSvelteKitFetch(originalFetch: SvelteKitFetch): SvelteKitFetch { | ||
| const client = getCurrentHub().getClient() as BaseClient<ClientOptions>; | ||
| const browserTracingIntegration = | ||
| client.getIntegrationById && (client.getIntegrationById('BrowserTracing') as BrowserTracing | undefined); | ||
| const breadcrumbsIntegration = | ||
| client.getIntegrationById && (client.getIntegrationById('Breadcrumbs') as Breadcrumbs | undefined); | ||
| const browserTracingOptions = browserTracingIntegration && browserTracingIntegration.options; | ||
| const shouldTraceFetch = browserTracingOptions && browserTracingOptions.traceFetch; | ||
| const shouldAddFetchBreadcrumb = breadcrumbsIntegration && breadcrumbsIntegration.options.fetch; | ||
| /* Identical check as in BrowserTracing, just that we also need to verify that BrowserTracing is actually installed */ | ||
| const shouldCreateSpan = | ||
| browserTracingOptions && typeof browserTracingOptions.shouldCreateSpanForRequest === 'function' | ||
| ? browserTracingOptions.shouldCreateSpanForRequest | ||
| : (_: string) => shouldTraceFetch; | ||
| /* Identical check as in BrowserTracing, just that we also need to verify that BrowserTracing is actually installed */ | ||
| const shouldAttachHeaders: (url: string) => boolean = url => { | ||
| return ( | ||
| !!shouldTraceFetch && | ||
| stringMatchesSomePattern(url, browserTracingOptions.tracePropagationTargets || ['localhost', /^\//]) | ||
| ); | ||
| }; | ||
| return new Proxy(originalFetch, { | ||
| apply: (wrappingTarget, thisArg, args: Parameters<LoadEvent['fetch']>) => { | ||
| const [input, init] = args; | ||
| const { url: rawUrl, method } = parseFetchArgs(args); | ||
| // TODO: extract this to a util function (and use it in breadcrumbs integration as well) | ||
| if (rawUrl.match(/sentry_key/)) { | ||
| // We don't create spans or breadcrumbs for fetch requests that contain `sentry_key` (internal sentry requests) | ||
| return wrappingTarget.apply(thisArg, args); | ||
| } | ||
| const urlObject = parseUrl(rawUrl); | ||
| const requestData: SanitizedRequestData = { | ||
| url: getSanitizedUrlString(urlObject), | ||
| method, | ||
| ...(urlObject.search && { 'http.query': urlObject.search.substring(1) }), | ||
| ...(urlObject.hash && { 'http.hash': urlObject.hash.substring(1) }), | ||
| }; | ||
| const patchedInit: RequestInit = { ...init }; | ||
| const activeSpan = getCurrentHub().getScope().getSpan(); | ||
| const activeTransaction = activeSpan && activeSpan.transaction; | ||
| const createSpan = activeTransaction && shouldCreateSpan(rawUrl); | ||
| const attachHeaders = createSpan && activeTransaction && shouldAttachHeaders(rawUrl); | ||
| // only attach headers if we should create a span | ||
| if (attachHeaders) { | ||
| const dsc = activeTransaction.getDynamicSamplingContext(); | ||
| const headers = addTracingHeadersToFetchRequest( | ||
| input as string | Request, | ||
| dsc, | ||
| activeSpan, | ||
| patchedInit as { | ||
| headers: | ||
| | { | ||
| [key: string]: string[] | string | undefined; | ||
| } | ||
| | Request['headers']; | ||
| }, | ||
| ) as HeadersInit; | ||
| patchedInit.headers = headers; | ||
| } | ||
| let fetchPromise: Promise<Response>; | ||
| const patchedFetchArgs = [input, patchedInit]; | ||
| if (createSpan) { | ||
| fetchPromise = trace( | ||
| { | ||
| name: `${requestData.method} ${requestData.url}`, // this will become the description of the span | ||
| op: 'http.client', | ||
| data: requestData, | ||
| }, | ||
| span => { | ||
| const promise: Promise<Response> = wrappingTarget.apply(thisArg, patchedFetchArgs); | ||
| if (span) { | ||
| promise.then(res => span.setHttpStatus(res.status)).catch(_ => span.setStatus('internal_error')); | ||
| } | ||
| return promise; | ||
| }, | ||
| ); | ||
| } else { | ||
| fetchPromise = wrappingTarget.apply(thisArg, patchedFetchArgs); | ||
| } | ||
| if (shouldAddFetchBreadcrumb) { | ||
| addFetchBreadcrumb(fetchPromise, requestData, args); | ||
| } | ||
| return fetchPromise; | ||
| }, | ||
| }); | ||
| } | ||
| /* Adds a breadcrumb for the given fetch result */ | ||
| function addFetchBreadcrumb( | ||
| fetchResult: Promise<Response>, | ||
| requestData: SanitizedRequestData, | ||
| args: Parameters<SvelteKitFetch>, | ||
| ): void { | ||
| const breadcrumbStartTimestamp = Date.now(); | ||
| fetchResult.then( | ||
| response => { | ||
| getCurrentHub().addBreadcrumb( | ||
| { | ||
| type: 'http', | ||
| category: 'fetch', | ||
| data: { | ||
| ...requestData, | ||
| status_code: response.status, | ||
| }, | ||
| }, | ||
| { | ||
| input: args, | ||
| response, | ||
| startTimestamp: breadcrumbStartTimestamp, | ||
| endTimestamp: Date.now(), | ||
| }, | ||
| ); | ||
| }, | ||
| error => { | ||
| getCurrentHub().addBreadcrumb( | ||
| { | ||
| type: 'http', | ||
| category: 'fetch', | ||
| level: 'error', | ||
| data: requestData, | ||
| }, | ||
| { | ||
| input: args, | ||
| data: error, | ||
| startTimestamp: breadcrumbStartTimestamp, | ||
| endTimestamp: Date.now(), | ||
| }, | ||
| ); | ||
| }, | ||
| ); | ||
| } | ||
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.