Skip to content
Merged
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
6 changes: 6 additions & 0 deletions packages/sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
"require": "./build/cjs/index.server.js"
}
},
"./opentelemetry-api": {
"types": "./build/types/opentelemetryApi.d.ts",
"import": "./build/esm/opentelemetryApi.js",
"require": "./build/cjs/opentelemetryApi.js"
},
"./vite": {
"types": "./build/types/vite/index.d.ts",
"import": "./build/esm/vite/index.js",
Expand All @@ -65,6 +70,7 @@
}
},
"dependencies": {
"@opentelemetry/api": "^1.9.1",
"@sentry/cloudflare": "11.0.0",
"@sentry/core": "11.0.0",
"@sentry/conventions": "^0.24.0",
Expand Down
1 change: 1 addition & 0 deletions packages/sveltekit/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default makeNPMConfigVariants(
'src/server/index.ts',
'src/worker/index.ts',
'src/vite/index.ts',
'src/opentelemetryApi.ts',
],
packageSpecificConfig: {
// Keep the variant subpath external so the transpiled output preserves the import for the
Expand Down
3 changes: 3 additions & 0 deletions packages/sveltekit/src/opentelemetryApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Re-exported under `@sentry/sveltekit/opentelemetry-api` so the `sentrySvelteKit()` plugin can point
// SvelteKit's `@opentelemetry/api` import here. See `makeOpenTelemetryApiResolverPlugin`.
export * from '@opentelemetry/api';
44 changes: 43 additions & 1 deletion packages/sveltekit/src/vite/sentryVitePlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}

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

if (mergedOptions.autoInstrument) {
const pluginOptions: AutoInstrumentSelection = {
Expand Down Expand Up @@ -144,6 +148,44 @@ function makeBrowserTracingVariantResolverPlugin(): Plugin {
};
}

const OTEL_API_ID = '@opentelemetry/api';
const OTEL_API_REEXPORT_ID = '@sentry/sveltekit/opentelemetry-api';
const OTEL_API_REEXPORT_FILE_REGEX = /[\\/]opentelemetryApi\.js$/;

/**
* SvelteKit 3 externalizes `@opentelemetry/api` in server builds, so its runtime imports it at runtime
* from the app root. That only resolves if the app depends on it directly: under pnpm (or Yarn PnP),
* the copy installed through `@sentry/sveltekit` isn't reachable, and Kit fails every request once
* `tracing.server` is enabled.
*
* We redirect the import to our re-export instead, which resolves because the app always depends on
* `@sentry/sveltekit` directly. It stays external, so the Kit runtime and `instrumentation.server.js`
* still share a single module instance, which is why Kit externalizes it in the first place.
*/
function makeOpenTelemetryApiResolverPlugin(): Plugin {
let isKit3Promise: Promise<boolean> | undefined;

return {
name: 'sentry-sveltekit-opentelemetry-api',
enforce: 'pre',
// In dev, Kit's import resolves from Kit's own install location, where pnpm links the peer dep.
apply: 'build',
async resolveId(id, importer, options) {
// Skip our own re-export, in case it gets bundled, so it doesn't re-export itself.
if (id !== OTEL_API_ID || !options?.ssr || (importer && OTEL_API_REEXPORT_FILE_REGEX.test(importer))) {
return null;
}

isKit3Promise ??= isSvelteKit3(id => this.resolve(id, undefined, { skipSelf: true }));
if (!(await isKit3Promise)) {
return null;
}

return { id: OTEL_API_REEXPORT_ID, external: true };
},
};
}

/**
* Whether to use the SvelteKit 3 (`$app/state`) variant, from the installed `@sveltejs/kit` version
* (resolved via the bundler, not `process.cwd()`).
Expand Down
69 changes: 62 additions & 7 deletions packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { Plugin } from 'vite';
import { describe, expect, it, vi } from 'vitest';
import * as autoInstrument from '../../src/vite/autoInstrument';
Expand Down Expand Up @@ -64,10 +67,11 @@ describe('sentrySvelteKit()', () => {
const plugins = await getSentrySvelteKitPlugins();

expect(plugins).toBeInstanceOf(Array);
// 1 kit config resolver + 1 browser-tracing variant resolver + 1 auto instrument plugin
// 1 kit config resolver + 1 browser-tracing variant resolver + 1 OpenTelemetry API resolver
// + 1 auto instrument plugin
// + 1 orchestrion plugin + 1 global values injection plugin + 1 modified main plugin
// + 3 custom plugins
expect(plugins).toHaveLength(9);
expect(plugins).toHaveLength(10);
});

it('returns the custom sentry source maps upload plugin, unmodified sourcemaps plugins and the auto-instrument plugin by default', async () => {
Expand All @@ -78,6 +82,8 @@ describe('sentrySvelteKit()', () => {
'sentry-sveltekit-kit-config-resolver',
// browser-tracing variant resolver:
'sentry-sveltekit-browser-tracing-variant',
// OpenTelemetry API resolver:
'sentry-sveltekit-opentelemetry-api',
// auto instrument plugin:
'sentry-auto-instrumentation',
// orchestrion build-time instrumentation plugin:
Expand All @@ -95,17 +101,17 @@ describe('sentrySvelteKit()', () => {

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

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

process.env.NODE_ENV = 'development';
const plugins = await getSentrySvelteKitPlugins({ autoUploadSourceMaps: true, autoInstrument: true });
const instrumentPlugin = plugins[2];
const instrumentPlugin = plugins[3];

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

process.env.NODE_ENV = previousEnv;
Expand All @@ -114,7 +120,7 @@ describe('sentrySvelteKit()', () => {
it("doesn't return the auto instrument plugin if autoInstrument is `false`", async () => {
const plugins = await getSentrySvelteKitPlugins({ autoInstrument: false });
const pluginNames = plugins.map(plugin => plugin.name);
expect(plugins).toHaveLength(8); // kit config resolver + browser-tracing variant resolver + orchestrion + global values injection + 1 modified main plugin + 3 custom plugins
expect(plugins).toHaveLength(9); // kit config resolver + browser-tracing variant resolver + OpenTelemetry API resolver + orchestrion + global values injection + 1 modified main plugin + 3 custom plugins
expect(pluginNames).not.toContain('sentry-auto-instrumentation');
});

Expand Down Expand Up @@ -226,7 +232,7 @@ describe('sentrySvelteKit()', () => {
// just to ignore the source maps plugin:
autoUploadSourceMaps: false,
});
const plugin = plugins[2]!;
const plugin = plugins[3]!;

expect(plugin.name).toEqual('sentry-auto-instrumentation');
expect(makePluginSpy).toHaveBeenCalledWith({
Expand All @@ -238,6 +244,55 @@ describe('sentrySvelteKit()', () => {
});
});

describe('OpenTelemetry API resolver plugin', () => {
async function getResolver(kitVersion: string) {
const plugins = await getSentrySvelteKitPlugins({ autoUploadSourceMaps: false });
const plugin = plugins.find(p => p.name === 'sentry-sveltekit-opentelemetry-api')!;

const kitPackageJson = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-kit-')), 'package.json');
fs.writeFileSync(kitPackageJson, JSON.stringify({ version: kitVersion }));
const context = { resolve: vi.fn().mockResolvedValue({ id: kitPackageJson }) };

const resolveId = (id: string, importer: string | undefined, ssr = true) =>
// @ts-expect-error - minimal plugin context
plugin.resolveId.call(context, id, importer, { ssr });

return { plugin, resolveId };
}

it('only applies to builds', async () => {
const { plugin } = await getResolver('3.0.0');
expect(plugin.apply).toBe('build');
});

it('redirects `@opentelemetry/api` to the external re-export on SvelteKit 3', async () => {
const { resolveId } = await getResolver('3.0.0-next.28');

await expect(resolveId('@opentelemetry/api', '/app/node_modules/@sveltejs/kit/src/instance.js')).resolves.toEqual({
id: '@sentry/sveltekit/opentelemetry-api',
external: true,
});
});

it('leaves `@opentelemetry/api` alone on SvelteKit 2', async () => {
const { resolveId } = await getResolver('2.70.2');

await expect(resolveId('@opentelemetry/api', '/app/src/instrumentation.server.js')).resolves.toBeNull();
});

it('leaves client builds, other ids and the re-export itself alone', async () => {
const { resolveId } = await getResolver('3.0.0');

await expect(resolveId('@opentelemetry/api', '/app/src/hooks.client.js', false)).resolves.toBeNull();
await expect(
resolveId('@opentelemetry/api/experimental', '/app/src/instrumentation.server.js'),
).resolves.toBeNull();
await expect(
resolveId('@opentelemetry/api', '/app/node_modules/@sentry/sveltekit/build/esm/opentelemetryApi.js'),
).resolves.toBeNull();
});
});

describe('generateVitePluginOptions', () => {
it('returns null if no relevant options are provided', () => {
const options: SentrySvelteKitPluginOptions = {};
Expand Down
Loading