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
63 changes: 11 additions & 52 deletions packages/sveltekit/src/vite/autoInstrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import * as fs from 'fs';
import * as path from 'path';
import type { Plugin } from 'vite';
import { WRAPPED_MODULE_SUFFIX } from '../common/utils';
import type { BackwardsForwardsCompatibleKitConfig, BackwardsForwardsCompatibleSvelteConfig } from './svelteConfig';
import type { ResolvedKitConfig } from './kitConfig';
import { isNativeServerTracingEnabled } from './kitConfig';

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

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

type AutoInstrumentPluginOptions = AutoInstrumentSelection & {
debug: boolean;
onlyInstrumentClient: boolean;
getKitConfig: () => Promise<ResolvedKitConfig>;
};

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

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;
// tracing is enabled. If it is, adding our own wrapper on top would emit duplicate spans.
let onlyInstrumentClientPromise: Promise<boolean> | undefined;
const shouldOnlyInstrumentClient = (): Promise<boolean> =>
(onlyInstrumentClientPromise ??= getKitConfig().then(isNativeServerTracingEnabled));

return {
name: 'sentry-auto-instrumentation',
Expand All @@ -62,16 +64,6 @@ 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) {
Expand All @@ -81,7 +73,7 @@ export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptio
const environmentName = (this as { environment?: { name?: string } }).environment?.name;
const isServerEnvironment = environmentName != null ? environmentName === 'ssr' : !!isServerBuild;

if (onlyInstrumentClient && isServerEnvironment) {
if (isServerEnvironment && (await shouldOnlyInstrumentClient())) {
return null;
}

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

if (onlyInstrumentClient) {
if (await shouldOnlyInstrumentClient()) {
// 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`.
// regexp checks below for server-only files.
return null;
}

Expand All @@ -118,39 +110,6 @@ 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 & BackwardsForwardsCompatibleKitConfig }
| undefined
)?.options;

// SvelteKit 3 flattened the plugin config: what used to live under `kit` now sits
// at the top level of the exposed options.
const kitConfig = options?.kit ?? options;

// SvelteKit 3 promoted `tracing` out of `experimental`; older versions nest it there.
if (kitConfig?.tracing?.server || kitConfig?.experimental?.tracing?.server) {
return true;
}
}

return false;
}

/**
* We only want to apply our wrapper to files that
*
Expand Down
16 changes: 8 additions & 8 deletions packages/sveltekit/src/vite/detectAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Package } from '@sentry/core';
import * as fs from 'fs';
import * as path from 'path';
import type { BackwardsForwardsCompatibleSvelteConfig } from './svelteConfig';
import type { ResolvedKitConfig } from './kitConfig';

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

/**
* Tries to detect the used adapter for SvelteKit.
* 1. If svelteConfig is provided and has kit.adapter.name, uses that (source of truth from svelte.config.js).
* 1. If kitConfig is provided and has adapter.name, uses that (source of truth from SvelteKit itself).
* 2. Otherwise falls back to inferring from package.json dependencies.
* Returns the name of the adapter or 'other' if no supported adapter was found.
*
* @param svelteConfig - Loaded svelte config (e.g. from loadSvelteConfig()). Pass `undefined` to skip config-based detection.
* @param kitConfig - Resolved SvelteKit config (e.g. from the kit config resolver). Pass `undefined` to skip config-based detection.
* @param debug - Whether to log detection result. Pass `undefined` for false.
*/
export async function detectAdapter(
svelteConfig: BackwardsForwardsCompatibleSvelteConfig | undefined,
kitConfig: ResolvedKitConfig | undefined,
debug: boolean | undefined,
): Promise<SupportedSvelteKitAdapters> {
const adapterName = svelteConfig?.kit?.adapter?.name;
const adapterName = kitConfig?.adapter?.name;
if (adapterName && typeof adapterName === 'string') {
const mapped = ADAPTER_NAME_MAP[adapterName];
if (mapped) {
if (debug) {
// eslint-disable-next-line no-console
console.log(`[Sentry SvelteKit Plugin] Detected SvelteKit ${mapped} adapter from \`svelte.config.js\``);
console.log(`[Sentry SvelteKit Plugin] Detected SvelteKit ${mapped} adapter from your SvelteKit config`);
}
return mapped;
}
// We found an adapter name but it's not in our supported list -> return 'other'
// svelte.config.js is the source of truth, so we don't need to fall back to package.json.
// The SvelteKit config is the source of truth, so we don't need to fall back to package.json.
if (debug) {
// eslint-disable-next-line no-console
console.warn(
`[Sentry SvelteKit Plugin] Detected unsupported adapter name ${adapterName} in \`svelte.config.js\`. Please set the 'adapter' option manually`,
`[Sentry SvelteKit Plugin] Detected unsupported adapter name ${adapterName} in your SvelteKit config. Please set the 'adapter' option manually`,
);
}
return 'other';
Expand Down
68 changes: 47 additions & 21 deletions packages/sveltekit/src/vite/injectGlobalValues.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { escapeStringForRegex, type InternalGlobal } from '@sentry/core';
import MagicString from 'magic-string';
import type { Plugin } from 'vite';
import { type BackwardsForwardsCompatibleSvelteConfig, getAdapterOutputDir, getHooksFileName } from './svelteConfig';
import type { SentrySvelteKitPluginOptions } from './types';
import type { ResolvedKitConfig } from './kitConfig';
import { getHooksFileName } from './svelteConfig';

export type GlobalSentryValues = {
__sentry_sveltekit_output_dir?: string;
Expand Down Expand Up @@ -32,33 +32,56 @@ export function getGlobalValueInjectionCode(globalSentryValues: GlobalSentryValu
return `${injectedValuesCode}\n`;
}

type GlobalValuesInjectionOptions = {
getKitConfig: () => Promise<ResolvedKitConfig>;
getAdapterOutputDir: () => Promise<string>;
debug?: boolean;
};

/**
* Injects SvelteKit app configuration values the svelte.config.js into the
* server's global object so that the SDK can pick up the information at runtime
* Injects SvelteKit app configuration values into the server's global object
* so that the SDK can pick up the information at runtime.
*/
export async function makeGlobalValuesInjectionPlugin(
svelteConfig: BackwardsForwardsCompatibleSvelteConfig,
options: Pick<SentrySvelteKitPluginOptions, 'adapter' | 'debug'>,
): Promise<Plugin> {
const { adapter = 'other', debug = false } = options;
export function makeGlobalValuesInjectionPlugin(options: GlobalValuesInjectionOptions): Plugin {
const { getKitConfig, getAdapterOutputDir, debug = false } = options;

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

const globalSentryValues: GlobalSentryValues = {
__sentry_sveltekit_output_dir: adapterOutputDir,
};
const getInjectionValues = (): Promise<{ globalSentryValues: GlobalSentryValues; hooksFileRegexp: RegExp }> =>
(injectionValuesPromise ??= (async () => {
const kitConfig = await getKitConfig();

if (debug) {
// eslint-disable-next-line no-console
console.log('[Sentry SvelteKit] Global values:', globalSentryValues);
}
const serverHooksFile = getHooksFileName(kitConfig, 'server');
const adapterOutputDir = await getAdapterOutputDir();

const globalSentryValues: GlobalSentryValues = {
__sentry_sveltekit_output_dir: adapterOutputDir,
};

// oxlint-disable-next-line sdk/no-regexp-constructor -- not end user input + escaped anyway
const hooksFileRegexp = new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`);
if (debug) {
// eslint-disable-next-line no-console
console.log('[Sentry SvelteKit] Global values:', globalSentryValues);
}

return {
globalSentryValues,
// oxlint-disable-next-line sdk/no-regexp-constructor -- not end user input + escaped anyway
hooksFileRegexp: new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`),
};
})());

return {
name: 'sentry-sveltekit-global-values-injection-plugin',

// Resolve eagerly rather than on the first `transform`: see the note on the adapter output dir
// in `sentrySvelteKit()`. Awaited so a failure surfaces as a config error instead of an
// unhandled rejection.
configResolved: async () => {
await getInjectionValues();
},

resolveId: (id, _importer, _ref) => {
if (id === VIRTUAL_GLOBAL_VALUES_FILE) {
return {
Expand All @@ -70,8 +93,9 @@ export async function makeGlobalValuesInjectionPlugin(
return null;
},

load: id => {
load: async id => {
if (id === VIRTUAL_GLOBAL_VALUES_FILE) {
const { globalSentryValues } = await getInjectionValues();
return {
code: getGlobalValueInjectionCode(globalSentryValues),
};
Expand All @@ -80,6 +104,8 @@ export async function makeGlobalValuesInjectionPlugin(
},

transform: async (code, id) => {
const { hooksFileRegexp } = await getInjectionValues();

const isServerEntryFile = /instrumentation\.server\./.test(id) || hooksFileRegexp.test(id);

if (isServerEntryFile) {
Expand Down
Loading
Loading