Skip to content
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

feat(sveltekit): Add wrapper for client load function #7447

Merged
merged 10 commits into from
Mar 14, 2023
4 changes: 1 addition & 3 deletions packages/sveltekit/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@ export * from '@sentry/svelte';

export { init } from './sdk';
export { handleErrorWithSentry } from './handleError';

// Just here so that eslint is happy until we export more stuff here
export const PLACEHOLDER_CLIENT = 'PLACEHOLDER';
export { wrapLoadWithSentry } from './load';
53 changes: 53 additions & 0 deletions packages/sveltekit/src/client/load.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { captureException } from '@sentry/svelte';
import { addExceptionMechanism, isThenable, objectify } from '@sentry/utils';
import type { ServerLoad } 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
// store a seen flag on it.
const objectifiedErr = objectify(e);

captureException(objectifiedErr, scope => {
scope.addEventProcessor(event => {
addExceptionMechanism(event, {
type: 'sveltekit',
handled: false,
data: {
function: 'load',
},
});
return event;
});

return scope;
});

return objectifiedErr;
}

/**
* Wrap load function with Sentry
*
* @param origLoad SvelteKit user defined load function
*/
export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad {
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;
},
});
}
4 changes: 3 additions & 1 deletion 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 } from '@sveltejs/kit';
import type { HandleClientError, HandleServerError, ServerLoad } from '@sveltejs/kit';

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

export declare function wrapLoadWithSentry<S extends ServerLoad>(origLoad: S): S;

// 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
80 changes: 80 additions & 0 deletions packages/sveltekit/test/client/load.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Scope } from '@sentry/svelte';
import type { ServerLoad } from '@sveltejs/kit';
import { vi } from 'vitest';

import { wrapLoadWithSentry } from '../../src/client/load';

const mockCaptureException = vi.fn();
let mockScope = new Scope();

vi.mock('@sentry/svelte', async () => {
const original = (await vi.importActual('@sentry/svelte')) as any;
return {
...original,
captureException: (err: unknown, cb: (arg0: unknown) => unknown) => {
cb(mockScope);
mockCaptureException(err, cb);
return original.captureException(err, cb);
},
};
});

const mockAddExceptionMechanism = vi.fn();

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

function getById(_id?: string) {
throw new Error('error');
}

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

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

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

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

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>> {
return {
post: getById(params.id),
};
}

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

expect(addEventProcessorSpy).toBeCalledTimes(1);
expect(mockAddExceptionMechanism).toBeCalledTimes(1);
expect(mockAddExceptionMechanism).toBeCalledWith(
{},
{ handled: false, type: 'sveltekit', data: { function: 'load' } },
);
});
});