Skip to content

Commit e8dcd3e

Browse files
committed
fix(v10/sveltekit): Read SvelteKit config from the Vite plugin
Backport of: #23678
1 parent f44df80 commit e8dcd3e

15 files changed

Lines changed: 774 additions & 241 deletions

‎packages/sveltekit/src/vite/autoInstrument.ts‎

Lines changed: 11 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import * as fs from 'fs';
44
import * as path from 'path';
55
import type { Plugin } from 'vite';
66
import { WRAPPED_MODULE_SUFFIX } from '../common/utils';
7-
import type { BackwardsForwardsCompatibleKitConfig, BackwardsForwardsCompatibleSvelteConfig } from './svelteConfig';
7+
import type { ResolvedKitConfig } from './kitConfig';
8+
import { isNativeServerTracingEnabled } from './kitConfig';
89

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

@@ -29,7 +30,7 @@ export type AutoInstrumentSelection = {
2930

3031
type AutoInstrumentPluginOptions = AutoInstrumentSelection & {
3132
debug: boolean;
32-
onlyInstrumentClient: boolean;
33+
getKitConfig: () => Promise<ResolvedKitConfig>;
3334
};
3435

3536
/**
@@ -42,14 +43,15 @@ type AutoInstrumentPluginOptions = AutoInstrumentSelection & {
4243
* @returns the plugin
4344
*/
4445
export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptions): Plugin {
45-
const { load: wrapLoadEnabled, serverLoad: wrapServerLoadEnabled, debug } = options;
46+
const { load: wrapLoadEnabled, serverLoad: wrapServerLoadEnabled, debug, getKitConfig } = options;
4647

4748
let isServerBuild: boolean | undefined = undefined;
4849

4950
// Whether we should skip server-side load instrumentation because SvelteKit's native server
50-
// tracing is enabled. Initialized from the option (derived from `svelte.config.js`), but may be
51-
// flipped to `true` in `configResolved` once we can read SvelteKit's resolved config (see below).
52-
let onlyInstrumentClient = options.onlyInstrumentClient;
51+
// tracing is enabled. If it is, adding our own wrapper on top would emit duplicate spans.
52+
let onlyInstrumentClientPromise: Promise<boolean> | undefined;
53+
const shouldOnlyInstrumentClient = (): Promise<boolean> =>
54+
(onlyInstrumentClientPromise ??= getKitConfig().then(isNativeServerTracingEnabled));
5355

5456
return {
5557
name: 'sentry-auto-instrumentation',
@@ -62,16 +64,6 @@ export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptio
6264
// `config.build.ssr` is `true` for that first build and `false` in the other ones.
6365
// Hence we can use it as a switch to upload source maps only once in main build.
6466
isServerBuild = !!config.build.ssr;
65-
66-
// As of SvelteKit 3, the native server-tracing config is no longer read from
67-
// `svelte.config.js` (so the `onlyInstrumentClient` option, derived from it, is `false`).
68-
// It's passed to the `sveltekit()` Vite plugin instead, which exposes the resolved config
69-
// via its plugin `api.options`. Reading it here lets us reliably detect native tracing
70-
// regardless of SvelteKit version. When it's enabled, we must not add our own server-side
71-
// load instrumentation, otherwise we'd emit duplicate spans on top of SvelteKit's.
72-
if (!onlyInstrumentClient && isNativeServerTracingEnabled(config.plugins)) {
73-
onlyInstrumentClient = true;
74-
}
7567
},
7668

7769
async load(id) {
@@ -81,7 +73,7 @@ export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptio
8173
const environmentName = (this as { environment?: { name?: string } }).environment?.name;
8274
const isServerEnvironment = environmentName != null ? environmentName === 'ssr' : !!isServerBuild;
8375

84-
if (onlyInstrumentClient && isServerEnvironment) {
76+
if (isServerEnvironment && (await shouldOnlyInstrumentClient())) {
8577
return null;
8678
}
8779

@@ -96,9 +88,9 @@ export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptio
9688
return getWrapperCode('wrapLoadWithSentry', `${id}${WRAPPED_MODULE_SUFFIX}`);
9789
}
9890

99-
if (onlyInstrumentClient) {
91+
if (await shouldOnlyInstrumentClient()) {
10092
// Now that we've checked universal files, we can early return and avoid further
101-
// regexp checks below for server-only files, in case `onlyInstrumentClient` is `true`.
93+
// regexp checks below for server-only files.
10294
return null;
10395
}
10496

@@ -118,39 +110,6 @@ export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptio
118110
};
119111
}
120112

121-
/**
122-
* Detects whether SvelteKit's native server-side tracing is enabled by reading the resolved
123-
* SvelteKit config that the `sveltekit()` Vite plugin exposes via its plugin `api.options`.
124-
*
125-
* This is the source of truth as of SvelteKit 3, where the config moved out of `svelte.config.js`
126-
* and into the `sveltekit()` plugin. On older SvelteKit versions that don't expose the config this
127-
* way, it simply returns `false` and we fall back to the `svelte.config.js`-derived value.
128-
*/
129-
function isNativeServerTracingEnabled(plugins: readonly Plugin[] | undefined): boolean {
130-
if (!plugins) {
131-
return false;
132-
}
133-
134-
for (const plugin of plugins) {
135-
const options = (
136-
plugin?.api as
137-
| { options?: BackwardsForwardsCompatibleSvelteConfig & BackwardsForwardsCompatibleKitConfig }
138-
| undefined
139-
)?.options;
140-
141-
// SvelteKit 3 flattened the plugin config: what used to live under `kit` now sits
142-
// at the top level of the exposed options.
143-
const kitConfig = options?.kit ?? options;
144-
145-
// SvelteKit 3 promoted `tracing` out of `experimental`; older versions nest it there.
146-
if (kitConfig?.tracing?.server || kitConfig?.experimental?.tracing?.server) {
147-
return true;
148-
}
149-
}
150-
151-
return false;
152-
}
153-
154113
/**
155114
* We only want to apply our wrapper to files that
156115
*

‎packages/sveltekit/src/vite/detectAdapter.ts‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Package } from '@sentry/core';
22
import * as fs from 'fs';
33
import * as path from 'path';
4-
import type { BackwardsForwardsCompatibleSvelteConfig } from './svelteConfig';
4+
import type { ResolvedKitConfig } from './kitConfig';
55

66
/**
77
* Supported @sveltejs/adapters-[adapter] SvelteKit adapters
@@ -21,33 +21,33 @@ const ADAPTER_NAME_MAP: Record<string, SupportedSvelteKitAdapters> = {
2121

2222
/**
2323
* Tries to detect the used adapter for SvelteKit.
24-
* 1. If svelteConfig is provided and has kit.adapter.name, uses that (source of truth from svelte.config.js).
24+
* 1. If kitConfig is provided and has adapter.name, uses that (source of truth from SvelteKit itself).
2525
* 2. Otherwise falls back to inferring from package.json dependencies.
2626
* Returns the name of the adapter or 'other' if no supported adapter was found.
2727
*
28-
* @param svelteConfig - Loaded svelte config (e.g. from loadSvelteConfig()). Pass `undefined` to skip config-based detection.
28+
* @param kitConfig - Resolved SvelteKit config (e.g. from the kit config resolver). Pass `undefined` to skip config-based detection.
2929
* @param debug - Whether to log detection result. Pass `undefined` for false.
3030
*/
3131
export async function detectAdapter(
32-
svelteConfig: BackwardsForwardsCompatibleSvelteConfig | undefined,
32+
kitConfig: ResolvedKitConfig | undefined,
3333
debug: boolean | undefined,
3434
): Promise<SupportedSvelteKitAdapters> {
35-
const adapterName = svelteConfig?.kit?.adapter?.name;
35+
const adapterName = kitConfig?.adapter?.name;
3636
if (adapterName && typeof adapterName === 'string') {
3737
const mapped = ADAPTER_NAME_MAP[adapterName];
3838
if (mapped) {
3939
if (debug) {
4040
// eslint-disable-next-line no-console
41-
console.log(`[Sentry SvelteKit Plugin] Detected SvelteKit ${mapped} adapter from \`svelte.config.js\``);
41+
console.log(`[Sentry SvelteKit Plugin] Detected SvelteKit ${mapped} adapter from your SvelteKit config`);
4242
}
4343
return mapped;
4444
}
4545
// We found an adapter name but it's not in our supported list -> return 'other'
46-
// svelte.config.js is the source of truth, so we don't need to fall back to package.json.
46+
// The SvelteKit config is the source of truth, so we don't need to fall back to package.json.
4747
if (debug) {
4848
// eslint-disable-next-line no-console
4949
console.warn(
50-
`[Sentry SvelteKit Plugin] Detected unsupported adapter name ${adapterName} in \`svelte.config.js\`. Please set the 'adapter' option manually`,
50+
`[Sentry SvelteKit Plugin] Detected unsupported adapter name ${adapterName} in your SvelteKit config. Please set the 'adapter' option manually`,
5151
);
5252
}
5353
return 'other';

‎packages/sveltekit/src/vite/injectGlobalValues.ts‎

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { escapeStringForRegex, type InternalGlobal } from '@sentry/core';
22
import MagicString from 'magic-string';
33
import type { Plugin } from 'vite';
4-
import { type BackwardsForwardsCompatibleSvelteConfig, getAdapterOutputDir, getHooksFileName } from './svelteConfig';
5-
import type { SentrySvelteKitPluginOptions } from './types';
4+
import type { ResolvedKitConfig } from './kitConfig';
5+
import { getHooksFileName } from './svelteConfig';
66

77
export type GlobalSentryValues = {
88
__sentry_sveltekit_output_dir?: string;
@@ -32,33 +32,55 @@ export function getGlobalValueInjectionCode(globalSentryValues: GlobalSentryValu
3232
return `${injectedValuesCode}\n`;
3333
}
3434

35+
type GlobalValuesInjectionOptions = {
36+
getKitConfig: () => Promise<ResolvedKitConfig>;
37+
getAdapterOutputDir: () => Promise<string>;
38+
debug?: boolean;
39+
};
40+
3541
/**
36-
* Injects SvelteKit app configuration values the svelte.config.js into the
37-
* server's global object so that the SDK can pick up the information at runtime
42+
* Injects SvelteKit app configuration values into the server's global object
43+
* so that the SDK can pick up the information at runtime.
3844
*/
39-
export async function makeGlobalValuesInjectionPlugin(
40-
svelteConfig: BackwardsForwardsCompatibleSvelteConfig,
41-
options: Pick<SentrySvelteKitPluginOptions, 'adapter' | 'debug'>,
42-
): Promise<Plugin> {
43-
const { adapter = 'other', debug = false } = options;
45+
export function makeGlobalValuesInjectionPlugin(options: GlobalValuesInjectionOptions): Plugin {
46+
const { getKitConfig, getAdapterOutputDir, debug = false } = options;
4447

45-
const serverHooksFile = getHooksFileName(svelteConfig, 'server');
46-
const adapterOutputDir = await getAdapterOutputDir(svelteConfig, adapter);
48+
// The SvelteKit config is only available once Vite has resolved its plugins, so we compute
49+
// the injected values lazily (but only once) instead of at plugin creation time.
50+
let injectionValuesPromise: Promise<{ globalSentryValues: GlobalSentryValues; hooksFileRegexp: RegExp }> | undefined;
4751

48-
const globalSentryValues: GlobalSentryValues = {
49-
__sentry_sveltekit_output_dir: adapterOutputDir,
50-
};
52+
const getInjectionValues = (): Promise<{ globalSentryValues: GlobalSentryValues; hooksFileRegexp: RegExp }> =>
53+
(injectionValuesPromise ??= (async () => {
54+
const kitConfig = await getKitConfig();
5155

52-
if (debug) {
53-
// eslint-disable-next-line no-console
54-
console.log('[Sentry SvelteKit] Global values:', globalSentryValues);
55-
}
56+
const serverHooksFile = getHooksFileName(kitConfig, 'server');
57+
const adapterOutputDir = await getAdapterOutputDir();
58+
59+
const globalSentryValues: GlobalSentryValues = {
60+
__sentry_sveltekit_output_dir: adapterOutputDir,
61+
};
5662

57-
// oxlint-disable-next-line sdk/no-regexp-constructor -- not end user input + escaped anyway
58-
const hooksFileRegexp = new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`);
63+
if (debug) {
64+
// eslint-disable-next-line no-console
65+
console.log('[Sentry SvelteKit] Global values:', globalSentryValues);
66+
}
67+
68+
return {
69+
globalSentryValues,
70+
// oxlint-disable-next-line sdk/no-regexp-constructor -- not end user input + escaped anyway
71+
hooksFileRegexp: new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`),
72+
};
73+
})());
5974

6075
return {
6176
name: 'sentry-sveltekit-global-values-injection-plugin',
77+
78+
// Eagerly, not on the first `transform`: see the note on the adapter output dir in
79+
// `sentrySvelteKit()`. Awaited so a failure surfaces as a config error, not a stray rejection.
80+
configResolved: async () => {
81+
await getInjectionValues();
82+
},
83+
6284
resolveId: (id, _importer, _ref) => {
6385
if (id === VIRTUAL_GLOBAL_VALUES_FILE) {
6486
return {
@@ -70,8 +92,9 @@ export async function makeGlobalValuesInjectionPlugin(
7092
return null;
7193
},
7294

73-
load: id => {
95+
load: async id => {
7496
if (id === VIRTUAL_GLOBAL_VALUES_FILE) {
97+
const { globalSentryValues } = await getInjectionValues();
7598
return {
7699
code: getGlobalValueInjectionCode(globalSentryValues),
77100
};
@@ -80,6 +103,8 @@ export async function makeGlobalValuesInjectionPlugin(
80103
},
81104

82105
transform: async (code, id) => {
106+
const { hooksFileRegexp } = await getInjectionValues();
107+
83108
const isServerEntryFile = /instrumentation\.server\./.test(id) || hooksFileRegexp.test(id);
84109

85110
if (isServerEntryFile) {

0 commit comments

Comments
 (0)