Skip to content

Commit cc26081

Browse files
authored
feat(sveltekit): Inject Sentry.init calls into server and client bundles (#7391)
Add an initial version of `Sentry.init` call injection to our new SvelteKit SDK: Specifically, we add a `withSentryViteConfig` wrapper function, which users will need to wrap around their Vite config. This will: * Inject a Vite plugin which takes care of injecting `Sentry.init` calls from `sentry.(client|server).config.(ts|js)` files, providing a DX identical to the NextJS SDK. * The server-side init is injected into the server `index.js` file * The client-side init is injected into the `app.js` file * The injection works both for production builds (with the Node adapter for now) as well as for a local dev server * Add the root directory of the project to the allowed directories for the Vite dev server. We need this so that the client config is correctly picked up by the Vite dev server.
1 parent 94266d7 commit cc26081

File tree

9 files changed

+324
-18
lines changed

9 files changed

+324
-18
lines changed

packages/sveltekit/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
"@sentry/node": "7.42.0",
2525
"@sentry/svelte": "7.42.0",
2626
"@sentry/types": "7.42.0",
27-
"@sentry/utils": "7.42.0"
27+
"@sentry/utils": "7.42.0",
28+
"magic-string": "^0.30.0"
2829
},
2930
"devDependencies": {
30-
"@sveltejs/kit": "^1.10.0",
31-
"vite": "^4.0.0"
31+
"@sveltejs/kit": "^1.5.0",
32+
"vite": "4.0.0",
33+
"typescript": "^4.9.3"
3234
},
3335
"scripts": {
3436
"build": "run-p build:transpile build:types",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { withSentryViteConfig } from './withSentryViteConfig';
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { logger } from '@sentry/utils';
2+
import * as fs from 'fs';
3+
import MagicString from 'magic-string';
4+
import * as path from 'path';
5+
import type { Plugin, TransformResult } from 'vite';
6+
7+
const serverIndexFilePath = path.join('@sveltejs', 'kit', 'src', 'runtime', 'server', 'index.js');
8+
const devClientAppFilePath = path.join('generated', 'client', 'app.js');
9+
const prodClientAppFilePath = path.join('generated', 'client-optimized', 'app.js');
10+
11+
/**
12+
* This plugin injects the `Sentry.init` calls from `sentry.(client|server).config.(ts|js)`
13+
* into SvelteKit runtime files.
14+
*/
15+
export const injectSentryInitPlugin: Plugin = {
16+
name: 'sentry-init-injection-plugin',
17+
18+
// In this hook, we inject the `Sentry.init` calls from `sentry.(client|server).config.(ts|js)`
19+
// into SvelteKit runtime files: For the server, we inject it into the server's `index.js`
20+
// file. For the client, we use the `_app.js` file.
21+
transform(code, id) {
22+
if (id.endsWith(serverIndexFilePath)) {
23+
logger.debug('Injecting Server Sentry.init into', id);
24+
return addSentryConfigFileImport('server', code, id) || code;
25+
}
26+
27+
if (id.endsWith(devClientAppFilePath) || id.endsWith(prodClientAppFilePath)) {
28+
logger.debug('Injecting Client Sentry.init into', id);
29+
return addSentryConfigFileImport('client', code, id) || code;
30+
}
31+
32+
return code;
33+
},
34+
35+
// This plugin should run as early as possible,
36+
// setting `enforce: 'pre'` ensures that it runs before the built-in vite plugins.
37+
// see: https://vitejs.dev/guide/api-plugin.html#plugin-ordering
38+
enforce: 'pre',
39+
};
40+
41+
function addSentryConfigFileImport(
42+
platform: 'server' | 'client',
43+
originalCode: string,
44+
entryFileId: string,
45+
): TransformResult | undefined {
46+
const projectRoot = process.cwd();
47+
const sentryConfigFilename = getUserConfigFile(projectRoot, platform);
48+
49+
if (!sentryConfigFilename) {
50+
logger.error(`Could not find sentry.${platform}.config.(ts|js) file.`);
51+
return undefined;
52+
}
53+
54+
const filePath = path.join(path.relative(path.dirname(entryFileId), projectRoot), sentryConfigFilename);
55+
const importStmt = `\nimport "${filePath}";`;
56+
57+
const ms = new MagicString(originalCode);
58+
ms.append(importStmt);
59+
60+
return { code: ms.toString(), map: ms.generateMap() };
61+
}
62+
63+
function getUserConfigFile(projectDir: string, platform: 'server' | 'client'): string | undefined {
64+
const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`];
65+
66+
for (const filename of possibilities) {
67+
if (fs.existsSync(path.resolve(projectDir, filename))) {
68+
return filename;
69+
}
70+
}
71+
72+
throw new Error(`Cannot find '${possibilities[0]}' or '${possibilities[1]}' in '${projectDir}'.`);
73+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { UserConfig, UserConfigExport } from 'vite';
2+
3+
import { injectSentryInitPlugin } from './vitePlugins';
4+
5+
/**
6+
* This function adds Sentry-specific configuration to your Vite config.
7+
* Pass your config to this function and make sure the return value is exported
8+
* from your `vite.config.js` file.
9+
*
10+
* Note: If you're already wrapping your config with another wrapper,
11+
* for instance with `defineConfig` from vitest, make sure
12+
* that the Sentry wrapper is the outermost one.
13+
*
14+
* @param originalConfig your original vite config
15+
*
16+
* @returns a vite config with Sentry-specific configuration added to it.
17+
*/
18+
export function withSentryViteConfig(originalConfig: UserConfigExport): UserConfigExport {
19+
if (typeof originalConfig === 'function') {
20+
return function (this: unknown, ...viteConfigFunctionArgs: unknown[]): UserConfig | Promise<UserConfig> {
21+
const userViteConfigObject = originalConfig.apply(this, viteConfigFunctionArgs);
22+
if (userViteConfigObject instanceof Promise) {
23+
return userViteConfigObject.then(userConfig => addSentryConfig(userConfig));
24+
}
25+
return addSentryConfig(userViteConfigObject);
26+
};
27+
} else if (originalConfig instanceof Promise) {
28+
return originalConfig.then(userConfig => addSentryConfig(userConfig));
29+
}
30+
return addSentryConfig(originalConfig);
31+
}
32+
33+
function addSentryConfig(originalConfig: UserConfig): UserConfig {
34+
const config = {
35+
...originalConfig,
36+
plugins: originalConfig.plugins ? [injectSentryInitPlugin, ...originalConfig.plugins] : [injectSentryInitPlugin],
37+
};
38+
39+
const mergedDevServerFileSystemConfig: UserConfig['server'] = {
40+
fs: {
41+
...(config.server && config.server.fs),
42+
allow: [...((config.server && config.server.fs && config.server.fs.allow) || []), '.'],
43+
},
44+
};
45+
46+
config.server = {
47+
...config.server,
48+
...mergedDevServerFileSystemConfig,
49+
};
50+
51+
return config;
52+
}

packages/sveltekit/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './server';
2+
export * from './config';
23

34
// This file is the main entrypoint on the server and/or when the package is `require`d
45

packages/sveltekit/src/index.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// Some of the exports collide, which is not allowed, unless we redifine the colliding
55
// exports in this file - which we do below.
66
export * from './client';
7+
export * from './config';
78
export * from './server';
89

910
import type { Integration, Options, StackParser } from '@sentry/types';
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as fs from 'fs';
2+
3+
import { injectSentryInitPlugin } from '../../src/config/vitePlugins';
4+
5+
describe('injectSentryInitPlugin', () => {
6+
it('has its basic properties set', () => {
7+
expect(injectSentryInitPlugin.name).toBe('sentry-init-injection-plugin');
8+
expect(injectSentryInitPlugin.enforce).toBe('pre');
9+
expect(typeof injectSentryInitPlugin.transform).toBe('function');
10+
});
11+
12+
describe('tansform', () => {
13+
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
14+
15+
it('transforms the server index file', () => {
16+
const code = 'foo();';
17+
const id = '/node_modules/@sveltejs/kit/src/runtime/server/index.js';
18+
19+
// @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that.
20+
const result = injectSentryInitPlugin.transform(code, id);
21+
22+
expect(result.code).toMatch(/foo\(\);\n.*import ".*sentry\.server\.config\.ts";/gm);
23+
expect(result.map).toBeDefined();
24+
});
25+
26+
it('transforms the client index file (dev server)', () => {
27+
const code = 'foo();';
28+
const id = '.svelte-kit/generated/client/app.js';
29+
30+
// @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that.
31+
const result = injectSentryInitPlugin.transform(code, id);
32+
33+
expect(result.code).toMatch(/foo\(\);\n.*import ".*sentry\.client\.config\.ts";/gm);
34+
expect(result.map).toBeDefined();
35+
});
36+
37+
it('transforms the client index file (prod build)', () => {
38+
const code = 'foo();';
39+
const id = '.svelte-kit/generated/client-optimized/app.js';
40+
41+
// @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that.
42+
const result = injectSentryInitPlugin.transform(code, id);
43+
44+
expect(result.code).toMatch(/foo\(\);\n.*import ".*sentry\.client\.config\.ts";/gm);
45+
expect(result.map).toBeDefined();
46+
});
47+
48+
it("doesn't transform other files", () => {
49+
const code = 'foo();';
50+
const id = './src/routes/+page.ts';
51+
52+
// @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that.
53+
const result = injectSentryInitPlugin.transform(code, id);
54+
55+
expect(result).toBe(code);
56+
});
57+
});
58+
});
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import type { Plugin, UserConfig } from 'vite';
2+
3+
import { withSentryViteConfig } from '../../src/config/withSentryViteConfig';
4+
5+
describe('withSentryViteConfig', () => {
6+
const originalConfig = {
7+
plugins: [{ name: 'foo' }],
8+
server: {
9+
fs: {
10+
allow: ['./bar'],
11+
},
12+
},
13+
test: {
14+
include: ['src/**/*.{test,spec}.{js,ts}'],
15+
},
16+
};
17+
18+
it('takes a POJO Vite config and returns the sentrified version', () => {
19+
const sentrifiedConfig = withSentryViteConfig(originalConfig);
20+
21+
expect(typeof sentrifiedConfig).toBe('object');
22+
23+
const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[];
24+
25+
expect(plugins).toHaveLength(2);
26+
expect(plugins[0].name).toBe('sentry-init-injection-plugin');
27+
expect(plugins[1].name).toBe('foo');
28+
29+
expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']);
30+
31+
expect((sentrifiedConfig as any).test).toEqual(originalConfig.test);
32+
});
33+
34+
it('takes a Vite config Promise and returns the sentrified version', async () => {
35+
const sentrifiedConfig = await withSentryViteConfig(Promise.resolve(originalConfig));
36+
37+
expect(typeof sentrifiedConfig).toBe('object');
38+
39+
const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[];
40+
41+
expect(plugins).toHaveLength(2);
42+
expect(plugins[0].name).toBe('sentry-init-injection-plugin');
43+
expect(plugins[1].name).toBe('foo');
44+
45+
expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']);
46+
47+
expect((sentrifiedConfig as any).test).toEqual(originalConfig.test);
48+
});
49+
50+
it('takes a function returning a Vite config and returns the sentrified version', () => {
51+
const sentrifiedConfigFunction = withSentryViteConfig(_env => {
52+
return originalConfig;
53+
});
54+
const sentrifiedConfig =
55+
typeof sentrifiedConfigFunction === 'function' && sentrifiedConfigFunction({ command: 'build', mode: 'test' });
56+
57+
expect(typeof sentrifiedConfig).toBe('object');
58+
59+
const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[];
60+
61+
expect(plugins).toHaveLength(2);
62+
expect(plugins[0].name).toBe('sentry-init-injection-plugin');
63+
expect(plugins[1].name).toBe('foo');
64+
65+
expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']);
66+
67+
expect((sentrifiedConfig as any).test).toEqual(originalConfig.test);
68+
});
69+
70+
it('takes a function returning a Vite config promise and returns the sentrified version', async () => {
71+
const sentrifiedConfigFunction = withSentryViteConfig(_env => {
72+
return Promise.resolve(originalConfig);
73+
});
74+
const sentrifiedConfig =
75+
typeof sentrifiedConfigFunction === 'function' &&
76+
(await sentrifiedConfigFunction({ command: 'build', mode: 'test' }));
77+
78+
expect(typeof sentrifiedConfig).toBe('object');
79+
80+
const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[];
81+
82+
expect(plugins).toHaveLength(2);
83+
expect(plugins[0].name).toBe('sentry-init-injection-plugin');
84+
expect(plugins[1].name).toBe('foo');
85+
86+
expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']);
87+
88+
expect((sentrifiedConfig as any).test).toEqual(originalConfig.test);
89+
});
90+
91+
it('adds the vite plugin if no plugins are present', () => {
92+
const sentrifiedConfig = withSentryViteConfig({
93+
test: {
94+
include: ['src/**/*.{test,spec}.{js,ts}'],
95+
},
96+
} as UserConfig);
97+
98+
expect(typeof sentrifiedConfig).toBe('object');
99+
100+
const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[];
101+
102+
expect(plugins).toHaveLength(1);
103+
expect(plugins[0].name).toBe('sentry-init-injection-plugin');
104+
});
105+
106+
it('adds the vite plugin and server config to an empty vite config', () => {
107+
const sentrifiedConfig = withSentryViteConfig({});
108+
109+
expect(typeof sentrifiedConfig).toBe('object');
110+
111+
const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[];
112+
113+
expect(plugins).toHaveLength(1);
114+
expect(plugins[0].name).toBe('sentry-init-injection-plugin');
115+
116+
expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['.']);
117+
});
118+
});

0 commit comments

Comments
 (0)