Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions packages/sveltekit/src/client/index.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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' } },
);
});
});