Skip to content

Commit

Permalink
feat(sveltekit): Add partial instrumentation for client-side fetch (#…
Browse files Browse the repository at this point in the history
…7626)

Add partial instrumentation to the client-side `fetch` passed to the universal `load` functions. It enables distributed traces of fetch calls happening **inside** a `load` function. 

Limitation: `fetch` requests made by SvelteKit (e.g. to call server-only load functions) are **not** touched by this instrumentation because we cannot access the Kit-internal fetch function at this time
  • Loading branch information
Lms24 authored Apr 5, 2023
1 parent 8ccb82d commit 105dcf1
Show file tree
Hide file tree
Showing 11 changed files with 631 additions and 63 deletions.
17 changes: 3 additions & 14 deletions packages/node/src/integrations/http.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Hub } from '@sentry/core';
import { getCurrentHub } from '@sentry/core';
import type { EventProcessor, Integration, Span, TracePropagationTargets } from '@sentry/types';
import type { EventProcessor, Integration, SanitizedRequestData, Span, TracePropagationTargets } from '@sentry/types';
import { dynamicSamplingContextToSentryBaggageHeader, fill, logger, stringMatchesSomePattern } from '@sentry/utils';
import type * as http from 'http';
import type * as https from 'https';
Expand Down Expand Up @@ -122,16 +122,6 @@ type OriginalRequestMethod = RequestMethod;
type WrappedRequestMethod = RequestMethod;
type WrappedRequestMethodFactory = (original: OriginalRequestMethod) => WrappedRequestMethod;

/**
* See https://develop.sentry.dev/sdk/data-handling/#structuring-data
*/
type RequestSpanData = {
url: string;
method: string;
'http.fragment'?: string;
'http.query'?: string;
};

/**
* Function which creates a function which creates wrapped versions of internal `request` and `get` calls within `http`
* and `https` modules. (NB: Not a typo - this is a creator^2!)
Expand Down Expand Up @@ -197,7 +187,7 @@ function _createWrappedRequestMethodFactory(

const scope = getCurrentHub().getScope();

const requestSpanData: RequestSpanData = {
const requestSpanData: SanitizedRequestData = {
url: requestUrl,
method: requestOptions.method || 'GET',
};
Expand Down Expand Up @@ -304,7 +294,7 @@ function _createWrappedRequestMethodFactory(
*/
function addRequestBreadcrumb(
event: string,
requestSpanData: RequestSpanData,
requestSpanData: SanitizedRequestData,
req: http.ClientRequest,
res?: http.IncomingMessage,
): void {
Expand All @@ -316,7 +306,6 @@ function addRequestBreadcrumb(
{
category: 'http',
data: {
method: req.method,
status_code: res && res.statusCode,
...requestSpanData,
},
Expand Down
204 changes: 200 additions & 4 deletions packages/sveltekit/src/client/load.ts
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';
Expand Down Expand Up @@ -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
Expand All @@ -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),
};

const routeId = event.route.id;
return trace(
{
Expand All @@ -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(),
},
);
},
);
}
Loading

0 comments on commit 105dcf1

Please sign in to comment.