-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
e5ad275
feat(sveltekit): Inject `Sentry.init` calls into server and client bu…
Lms24 bbb97b8
cleanup/fix linting errors
Lms24 5702314
s/server/client placeholder
Lms24 0d84cee
improve and add more tests
Lms24 e601994
simplify plugin injection code
Lms24 4607875
make client app.js file check more versatile
Lms24 f39b561
fix linter error
Lms24 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { withSentryViteConfig } from './withSentryViteConfig'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}'.`); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Show resolved
Hide resolved
|
||
* | ||
* @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.
Show resolved
Hide resolved
|
||
}, | ||
}; | ||
|
||
config.server = { | ||
...config.server, | ||
...mergedDevServerFileSystemConfig, | ||
}; | ||
|
||
return config; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
118 changes: 118 additions & 0 deletions
118
packages/sveltekit/test/config/withSentryViteConfig.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Show resolved
Hide resolved
|
||
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(['.']); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.