Skip to content

Commit

Permalink
feat(sveltekit): Add wrapper for client load function (#7447)
Browse files Browse the repository at this point in the history
  • Loading branch information
AbhiPrasad committed Mar 14, 2023
1 parent b1ef00d commit defcb52
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 4 deletions.
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' } },
);
});
});

0 comments on commit defcb52

Please sign in to comment.