Skip to content

Commit

Permalink
feat(sveltekit): Add performance monitoring for client load (#7537)
Browse files Browse the repository at this point in the history
  • Loading branch information
AbhiPrasad committed Mar 22, 2023
1 parent 046c0c2 commit 09ee30b
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 25 deletions.
39 changes: 20 additions & 19 deletions packages/sveltekit/src/client/load.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { trace } from '@sentry/core';
import { captureException } from '@sentry/svelte';
import { addExceptionMechanism, isThenable, objectify } from '@sentry/utils';
import type { ServerLoad } from '@sveltejs/kit';
import { addExceptionMechanism, objectify } from '@sentry/utils';
import type { Load } 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 Down Expand Up @@ -30,24 +31,24 @@ function sendErrorToSentry(e: unknown): unknown {
*
* @param origLoad SvelteKit user defined load function
*/
export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad {
export function wrapLoadWithSentry(origLoad: Load): Load {
return new Proxy(origLoad, {
apply: (wrappingTarget, thisArg, args: Parameters<ServerLoad>) => {
let maybePromiseResult;

try {
maybePromiseResult = wrappingTarget.apply(thisArg, args);
} catch (e) {
throw sendErrorToSentry(e);
}

if (isThenable(maybePromiseResult)) {
Promise.resolve(maybePromiseResult).then(null, e => {
sendErrorToSentry(e);
});
}

return maybePromiseResult;
apply: (wrappingTarget, thisArg, args: Parameters<Load>) => {
const [event] = args;

const routeId = event.route.id;
return trace(
{
op: 'function.sveltekit.load',
name: routeId ? routeId : event.url.pathname,
status: 'ok',
metadata: {
source: routeId ? 'route' : 'url',
},
},
() => wrappingTarget.apply(thisArg, args),
sendErrorToSentry,
);
},
});
}
82 changes: 76 additions & 6 deletions packages/sveltekit/test/client/load.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Scope } from '@sentry/svelte';
import type { ServerLoad } from '@sveltejs/kit';
import { addTracingExtensions, Scope } from '@sentry/svelte';
import type { Load } from '@sveltejs/kit';
import { vi } from 'vitest';

import { wrapLoadWithSentry } from '../../src/client/load';
Expand All @@ -19,6 +19,19 @@ vi.mock('@sentry/svelte', async () => {
};
});

const mockTrace = vi.fn();

vi.mock('@sentry/core', async () => {
const original = (await vi.importActual('@sentry/core')) as any;
return {
...original,
trace: (...args: unknown[]) => {
mockTrace(...args);
return original.trace(...args);
},
};
});

const mockAddExceptionMechanism = vi.fn();

vi.mock('@sentry/utils', async () => {
Expand All @@ -33,41 +46,98 @@ function getById(_id?: string) {
throw new Error('error');
}

const MOCK_LOAD_ARGS: any = {
params: { id: '123' },
route: {
id: '/users/[id]',
},
url: new URL('http://localhost:3000/users/123'),
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;
},
},
},
};

beforeAll(() => {
addTracingExtensions();
});

describe('wrapLoadWithSentry', () => {
beforeEach(() => {
mockCaptureException.mockClear();
mockAddExceptionMechanism.mockClear();
mockTrace.mockClear();
mockScope = new Scope();
});

it('calls captureException', async () => {
async function load({ params }: Parameters<ServerLoad>[0]): Promise<ReturnType<ServerLoad>> {
async function load({ params }: Parameters<Load>[0]): Promise<ReturnType<Load>> {
return {
post: getById(params.id),
};
}

const wrappedLoad = wrapLoadWithSentry(load);
const res = wrappedLoad({ params: { id: '1' } } as any);
const res = wrappedLoad(MOCK_LOAD_ARGS);
await expect(res).rejects.toThrow();

expect(mockCaptureException).toHaveBeenCalledTimes(1);
});

it('calls trace function', async () => {
async function load({ params }: Parameters<Load>[0]): Promise<ReturnType<Load>> {
return {
post: params.id,
};
}

const wrappedLoad = wrapLoadWithSentry(load);
await wrappedLoad(MOCK_LOAD_ARGS);

expect(mockTrace).toHaveBeenCalledTimes(1);
expect(mockTrace).toHaveBeenCalledWith(
{
op: 'function.sveltekit.load',
name: '/users/[id]',
status: 'ok',
metadata: {
source: 'route',
},
},
expect.any(Function),
expect.any(Function),
);
});

it('adds an exception mechanism', async () => {
const addEventProcessorSpy = vi.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => {
void callback({}, { event_id: 'fake-event-id' });
return mockScope;
});

async function load({ params }: Parameters<ServerLoad>[0]): Promise<ReturnType<ServerLoad>> {
async function load({ params }: Parameters<Load>[0]): Promise<ReturnType<Load>> {
return {
post: getById(params.id),
};
}

const wrappedLoad = wrapLoadWithSentry(load);
const res = wrappedLoad({ params: { id: '1' } } as any);
const res = wrappedLoad(MOCK_LOAD_ARGS);
await expect(res).rejects.toThrow();

expect(addEventProcessorSpy).toBeCalledTimes(1);
Expand Down

0 comments on commit 09ee30b

Please sign in to comment.