Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';

// FIXME(sveltekit-3): server-side error capture works, but stack-frame function names are
// `load$1` (not `load`) and the request URL scheme is `https` (not `http`). Root cause: the SDK's
// Vite plugin reads native-tracing config from `svelte.config.js`, which Kit 3 removed, so it still
// injects manual load instrumentation (which Rolldown renames to `load$1`). Unskip once the SDK
// detects Kit 3 native tracing from the Vite plugin options. See repros + tracking notes.
test.describe.skip('server-side errors', () => {
test('captures universal load error', async ({ page }) => {
test.describe('server-side errors', () => {
// FIXME(sveltekit-3): the universal load function's frame is reported as `load$1` (not `load`)
// because the SDK still wraps universal `+page.ts` load in the server build. Unlike server-only
// load, this isn't suppressed by native-tracing detection: the wrapper is skipped via
// `config.build.ssr`, which is unreliable under Vite 8's Environment API. Unskip once the SDK
// detects the server environment via the Vite Environment API.
test.skip('captures universal load error', async ({ page }) => {
const errorEventPromise = waitForError('sveltekit-3', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Universal Load Error (server)';
});
Expand All @@ -31,8 +31,7 @@ test.describe.skip('server-side errors', () => {
'user-agent': expect.any(String),
}),
method: 'GET',
// SvelteKit's node adapter defaults to https in the protocol even if served on http
url: 'http://localhost:3030/universal-load-error',
url: 'https://localhost:3030/universal-load-error',
});
});

Expand Down Expand Up @@ -60,7 +59,7 @@ test.describe.skip('server-side errors', () => {
'user-agent': expect.any(String),
}),
method: 'GET',
url: 'http://localhost:3030/server-load-error',
url: 'https://localhost:3030/server-load-error',
});
});

Expand Down Expand Up @@ -90,7 +89,7 @@ test.describe.skip('server-side errors', () => {
accept: expect.any(String),
}),
method: 'GET',
url: 'http://localhost:3030/server-route-error',
url: 'https://localhost:3030/server-route-error',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/sveltekit';

// FIXME(sveltekit-3): a duplicate server-load span appears (the SDK's `function.sveltekit.server.load`
// on top of Kit 3's native `sveltekit.load`), because the SDK still injects manual load
// instrumentation when it can't detect native tracing (config moved out of `svelte.config.js` in
// Kit 3). Unskip once the SDK suppresses its manual load span under native tracing.
test.skip('server pageload request span has nested request span for sub request', async ({ page }) => {
test('server pageload request span has nested request span for sub request', async ({ page }) => {
const serverTxnEventPromise = waitForTransaction('sveltekit-3', txnEvent => {
return txnEvent?.transaction === 'GET /server-load-fetch';
});
Expand Down Expand Up @@ -85,14 +81,14 @@ test.skip('server pageload request span has nested request span for sub request'
data: expect.objectContaining({
'http.method': 'GET',
'http.route': '/api/users',
'http.url': 'http://localhost:3030/api/users',
'http.url': 'https://localhost:3030/api/users',
'sentry.op': 'http.server',
'sentry.origin': 'auto.http.sveltekit',
'sentry.source': 'route',
'sveltekit.is_data_request': false,
'sveltekit.is_sub_request': true,
'sveltekit.tracing.original_name': 'sveltekit.handle.root',
url: 'http://localhost:3030/api/users',
url: 'https://localhost:3030/api/users',
}),
description: 'GET /api/users',
op: 'http.server',
Expand Down
43 changes: 41 additions & 2 deletions packages/sveltekit/src/vite/autoInstrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as fs from 'fs';
import * as path from 'path';
import type { Plugin } from 'vite';
import { WRAPPED_MODULE_SUFFIX } from '../common/utils';
import type { BackwardsForwardsCompatibleSvelteConfig } from './svelteConfig';

const AcornParser = acorn.Parser.extend(tsPlugin());

Expand Down Expand Up @@ -45,6 +46,11 @@ export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptio

let isServerBuild: boolean | undefined = undefined;

// Whether we should skip server-side load instrumentation because SvelteKit's native server
// tracing is enabled. Initialized from the option (derived from `svelte.config.js`), but may be
// flipped to `true` in `configResolved` once we can read SvelteKit's resolved config (see below).
let onlyInstrumentClient = options.onlyInstrumentClient;

return {
name: 'sentry-auto-instrumentation',
// This plugin needs to run as early as possible, before the SvelteKit plugin virtualizes all paths and ids
Expand All @@ -56,10 +62,20 @@ export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptio
// `config.build.ssr` is `true` for that first build and `false` in the other ones.
// Hence we can use it as a switch to upload source maps only once in main build.
isServerBuild = !!config.build.ssr;

// As of SvelteKit 3, the native server-tracing config is no longer read from
// `svelte.config.js` (so the `onlyInstrumentClient` option, derived from it, is `false`).
// It's passed to the `sveltekit()` Vite plugin instead, which exposes the resolved config
// via its plugin `api.options`. Reading it here lets us reliably detect native tracing
// regardless of SvelteKit version. When it's enabled, we must not add our own server-side
// load instrumentation, otherwise we'd emit duplicate spans on top of SvelteKit's.
if (!onlyInstrumentClient && isNativeServerTracingEnabled(config.plugins)) {
onlyInstrumentClient = true;
}
},

async load(id) {
if (options.onlyInstrumentClient && isServerBuild) {
if (onlyInstrumentClient && isServerBuild) {
return null;
}

Expand All @@ -74,7 +90,7 @@ export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptio
return getWrapperCode('wrapLoadWithSentry', `${id}${WRAPPED_MODULE_SUFFIX}`);
}

if (options.onlyInstrumentClient) {
if (onlyInstrumentClient) {
// Now that we've checked universal files, we can early return and avoid further
// regexp checks below for server-only files, in case `onlyInstrumentClient` is `true`.
return null;
Expand All @@ -96,6 +112,29 @@ export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptio
};
}

/**
* Detects whether SvelteKit's native server-side tracing is enabled by reading the resolved
* SvelteKit config that the `sveltekit()` Vite plugin exposes via its plugin `api.options`.
*
* This is the source of truth as of SvelteKit 3, where the config moved out of `svelte.config.js`
* and into the `sveltekit()` plugin. On older SvelteKit versions that don't expose the config this
* way, it simply returns `false` and we fall back to the `svelte.config.js`-derived value.
*/
function isNativeServerTracingEnabled(plugins: readonly Plugin[] | undefined): boolean {
if (!plugins) {
return false;
}

for (const plugin of plugins) {
const options = (plugin?.api as { options?: BackwardsForwardsCompatibleSvelteConfig } | undefined)?.options;
if (options?.kit?.experimental?.tracing?.server) {
return true;
}
}

return false;
}

/**
* We only want to apply our wrapper to files that
*
Expand Down
61 changes: 61 additions & 0 deletions packages/sveltekit/test/vite/autoInstrument.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,67 @@ describe('makeAutoInstrumentationPlugin()', () => {
},
);
});

describe('when SvelteKit native server tracing is detected via the Vite plugin `api`', () => {
// SvelteKit 3 no longer reads native-tracing config from `svelte.config.js` (so the
// `onlyInstrumentClient` option computed from it is `false`); the config is exposed on the
// SvelteKit Vite plugin's `api.options` instead.
function configWithKitTracing(ssr: boolean, serverTracing: boolean): unknown {
return {
build: { ssr },
plugins: [
{ name: 'some-other-plugin' },
{
name: 'vite-plugin-sveltekit-setup',
api: { options: { kit: { experimental: { tracing: { server: serverTracing } } } } },
},
],
};
}

it.each(['path/to/+page.server.ts', 'path/to/+layout.server.js', 'path/to/+page.ts', 'path/to/+layout.mjs'])(
"doesn't wrap %s in the SSR build when native tracing is enabled, even if `onlyInstrumentClient` is `false`",
async (path: string) => {
const plugin = makeAutoInstrumentationPlugin({
debug: false,
load: true,
serverLoad: true,
onlyInstrumentClient: false,
});

// @ts-expect-error this exists and is callable
plugin.configResolved(configWithKitTracing(true, true));

// @ts-expect-error this exists and is callable
const loadResult = await plugin.load(path);

expect(loadResult).toEqual(null);
},
);

it('still wraps server load in the SSR build when native tracing is not enabled', async () => {
const plugin = makeAutoInstrumentationPlugin({
debug: false,
load: true,
serverLoad: true,
onlyInstrumentClient: false,
});

// @ts-expect-error this exists and is callable
plugin.configResolved(configWithKitTracing(true, false));

const path = 'path/to/+page.server.ts';
// @ts-expect-error this exists and is callable
const loadResult = await plugin.load(path);

expect(loadResult).toBe(
'import { wrapServerLoadWithSentry } from "@sentry/sveltekit";' +
`import * as userModule from "${path}?sentry-auto-wrap";` +
'export const load = userModule.load ? wrapServerLoadWithSentry(userModule.load) : undefined;' +
`export * from "${path}?sentry-auto-wrap";`,
);
});
});
});

describe('canWrapLoad', () => {
Expand Down
Loading