Skip to content

Commit ae469bf

Browse files
committed
feat(sveltekit): Inject Sentry.init calls into server and client bundles
1 parent 17f31c6 commit ae469bf

File tree

10 files changed

+309
-20
lines changed

10 files changed

+309
-20
lines changed

packages/sveltekit/package.json

+5-3
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
"@sentry/node": "7.41.0",
2525
"@sentry/svelte": "7.41.0",
2626
"@sentry/types": "7.41.0",
27-
"@sentry/utils": "7.41.0"
27+
"@sentry/utils": "7.41.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",
+9-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
export * from '@sentry/svelte';
22

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

packages/sveltekit/src/index.server.ts

+1
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

+2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
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';
11+
import { UserConfig, UserConfigExport } from 'vite';
1012

1113
import type * as clientSdk from './client';
1214
import type * as serverSdk from './server';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { injectSentryInitPlugin } from '../../src/config/vitePlugins';
2+
3+
import * as fs from 'fs';
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+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { UserConfig, Plugin } from 'vite';
2+
import { withSentryViteConfig } from '../../src/config/withSentryViteConfig';
3+
4+
describe('withSentryViteConfig', () => {
5+
const originalConfig = {
6+
plugins: [{ name: 'foo' }],
7+
server: {
8+
fs: {
9+
allow: ['./bar'],
10+
},
11+
},
12+
test: {
13+
include: ['src/**/*.{test,spec}.{js,ts}'],
14+
},
15+
};
16+
17+
it('takes a POJO Vite config and returns the sentrified version', () => {
18+
const sentrifiedConfig = withSentryViteConfig(originalConfig);
19+
20+
expect(typeof sentrifiedConfig).toBe('object');
21+
22+
const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[];
23+
24+
expect(plugins).toHaveLength(2);
25+
expect(plugins[0].name).toBe('sentry-init-injection-plugin');
26+
expect(plugins[1].name).toBe('foo');
27+
28+
expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']);
29+
30+
expect((sentrifiedConfig as any).test).toEqual(originalConfig.test);
31+
});
32+
33+
it('takes a Vite config Promise and returns the sentrified version', async () => {
34+
const sentrifiedConfig = await withSentryViteConfig(Promise.resolve(originalConfig));
35+
36+
expect(typeof sentrifiedConfig).toBe('object');
37+
38+
const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[];
39+
40+
expect(plugins).toHaveLength(2);
41+
expect(plugins[0].name).toBe('sentry-init-injection-plugin');
42+
expect(plugins[1].name).toBe('foo');
43+
44+
expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']);
45+
46+
expect((sentrifiedConfig as any).test).toEqual(originalConfig.test);
47+
});
48+
49+
it('takes a function returning a Vite config and returns the sentrified version', () => {
50+
const sentrifiedConfigFunction = withSentryViteConfig(_env => {
51+
return originalConfig;
52+
});
53+
const sentrifiedConfig =
54+
typeof sentrifiedConfigFunction === 'function' && sentrifiedConfigFunction({ command: 'build', mode: 'test' });
55+
56+
expect(typeof sentrifiedConfig).toBe('object');
57+
58+
const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[];
59+
60+
expect(plugins).toHaveLength(2);
61+
expect(plugins[0].name).toBe('sentry-init-injection-plugin');
62+
expect(plugins[1].name).toBe('foo');
63+
64+
expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']);
65+
66+
expect((sentrifiedConfig as any).test).toEqual(originalConfig.test);
67+
});
68+
69+
it('takes a function returning a Vite config promise and returns the sentrified version', async () => {
70+
const sentrifiedConfigFunction = withSentryViteConfig(_env => {
71+
return originalConfig;
72+
});
73+
const sentrifiedConfig =
74+
typeof sentrifiedConfigFunction === 'function' &&
75+
(await sentrifiedConfigFunction({ command: 'build', mode: 'test' }));
76+
77+
expect(typeof sentrifiedConfig).toBe('object');
78+
79+
const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[];
80+
81+
expect(plugins).toHaveLength(2);
82+
expect(plugins[0].name).toBe('sentry-init-injection-plugin');
83+
expect(plugins[1].name).toBe('foo');
84+
85+
expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']);
86+
87+
expect((sentrifiedConfig as any).test).toEqual(originalConfig.test);
88+
});
89+
});

0 commit comments

Comments
 (0)