Skip to content

Commit f3f85a5

Browse files
Lms24claude
authored andcommitted
fix(v10/sveltekit): Resolve @opentelemetry/api via the SDK on SvelteKit 3
Backport of #24736 Since `@sveltejs/kit@3.0.0-next.28` (sveltejs/kit#17081), the SvelteKit server build keeps `@opentelemetry/api` external. With `tracing.server` enabled, SvelteKit imports it at runtime from the app. Under pnpm or Yarn PnP, the app cannot resolve it, because it is only a transitive dependency of `@sentry/node`. Then SvelteKit fails every request with `tracing_api_missing`. This is also why the `sveltekit-3` e2e app fails on v10 against stable SvelteKit 3. `@sentry/sveltekit` now depends on `@opentelemetry/api`, re-exports it as `@sentry/sveltekit/opentelemetry-api`, and the `sentrySvelteKit()` Vite plugin redirects SvelteKit's import to that re-export. ## Differences to the original PR - `packages/sveltekit/package.json`: v10 has no `./vite` subpath export, so only the `./opentelemetry-api` export is added. The dependency list keeps the v10 packages and only adds `@opentelemetry/api`. - `packages/sveltekit/rollup.npm.config.mjs`: only adds the `src/opentelemetryApi.ts` entry point, not `src/vite/index.ts`. - `packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts`: v10 has no orchestrion Vite plugin, so the expected plugin counts are one lower than on `develop`. (cherry picked from commit 2a58c97) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2cf2dd0 commit f3f85a5

5 files changed

Lines changed: 116 additions & 9 deletions

File tree

‎packages/sveltekit/package.json‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
"import": "./build/esm/index.server.js",
4242
"require": "./build/cjs/index.server.js"
4343
}
44+
},
45+
"./opentelemetry-api": {
46+
"types": "./build/types/opentelemetryApi.d.ts",
47+
"import": "./build/esm/opentelemetryApi.js",
48+
"require": "./build/cjs/opentelemetryApi.js"
4449
}
4550
},
4651
"publishConfig": {
@@ -57,6 +62,7 @@
5762
}
5863
},
5964
"dependencies": {
65+
"@opentelemetry/api": "^1.9.1",
6066
"@sentry/cloudflare": "10.76.0",
6167
"@sentry/core": "10.76.0",
6268
"@sentry/conventions": "^0.16.0",

‎packages/sveltekit/rollup.npm.config.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export default makeNPMConfigVariants(
1313
'src/client/svelte5BrowserTracing.ts',
1414
'src/server/index.ts',
1515
'src/worker/index.ts',
16+
'src/opentelemetryApi.ts',
1617
],
1718
packageSpecificConfig: {
1819
// Keep the variant subpath external so the transpiled output preserves the import for the
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Re-exported under `@sentry/sveltekit/opentelemetry-api` so the `sentrySvelteKit()` plugin can point
2+
// SvelteKit's `@opentelemetry/api` import here. See `makeOpenTelemetryApiResolverPlugin`.
3+
export * from '@opentelemetry/api';

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}
4949

5050
// First so the config settles as early as possible. The plugins below read it in `configResolved`,
5151
// which Vite runs concurrently, so their order relative to the resolver doesn't matter.
52-
const sentryPlugins: Plugin[] = [kitConfigResolver.plugin, makeBrowserTracingVariantResolverPlugin()];
52+
const sentryPlugins: Plugin[] = [
53+
kitConfigResolver.plugin,
54+
makeBrowserTracingVariantResolverPlugin(),
55+
makeOpenTelemetryApiResolverPlugin(),
56+
];
5357

5458
if (mergedOptions.autoInstrument) {
5559
const pluginOptions: AutoInstrumentSelection = {
@@ -135,6 +139,44 @@ function makeBrowserTracingVariantResolverPlugin(): Plugin {
135139
};
136140
}
137141

142+
const OTEL_API_ID = '@opentelemetry/api';
143+
const OTEL_API_REEXPORT_ID = '@sentry/sveltekit/opentelemetry-api';
144+
const OTEL_API_REEXPORT_FILE_REGEX = /[\\/]opentelemetryApi\.js$/;
145+
146+
/**
147+
* SvelteKit 3 externalizes `@opentelemetry/api` in server builds, so its runtime imports it at runtime
148+
* from the app root. That only resolves if the app depends on it directly: under pnpm (or Yarn PnP),
149+
* the copy installed through `@sentry/sveltekit` isn't reachable, and Kit fails every request once
150+
* `tracing.server` is enabled.
151+
*
152+
* We redirect the import to our re-export instead, which resolves because the app always depends on
153+
* `@sentry/sveltekit` directly. It stays external, so the Kit runtime and `instrumentation.server.js`
154+
* still share a single module instance, which is why Kit externalizes it in the first place.
155+
*/
156+
function makeOpenTelemetryApiResolverPlugin(): Plugin {
157+
let isKit3Promise: Promise<boolean> | undefined;
158+
159+
return {
160+
name: 'sentry-sveltekit-opentelemetry-api',
161+
enforce: 'pre',
162+
// In dev, Kit's import resolves from Kit's own install location, where pnpm links the peer dep.
163+
apply: 'build',
164+
async resolveId(id, importer, options) {
165+
// Skip our own re-export, in case it gets bundled, so it doesn't re-export itself.
166+
if (id !== OTEL_API_ID || !options?.ssr || (importer && OTEL_API_REEXPORT_FILE_REGEX.test(importer))) {
167+
return null;
168+
}
169+
170+
isKit3Promise ??= isSvelteKit3(id => this.resolve(id, undefined, { skipSelf: true }));
171+
if (!(await isKit3Promise)) {
172+
return null;
173+
}
174+
175+
return { id: OTEL_API_REEXPORT_ID, external: true };
176+
},
177+
};
178+
}
179+
138180
/**
139181
* Whether to use the SvelteKit 3 (`$app/state`) variant, from the installed `@sveltejs/kit` version
140182
* (resolved via the bundler, not `process.cwd()`).

‎packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts‎

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import * as fs from 'fs';
2+
import * as os from 'os';
3+
import * as path from 'path';
14
import type { Plugin } from 'vite';
25
import { describe, expect, it, vi } from 'vitest';
36
import * as autoInstrument from '../../src/vite/autoInstrument';
@@ -42,9 +45,10 @@ describe('sentrySvelteKit()', () => {
4245
const plugins = await getSentrySvelteKitPlugins();
4346

4447
expect(plugins).toBeInstanceOf(Array);
45-
// 1 kit config resolver + 1 browser-tracing variant resolver + 1 auto instrument plugin
46-
// + 1 global values injection plugin + 1 modified main plugin + 3 custom plugins
47-
expect(plugins).toHaveLength(8);
48+
// 1 kit config resolver + 1 browser-tracing variant resolver + 1 OpenTelemetry API resolver
49+
// + 1 auto instrument plugin + 1 global values injection plugin + 1 modified main plugin
50+
// + 3 custom plugins
51+
expect(plugins).toHaveLength(9);
4852
});
4953

5054
it('returns the custom sentry source maps upload plugin, unmodified sourcemaps plugins and the auto-instrument plugin by default', async () => {
@@ -55,6 +59,8 @@ describe('sentrySvelteKit()', () => {
5559
'sentry-sveltekit-kit-config-resolver',
5660
// browser-tracing variant resolver:
5761
'sentry-sveltekit-browser-tracing-variant',
62+
// OpenTelemetry API resolver:
63+
'sentry-sveltekit-opentelemetry-api',
5864
// auto instrument plugin:
5965
'sentry-auto-instrumentation',
6066
// global values injection plugin:
@@ -70,17 +76,17 @@ describe('sentrySvelteKit()', () => {
7076

7177
it("doesn't return the sentry source maps plugins if autoUploadSourcemaps is `false`", async () => {
7278
const plugins = await getSentrySvelteKitPlugins({ autoUploadSourceMaps: false });
73-
expect(plugins).toHaveLength(3); // kit config resolver + browser-tracing variant resolver + auto instrument
79+
expect(plugins).toHaveLength(4); // kit config resolver + browser-tracing variant resolver + OpenTelemetry API resolver + auto instrument
7480
});
7581

7682
it("doesn't return the sentry source maps plugins if `NODE_ENV` is development", async () => {
7783
const previousEnv = process.env.NODE_ENV;
7884

7985
process.env.NODE_ENV = 'development';
8086
const plugins = await getSentrySvelteKitPlugins({ autoUploadSourceMaps: true, autoInstrument: true });
81-
const instrumentPlugin = plugins[2];
87+
const instrumentPlugin = plugins[3];
8288

83-
expect(plugins).toHaveLength(4); // kit config resolver + browser-tracing variant resolver + auto instrument + global values injection
89+
expect(plugins).toHaveLength(5); // kit config resolver + browser-tracing variant resolver + OpenTelemetry API resolver + auto instrument + global values injection
8490
expect(instrumentPlugin?.name).toEqual('sentry-auto-instrumentation');
8591

8692
process.env.NODE_ENV = previousEnv;
@@ -89,7 +95,7 @@ describe('sentrySvelteKit()', () => {
8995
it("doesn't return the auto instrument plugin if autoInstrument is `false`", async () => {
9096
const plugins = await getSentrySvelteKitPlugins({ autoInstrument: false });
9197
const pluginNames = plugins.map(plugin => plugin.name);
92-
expect(plugins).toHaveLength(7); // kit config resolver + browser-tracing variant resolver + global values injection + 1 modified main plugin + 3 custom plugins
98+
expect(plugins).toHaveLength(8); // kit config resolver + browser-tracing variant resolver + OpenTelemetry API resolver + global values injection + 1 modified main plugin + 3 custom plugins
9399
expect(pluginNames).not.toContain('sentry-auto-instrumentation');
94100
});
95101

@@ -191,7 +197,7 @@ describe('sentrySvelteKit()', () => {
191197
// just to ignore the source maps plugin:
192198
autoUploadSourceMaps: false,
193199
});
194-
const plugin = plugins[2]!;
200+
const plugin = plugins[3]!;
195201

196202
expect(plugin.name).toEqual('sentry-auto-instrumentation');
197203
expect(makePluginSpy).toHaveBeenCalledWith({
@@ -203,6 +209,55 @@ describe('sentrySvelteKit()', () => {
203209
});
204210
});
205211

212+
describe('OpenTelemetry API resolver plugin', () => {
213+
async function getResolver(kitVersion: string) {
214+
const plugins = await getSentrySvelteKitPlugins({ autoUploadSourceMaps: false });
215+
const plugin = plugins.find(p => p.name === 'sentry-sveltekit-opentelemetry-api')!;
216+
217+
const kitPackageJson = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-kit-')), 'package.json');
218+
fs.writeFileSync(kitPackageJson, JSON.stringify({ version: kitVersion }));
219+
const context = { resolve: vi.fn().mockResolvedValue({ id: kitPackageJson }) };
220+
221+
const resolveId = (id: string, importer: string | undefined, ssr = true) =>
222+
// @ts-expect-error - minimal plugin context
223+
plugin.resolveId.call(context, id, importer, { ssr });
224+
225+
return { plugin, resolveId };
226+
}
227+
228+
it('only applies to builds', async () => {
229+
const { plugin } = await getResolver('3.0.0');
230+
expect(plugin.apply).toBe('build');
231+
});
232+
233+
it('redirects `@opentelemetry/api` to the external re-export on SvelteKit 3', async () => {
234+
const { resolveId } = await getResolver('3.0.0-next.28');
235+
236+
await expect(resolveId('@opentelemetry/api', '/app/node_modules/@sveltejs/kit/src/instance.js')).resolves.toEqual({
237+
id: '@sentry/sveltekit/opentelemetry-api',
238+
external: true,
239+
});
240+
});
241+
242+
it('leaves `@opentelemetry/api` alone on SvelteKit 2', async () => {
243+
const { resolveId } = await getResolver('2.70.2');
244+
245+
await expect(resolveId('@opentelemetry/api', '/app/src/instrumentation.server.js')).resolves.toBeNull();
246+
});
247+
248+
it('leaves client builds, other ids and the re-export itself alone', async () => {
249+
const { resolveId } = await getResolver('3.0.0');
250+
251+
await expect(resolveId('@opentelemetry/api', '/app/src/hooks.client.js', false)).resolves.toBeNull();
252+
await expect(
253+
resolveId('@opentelemetry/api/experimental', '/app/src/instrumentation.server.js'),
254+
).resolves.toBeNull();
255+
await expect(
256+
resolveId('@opentelemetry/api', '/app/node_modules/@sentry/sveltekit/build/esm/opentelemetryApi.js'),
257+
).resolves.toBeNull();
258+
});
259+
});
260+
206261
describe('generateVitePluginOptions', () => {
207262
it('returns null if no relevant options are provided', () => {
208263
const options: SentrySvelteKitPluginOptions = {};

0 commit comments

Comments
 (0)