Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(sveltekit): Inject Sentry.init calls into server and client bundles#7391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
e5ad275bbb97b857023140d84ceee6019944607875f39b561File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { withSentryViteConfig } from './withSentryViteConfig'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import { logger } from '@sentry/utils'; | ||
| import * as fs from 'fs'; | ||
| import MagicString from 'magic-string'; | ||
| import * as path from 'path'; | ||
| import type { Plugin, TransformResult } from 'vite'; | ||
| const serverIndexFilePath = path.join('@sveltejs', 'kit', 'src', 'runtime', 'server', 'index.js'); | ||
| const devClientAppFilePath = path.join('generated', 'client', 'app.js'); | ||
| const prodClientAppFilePath = path.join('generated', 'client-optimized', 'app.js'); | ||
| /** | ||
| * This plugin injects the `Sentry.init` calls from `sentry.(client|server).config.(ts|js)` | ||
| * into SvelteKit runtime files. | ||
| */ | ||
| export const injectSentryInitPlugin: Plugin = { | ||
| name: 'sentry-init-injection-plugin', | ||
| // In this hook, we inject the `Sentry.init` calls from `sentry.(client|server).config.(ts|js)` | ||
| // into SvelteKit runtime files: For the server, we inject it into the server's `index.js` | ||
| // file. For the client, we use the `_app.js` file. | ||
| transform(code, id) { | ||
| if (id.endsWith(serverIndexFilePath)) { | ||
| logger.debug('Injecting Server Sentry.init into', id); | ||
| return addSentryConfigFileImport('server', code, id) || code; | ||
| } | ||
| if (id.endsWith(devClientAppFilePath) || id.endsWith(prodClientAppFilePath)) { | ||
| logger.debug('Injecting Client Sentry.init into', id); | ||
| return addSentryConfigFileImport('client', code, id) || code; | ||
| } | ||
| return code; | ||
| }, | ||
| // This plugin should run as early as possible, | ||
| // setting `enforce: 'pre'` ensures that it runs before the built-in vite plugins. | ||
| // see: https://vitejs.dev/guide/api-plugin.html#plugin-ordering | ||
| enforce: 'pre', | ||
| }; | ||
| function addSentryConfigFileImport( | ||
| platform: 'server' | 'client', | ||
| originalCode: string, | ||
| entryFileId: string, | ||
| ): TransformResult | undefined { | ||
| const projectRoot = process.cwd(); | ||
| const sentryConfigFilename = getUserConfigFile(projectRoot, platform); | ||
| if (!sentryConfigFilename) { | ||
| logger.error(`Could not find sentry.${platform}.config.(ts|js) file.`); | ||
| return undefined; | ||
| } | ||
| const filePath = path.join(path.relative(path.dirname(entryFileId), projectRoot), sentryConfigFilename); | ||
| const importStmt = `\nimport "${filePath}";`; | ||
| const ms = new MagicString(originalCode); | ||
| ms.append(importStmt); | ||
| return { code: ms.toString(), map: ms.generateMap() }; | ||
| } | ||
| function getUserConfigFile(projectDir: string, platform: 'server' | 'client'): string | undefined { | ||
| const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`]; | ||
| for (const filename of possibilities) { | ||
| if (fs.existsSync(path.resolve(projectDir, filename))) { | ||
| return filename; | ||
| } | ||
| } | ||
| throw new Error(`Cannot find '${possibilities[0]}' or '${possibilities[1]}' in '${projectDir}'.`); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import type { UserConfig, UserConfigExport } from 'vite'; | ||
| import { injectSentryInitPlugin } from './vitePlugins'; | ||
| /** | ||
| * This function adds Sentry-specific configuration to your Vite config. | ||
| * Pass your config to this function and make sure the return value is exported | ||
| * from your `vite.config.js` file. | ||
| * | ||
| * Note: If you're already wrapping your config with another wrapper, | ||
| * for instance with `defineConfig` from vitest, make sure | ||
| * that the Sentry wrapper is the outermost one. | ||
Lms24 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| * | ||
| * @param originalConfig your original vite config | ||
| * | ||
| * @returns a vite config with Sentry-specific configuration added to it. | ||
| */ | ||
| export function withSentryViteConfig(originalConfig: UserConfigExport): UserConfigExport { | ||
| if (typeof originalConfig === 'function') { | ||
| return function (this: unknown, ...viteConfigFunctionArgs: unknown[]): UserConfig | Promise<UserConfig> { | ||
| const userViteConfigObject = originalConfig.apply(this, viteConfigFunctionArgs); | ||
| if (userViteConfigObject instanceof Promise) { | ||
| return userViteConfigObject.then(userConfig => addSentryConfig(userConfig)); | ||
| } | ||
| return addSentryConfig(userViteConfigObject); | ||
| }; | ||
| } else if (originalConfig instanceof Promise) { | ||
| return originalConfig.then(userConfig => addSentryConfig(userConfig)); | ||
| } | ||
| return addSentryConfig(originalConfig); | ||
| } | ||
| function addSentryConfig(originalConfig: UserConfig): UserConfig { | ||
| const config = { | ||
| ...originalConfig, | ||
| plugins: originalConfig.plugins ? [injectSentryInitPlugin, ...originalConfig.plugins] : [injectSentryInitPlugin], | ||
| }; | ||
| const mergedDevServerFileSystemConfig: UserConfig['server'] = { | ||
| fs: { | ||
| ...(config.server && config.server.fs), | ||
| allow: [...((config.server && config.server.fs && config.server.fs.allow) || []), '.'], | ||
AbhiPrasad marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| }, | ||
| }; | ||
| config.server = { | ||
| ...config.server, | ||
| ...mergedDevServerFileSystemConfig, | ||
| }; | ||
| return config; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import * as fs from 'fs'; | ||
| import { injectSentryInitPlugin } from '../../src/config/vitePlugins'; | ||
| describe('injectSentryInitPlugin', () => { | ||
| it('has its basic properties set', () => { | ||
| expect(injectSentryInitPlugin.name).toBe('sentry-init-injection-plugin'); | ||
| expect(injectSentryInitPlugin.enforce).toBe('pre'); | ||
| expect(typeof injectSentryInitPlugin.transform).toBe('function'); | ||
| }); | ||
| describe('tansform', () => { | ||
| jest.spyOn(fs, 'existsSync').mockReturnValue(true); | ||
| it('transforms the server index file', () => { | ||
| const code = 'foo();'; | ||
| const id = '/node_modules/@sveltejs/kit/src/runtime/server/index.js'; | ||
| // @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that. | ||
| const result = injectSentryInitPlugin.transform(code, id); | ||
| expect(result.code).toMatch(/foo\(\);\n.*import ".*sentry\.server\.config\.ts";/gm); | ||
| expect(result.map).toBeDefined(); | ||
| }); | ||
| it('transforms the client index file (dev server)', () => { | ||
| const code = 'foo();'; | ||
| const id = '.svelte-kit/generated/client/app.js'; | ||
| // @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that. | ||
| const result = injectSentryInitPlugin.transform(code, id); | ||
| expect(result.code).toMatch(/foo\(\);\n.*import ".*sentry\.client\.config\.ts";/gm); | ||
| expect(result.map).toBeDefined(); | ||
| }); | ||
| it('transforms the client index file (prod build)', () => { | ||
| const code = 'foo();'; | ||
| const id = '.svelte-kit/generated/client-optimized/app.js'; | ||
| // @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that. | ||
| const result = injectSentryInitPlugin.transform(code, id); | ||
| expect(result.code).toMatch(/foo\(\);\n.*import ".*sentry\.client\.config\.ts";/gm); | ||
| expect(result.map).toBeDefined(); | ||
| }); | ||
| it("doesn't transform other files", () => { | ||
| const code = 'foo();'; | ||
| const id = './src/routes/+page.ts'; | ||
| // @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that. | ||
| const result = injectSentryInitPlugin.transform(code, id); | ||
| expect(result).toBe(code); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| import type { Plugin, UserConfig } from 'vite'; | ||
| import { withSentryViteConfig } from '../../src/config/withSentryViteConfig'; | ||
| describe('withSentryViteConfig', () => { | ||
| const originalConfig = { | ||
| plugins: [{ name: 'foo' }], | ||
Lms24 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| server: { | ||
| fs: { | ||
| allow: ['./bar'], | ||
| }, | ||
| }, | ||
| test: { | ||
| include: ['src/**/*.{test,spec}.{js,ts}'], | ||
| }, | ||
| }; | ||
| it('takes a POJO Vite config and returns the sentrified version', () => { | ||
| const sentrifiedConfig = withSentryViteConfig(originalConfig); | ||
| expect(typeof sentrifiedConfig).toBe('object'); | ||
| const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; | ||
| expect(plugins).toHaveLength(2); | ||
| expect(plugins[0].name).toBe('sentry-init-injection-plugin'); | ||
| expect(plugins[1].name).toBe('foo'); | ||
| expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']); | ||
| expect((sentrifiedConfig as any).test).toEqual(originalConfig.test); | ||
| }); | ||
| it('takes a Vite config Promise and returns the sentrified version', async () => { | ||
| const sentrifiedConfig = await withSentryViteConfig(Promise.resolve(originalConfig)); | ||
| expect(typeof sentrifiedConfig).toBe('object'); | ||
| const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; | ||
| expect(plugins).toHaveLength(2); | ||
| expect(plugins[0].name).toBe('sentry-init-injection-plugin'); | ||
| expect(plugins[1].name).toBe('foo'); | ||
| expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']); | ||
| expect((sentrifiedConfig as any).test).toEqual(originalConfig.test); | ||
| }); | ||
| it('takes a function returning a Vite config and returns the sentrified version', () => { | ||
| const sentrifiedConfigFunction = withSentryViteConfig(_env => { | ||
| return originalConfig; | ||
| }); | ||
| const sentrifiedConfig = | ||
| typeof sentrifiedConfigFunction === 'function' && sentrifiedConfigFunction({ command: 'build', mode: 'test' }); | ||
| expect(typeof sentrifiedConfig).toBe('object'); | ||
| const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; | ||
| expect(plugins).toHaveLength(2); | ||
| expect(plugins[0].name).toBe('sentry-init-injection-plugin'); | ||
| expect(plugins[1].name).toBe('foo'); | ||
| expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']); | ||
| expect((sentrifiedConfig as any).test).toEqual(originalConfig.test); | ||
| }); | ||
| it('takes a function returning a Vite config promise and returns the sentrified version', async () => { | ||
| const sentrifiedConfigFunction = withSentryViteConfig(_env => { | ||
| return Promise.resolve(originalConfig); | ||
| }); | ||
| const sentrifiedConfig = | ||
| typeof sentrifiedConfigFunction === 'function' && | ||
| (await sentrifiedConfigFunction({ command: 'build', mode: 'test' })); | ||
| expect(typeof sentrifiedConfig).toBe('object'); | ||
| const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; | ||
| expect(plugins).toHaveLength(2); | ||
| expect(plugins[0].name).toBe('sentry-init-injection-plugin'); | ||
| expect(plugins[1].name).toBe('foo'); | ||
| expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']); | ||
| expect((sentrifiedConfig as any).test).toEqual(originalConfig.test); | ||
| }); | ||
| it('adds the vite plugin if no plugins are present', () => { | ||
| const sentrifiedConfig = withSentryViteConfig({ | ||
| test: { | ||
| include: ['src/**/*.{test,spec}.{js,ts}'], | ||
| }, | ||
| } as UserConfig); | ||
| expect(typeof sentrifiedConfig).toBe('object'); | ||
| const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; | ||
| expect(plugins).toHaveLength(1); | ||
| expect(plugins[0].name).toBe('sentry-init-injection-plugin'); | ||
| }); | ||
| it('adds the vite plugin and server config to an empty vite config', () => { | ||
| const sentrifiedConfig = withSentryViteConfig({}); | ||
| expect(typeof sentrifiedConfig).toBe('object'); | ||
| const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; | ||
| expect(plugins).toHaveLength(1); | ||
| expect(plugins[0].name).toBe('sentry-init-injection-plugin'); | ||
| expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['.']); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.