Skip to content
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

feat(sveltekit): Inject Sentry.init calls into server and client bundles #7391

Merged
merged 7 commits into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 5 additions & 3 deletions packages/sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@
"@sentry/node": "7.42.0",
"@sentry/svelte": "7.42.0",
"@sentry/types": "7.42.0",
"@sentry/utils": "7.42.0"
"@sentry/utils": "7.42.0",
"magic-string": "^0.30.0"
},
"devDependencies": {
"@sveltejs/kit": "^1.10.0",
"vite": "^4.0.0"
"@sveltejs/kit": "^1.5.0",
Lms24 marked this conversation as resolved.
Show resolved Hide resolved
"vite": "4.0.0",
"typescript": "^4.9.3"
},
"scripts": {
"build": "run-p build:transpile build:types",
Expand Down
1 change: 1 addition & 0 deletions packages/sveltekit/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { withSentryViteConfig } from './withSentryViteConfig';
73 changes: 73 additions & 0 deletions packages/sveltekit/src/config/vitePlugins.ts
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';

/**
* 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) {
const serverIndexFilePath = path.join('@sveltejs', 'kit', 'src', 'runtime', 'server', 'index.js');
const devClientAppFilePath = path.join('.svelte-kit', 'generated', 'client', 'app.js');
const prodClientAppFilePath = path.join('.svelte-kit', 'generated', 'client-optimized', 'app.js');
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved

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}'.`);
}
56 changes: 56 additions & 0 deletions packages/sveltekit/src/config/withSentryViteConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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 };

const { plugins } = config;
if (!plugins) {
config.plugins = [injectSentryInitPlugin];
} else {
config.plugins = [injectSentryInitPlugin, ...plugins];
}
Lms24 marked this conversation as resolved.
Show resolved Hide resolved

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;
}
1 change: 1 addition & 0 deletions packages/sveltekit/src/index.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './server';
export * from './config';

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

Expand Down
1 change: 1 addition & 0 deletions packages/sveltekit/src/index.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// Some of the exports collide, which is not allowed, unless we redifine the colliding
// exports in this file - which we do below.
export * from './client';
export * from './config';
export * from './server';

import type { Integration, Options, StackParser } from '@sentry/types';
Expand Down
58 changes: 58 additions & 0 deletions packages/sveltekit/test/config/vitePlugins.test.ts
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);
});
});
});
90 changes: 90 additions & 0 deletions packages/sveltekit/test/config/withSentryViteConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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 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);
});
});
30 changes: 15 additions & 15 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4205,10 +4205,10 @@
dependencies:
highlight.js "^9.15.6"

"@sveltejs/kit@^1.10.0":
version "1.10.0"
resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-1.10.0.tgz#17d3565e5903f6d2c0730197fd875c2cf921ad01"
integrity sha512-0P35zHrByfbF3Ym3RdQL+RvzgsCDSyO3imSwuZ67XAD5HoCQFF3a8Mhh0V3sObz3rc5aJd4Qn82UpAihJqZ6gQ==
"@sveltejs/kit@^1.5.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-1.11.0.tgz#23f233c351e5956356ba6f3206e40637c5f5dbda"
integrity sha512-PwViZcMoLgEU/jhLoSyjf5hSrHS67wvSm0ifBo4prP9irpGa5HuPOZeVDTL5tPDSBoKxtdYi1zlGdoiJfO86jA==
dependencies:
"@sveltejs/vite-plugin-svelte" "^2.0.0"
"@types/cookie" "^0.5.1"
Expand Down Expand Up @@ -12057,7 +12057,7 @@ esbuild@0.13.8:
esbuild-windows-64 "0.13.8"
esbuild-windows-arm64 "0.13.8"

esbuild@^0.16.14:
esbuild@^0.16.3:
version "0.16.17"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.17.tgz#fc2c3914c57ee750635fee71b89f615f25065259"
integrity sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==
Expand Down Expand Up @@ -21687,7 +21687,7 @@ postcss@^8.1.10, postcss@^8.1.7, postcss@^8.2.15:
picocolors "^1.0.0"
source-map-js "^1.0.2"

postcss@^8.2.4, postcss@^8.3.5, postcss@^8.3.7, postcss@^8.4.21:
postcss@^8.2.4, postcss@^8.3.5, postcss@^8.3.7, postcss@^8.4.19:
version "8.4.21"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4"
integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
Expand Down Expand Up @@ -23284,7 +23284,7 @@ rollup@^2.45.1:
optionalDependencies:
fsevents "~2.3.2"

rollup@^3.10.0:
rollup@^3.7.0:
version "3.18.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.18.0.tgz#2354ba63ba66d6a09c652c3ea0dbcd9dad72bbde"
integrity sha512-J8C6VfEBjkvYPESMQYxKHxNOh4A5a3FlP+0BETGo34HEcE4eTlgCrO2+eWzlu2a/sHs2QUkZco+wscH7jhhgWg==
Expand Down Expand Up @@ -25983,7 +25983,7 @@ typescript@^3.9.5, typescript@^3.9.7:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.9.tgz#e69905c54bc0681d0518bd4d587cc6f2d0b1a674"
integrity sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==

typescript@^4.9.4:
typescript@^4.9.3, typescript@^4.9.4:
version "4.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
Expand Down Expand Up @@ -26518,15 +26518,15 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"

vite@^4.0.0:
version "4.1.4"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.1.4.tgz#170d93bcff97e0ebc09764c053eebe130bfe6ca0"
integrity sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==
vite@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.0.0.tgz#b81b88349a06b2faaa53ae14cf96c942548e3454"
integrity sha512-ynad+4kYs8Jcnn8J7SacS9vAbk7eMy0xWg6E7bAhS1s79TK+D7tVFGXVZ55S7RNLRROU1rxoKlvZ/qjaB41DGA==
dependencies:
esbuild "^0.16.14"
postcss "^8.4.21"
esbuild "^0.16.3"
postcss "^8.4.19"
resolve "^1.22.1"
rollup "^3.10.0"
rollup "^3.7.0"
optionalDependencies:
fsevents "~2.3.2"

Expand Down