Skip to content

Commit

Permalink
ref(sveltekit): Split up universal and server page load wrappers
Browse files Browse the repository at this point in the history
  • Loading branch information
Lms24 committed Mar 29, 2023
1 parent 31eff90 commit 262ff62
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 27 deletions.
29 changes: 23 additions & 6 deletions packages/sveltekit/src/client/load.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { trace } from '@sentry/core';
import { captureException } from '@sentry/svelte';
import { addExceptionMechanism, objectify } from '@sentry/utils';
import type { Load } from '@sveltejs/kit';
import type { LoadEvent } from '@sveltejs/kit';

function sendErrorToSentry(e: unknown): unknown {
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
Expand All @@ -27,15 +27,32 @@ function sendErrorToSentry(e: unknown): unknown {
}

/**
* Wrap load function with Sentry
* TODO: usage
* Wrap a universal load function (e.g. +page.js or +layout.js) with Sentry functionality
*
* @param origLoad SvelteKit user defined load function
* Usage:
*
* ```js
* // +page.js
*
* import { wrapLoadWithSentry }
*
* export const load = wrapLoadWithSentry((event) => {
* // your load code
* });
* ```
*
* @param origLoad SvelteKit user defined universal `load` function
*/
export function wrapLoadWithSentry<T extends Load>(origLoad: T): T {
// 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
// because SvelteKit generates the narrowed down `PageLoad` or `LayoutLoad` types
// at build time for every route.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T): T {
return new Proxy(origLoad, {
apply: (wrappingTarget, thisArg, args: Parameters<T>) => {
const [event] = args;
// Type casting here because `T` cannot extend `Load` (see comment above function signature)
const event = args[0] as LoadEvent;

const routeId = event.route.id;
return trace(
Expand Down
22 changes: 20 additions & 2 deletions packages/sveltekit/src/index.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export * from './server';

import type { Integration, Options, StackParser } from '@sentry/types';
// eslint-disable-next-line import/no-unresolved
import type { HandleClientError, HandleServerError, Load } from '@sveltejs/kit';
import type { HandleClientError, HandleServerError } from '@sveltejs/kit';

import type * as clientSdk from './client';
import type * as serverSdk from './server';
Expand All @@ -21,7 +21,25 @@ export declare function handleErrorWithSentry<T extends HandleClientError | Hand
handleError?: T,
): ReturnType<T>;

export declare function wrapLoadWithSentry<T extends Load>(origLoad: T): T;
/**
* Wrap a universal load function (e.g. +page.js or +layout.js) with Sentry functionality
*
* Usage:
*
* ```js
* // +page.js
*
* import { wrapLoadWithSentry }
*
* export const load = wrapLoadWithSentry((event) => {
* // your load code
* });
* ```
*
* @param origLoad SvelteKit user defined universal `load` function
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export declare function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T): T;

// We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere.
export declare const Integrations: typeof clientSdk.Integrations & typeof serverSdk.Integrations;
Expand Down
2 changes: 1 addition & 1 deletion packages/sveltekit/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ export * from '@sentry/node';

export { init } from './sdk';
export { handleErrorWithSentry } from './handleError';
export { wrapLoadWithSentry } from './load';
export { wrapLoadWithSentry, wrapServerLoadWithSentry } from './load';
export { sentryHandle } from './handle';
48 changes: 30 additions & 18 deletions packages/sveltekit/src/server/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { trace } from '@sentry/core';
import { captureException } from '@sentry/node';
import type { TransactionContext } from '@sentry/types';
import { addExceptionMechanism, objectify } from '@sentry/utils';
import type { HttpError, Load, ServerLoad } from '@sveltejs/kit';
import type { HttpError, LoadEvent, ServerLoadEvent } from '@sveltejs/kit';

import { getTracePropagationData } from './utils';

Expand Down Expand Up @@ -42,19 +42,18 @@ function sendErrorToSentry(e: unknown): unknown {
}

/**
* Wrap a universal load function (e.g. +page.js or +layout.js) with Sentry functionality
*
* Usage:
* ```js
* import { }
* ```
*
* @param origLoad SvelteKit user defined load function
* @inheritdoc
*/
export function wrapLoadWithSentry<T extends Load>(origLoad: T): T {
// 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
// because SvelteKit generates the narrowed down `PageLoad` or `LayoutLoad` types
// at build time for every route.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T): T {
return new Proxy(origLoad, {
apply: (wrappingTarget, thisArg, args: Parameters<T>) => {
const [event] = args;
// Type casting here because `T` cannot extend `Load` (see comment above function signature)
const event = args[0] as LoadEvent;
const routeId = event.route && event.route.id;

const traceLoadContext: TransactionContext = {
Expand All @@ -73,20 +72,33 @@ export function wrapLoadWithSentry<T extends Load>(origLoad: T): T {

/**
* Wrap a server-only load function (e.g. +page.server.js or +layout.server.js) with Sentry functionality
* TODO: usage
*
* Usage:
*
* ```js
* // +page.serverjs
*
* import { wrapServerLoadWithSentry }
*
* export const load = wrapServerLoadWithSentry((event) => {
* // your load code
* });
* ```
*
* @param origServerLoad SvelteKit user defined server-only load function
*/
export function wrapServerLoadWithSentry<T extends ServerLoad>(origServerLoad: T): T {
// The liberal generic typing of `T` is necessary because we cannot let T extend `ServerLoad`.
// This function needs to tell TS that it returns exactly the type that it was called with
// because SvelteKit generates the narrowed down `PageServerLoad` or `LayoutServerLoad` types
// at build time for every route.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function wrapServerLoadWithSentry<T extends (...args: any) => any>(origServerLoad: T): T {
return new Proxy(origServerLoad, {
apply: (wrappingTarget, thisArg, args: Parameters<T>) => {
const [event] = args;
// Type casting here because `T` cannot extend `ServerLoad` (see comment above function signature)
const event = args[0] as ServerLoadEvent;
const routeId = event.route && event.route.id;

// Usually, the `handleWithSentry` hook handler should already create a transaction and store
// traceparent and DSC on that transaction before the server-only load function is called.
// However, since we have access to `event.request` we can still pass it to `trace`
// in case our handler isn't called or for some reason the handle hook is bypassed.
const { dynamicSamplingContext, traceparentData } = getTracePropagationData(event);

const traceLoadContext: TransactionContext = {
Expand Down
19 changes: 19 additions & 0 deletions packages/sveltekit/src/server/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { DynamicSamplingContext, TraceparentData } from '@sentry/types';
import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils';
import type { RequestEvent } from '@sveltejs/kit';

/**
* Takes a request event and extracts traceparent and DSC data
* from the `sentry-trace` and `baggage` DSC headers.
*/
export function getTracePropagationData(event: RequestEvent): {
traceparentData?: TraceparentData;
dynamicSamplingContext?: Partial<DynamicSamplingContext>;
} {
const sentryTraceHeader = event.request.headers.get('sentry-trace');
const baggageHeader = event.request.headers.get('baggage');
const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);

return { traceparentData, dynamicSamplingContext };
}
55 changes: 55 additions & 0 deletions packages/sveltekit/test/server/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { getTracePropagationData } from '../../src/server/utils';

const MOCK_REQUEST_EVENT: any = {
request: {
headers: {
get: (key: string) => {
if (key === 'sentry-trace') {
return '1234567890abcdef1234567890abcdef-1234567890abcdef-1';
}

if (key === 'baggage') {
return (
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,' +
'sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,' +
'sentry-trace_id=1234567890abcdef1234567890abcdef,sentry-sample_rate=1'
);
}

return null;
},
},
},
};

describe('getTracePropagationData', () => {
it('returns traceParentData and DSC if both are available', () => {
const event: any = MOCK_REQUEST_EVENT;

const { traceparentData, dynamicSamplingContext } = getTracePropagationData(event);

expect(traceparentData).toEqual({
parentSampled: true,
parentSpanId: '1234567890abcdef',
traceId: '1234567890abcdef1234567890abcdef',
});

expect(dynamicSamplingContext).toEqual({
environment: 'production',
public_key: 'dogsarebadatkeepingsecrets',
release: '1.0.0',
sample_rate: '1',
trace_id: '1234567890abcdef1234567890abcdef',
transaction: 'dogpark',
user_segment: 'segmentA',
});
});

it('returns undefined if the necessary header is not avaolable', () => {
const event: any = { request: { headers: { get: () => undefined } } };
const { traceparentData, dynamicSamplingContext } = getTracePropagationData(event);

expect(traceparentData).toBeUndefined();
expect(dynamicSamplingContext).toBeUndefined();
});
});

0 comments on commit 262ff62

Please sign in to comment.