diff --git a/packages/sveltekit/src/server/load.ts b/packages/sveltekit/src/server/load.ts index ef0433091a9e..5e773365e4a4 100644 --- a/packages/sveltekit/src/server/load.ts +++ b/packages/sveltekit/src/server/load.ts @@ -1,6 +1,14 @@ +/* eslint-disable @sentry-internal/sdk/no-optional-chaining */ +import { trace } from '@sentry/core'; import { captureException } from '@sentry/node'; -import { addExceptionMechanism, isThenable, objectify } from '@sentry/utils'; +import { + addExceptionMechanism, + baggageHeaderToDynamicSamplingContext, + extractTraceparentData, + objectify, +} from '@sentry/utils'; import type { HttpError, ServerLoad } from '@sveltejs/kit'; +import * as domain from 'domain'; function isHttpError(err: unknown): err is HttpError { return typeof err === 'object' && err !== null && 'status' in err && 'body' in err; @@ -44,21 +52,30 @@ function sendErrorToSentry(e: unknown): unknown { export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad { return new Proxy(origLoad, { apply: (wrappingTarget, thisArg, args: Parameters) => { - let maybePromiseResult; + return domain.create().bind(() => { + const [event] = args; - try { - maybePromiseResult = wrappingTarget.apply(thisArg, args); - } catch (e) { - throw sendErrorToSentry(e); - } + 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); - if (isThenable(maybePromiseResult)) { - Promise.resolve(maybePromiseResult).then(null, e => { - sendErrorToSentry(e); - }); - } - - return maybePromiseResult; + const routeId = event.route.id; + return trace( + { + op: 'function.sveltekit.load', + name: routeId ? routeId : event.url.pathname, + status: 'ok', + ...traceparentData, + metadata: { + source: routeId ? 'route' : 'url', + dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + }, + }, + () => wrappingTarget.apply(thisArg, args), + sendErrorToSentry, + ); + })(); }, }); } diff --git a/packages/sveltekit/test/server/load.test.ts b/packages/sveltekit/test/server/load.test.ts index ec2503b945c4..81b067689093 100644 --- a/packages/sveltekit/test/server/load.test.ts +++ b/packages/sveltekit/test/server/load.test.ts @@ -1,3 +1,4 @@ +import { addTracingExtensions } from '@sentry/core'; import { Scope } from '@sentry/node'; import type { ServerLoad } from '@sveltejs/kit'; import { error } from '@sveltejs/kit'; @@ -20,6 +21,19 @@ vi.mock('@sentry/node', 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 () => { @@ -34,10 +48,42 @@ 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(); }); @@ -49,12 +95,49 @@ describe('wrapLoadWithSentry', () => { } 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[0]): Promise> { + 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]', + parentSampled: true, + parentSpanId: '1234567890abcdef', + status: 'ok', + traceId: '1234567890abcdef1234567890abcdef', + metadata: { + dynamicSamplingContext: { + environment: 'production', + public_key: 'dogsarebadatkeepingsecrets', + release: '1.0.0', + sample_rate: '1', + trace_id: '1234567890abcdef1234567890abcdef', + transaction: 'dogpark', + user_segment: 'segmentA', + }, + source: 'route', + }, + }, + expect.any(Function), + expect.any(Function), + ); + }); + describe('with error() helper', () => { it.each([ // [statusCode, timesCalled] @@ -75,7 +158,7 @@ describe('wrapLoadWithSentry', () => { } 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(times); @@ -95,7 +178,7 @@ describe('wrapLoadWithSentry', () => { } 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);