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): Add Sentry Vite Plugin to upload source maps #7811

Merged
merged 4 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 5 additions & 5 deletions packages/sveltekit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,20 +150,20 @@ The Sentry SvelteKit SDK mostly relies on [SvelteKit Hooks](https://kit.svelte.d

### 5. Vite Setup

1. Add our `sentrySvelteKitPlugin` to your `vite.config.(js|ts)` file so that the Sentry SDK can apply build-time features.
Make sure that it is added before the `sveltekit` plugin:
1. Add our `sentryVite` plugins to your `vite.config.(js|ts)` file so that the Sentry SDK can apply build-time features.
Make sure that it is added before the `sveltekit` plugin:

```javascript
import { sveltekit } from '@sveltejs/kit/vite';
import { sentrySvelteKitPlugin } from '@sentry/sveltekit';
import { sentryVite } from '@sentry/sveltekit';

export default {
plugins: [sentrySvelteKitPlugin(), sveltekit()],
plugins: [sentryVite(), sveltekit()],
Lms24 marked this conversation as resolved.
Show resolved Hide resolved
// ... rest of your Vite config
};
```

In the near future this plugin will add and configure our [Sentry Vite Plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/vite-plugin) to automatically upload source maps to Sentry.
This adds the [Sentry Vite Plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/vite-plugin) to your Vite config to automatically upload source maps to Sentry.

## Known Limitations

Expand Down
4 changes: 3 additions & 1 deletion packages/sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"@sentry/svelte": "7.47.0",
"@sentry/types": "7.47.0",
"@sentry/utils": "7.47.0",
"magic-string": "^0.30.0"
"@sentry/vite-plugin": "^0.6.0",
"magic-string": "^0.30.0",
"sorcery": "^0.11.0"
Lms24 marked this conversation as resolved.
Show resolved Hide resolved
},
"devDependencies": {
"@sveltejs/kit": "^1.11.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/sveltekit/src/vite/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { sentrySvelteKitPlugin } from './sentrySvelteKitPlugin';
export { sentryVite } from './sentryVitePlugins';
33 changes: 0 additions & 33 deletions packages/sveltekit/src/vite/sentrySvelteKitPlugin.ts

This file was deleted.

57 changes: 57 additions & 0 deletions packages/sveltekit/src/vite/sentryVitePlugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { SentryVitePluginOptions } from '@sentry/vite-plugin';
import type { Plugin } from 'vite';

import { makeCustomSentryVitePlugin } from './sourceMaps';

type SourceMapsUploadOptions = {
/**
* If this flag is `true`, the Sentry plugins will automatically upload source maps to Sentry.
* Defaults to `true`.
*/
autoUploadSourceMaps?: boolean;

/**
* Options for the Sentry Vite plugin to customize and override the release creation and source maps upload process.
* See [Sentry Vite Plugin Options](https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/vite-plugin#configuration) for a detailed description.
*/
sourceMapsUploadOptions?: Partial<SentryVitePluginOptions>;
};

export type SentrySvelteKitPluginOptions = {
/**
* If this flag is `true`, the Sentry plugins will log some useful debug information.
* Defaults to `false`.
*/
debug?: boolean;
} & SourceMapsUploadOptions;

const DEFAULT_PLUGIN_OPTIONS: SentrySvelteKitPluginOptions = {
autoUploadSourceMaps: true,
debug: false,
};

/**
* Vite Plugins for the Sentry SvelteKit SDK, taking care of creating
* Sentry releases and uploading source maps to Sentry.
*
* Sentry adds a few additional properties to your Vite config.
* Make sure, it is registered before the SvelteKit plugin.
*/
export function sentryVite(options: SentrySvelteKitPluginOptions = {}): Plugin[] {
const mergedOptions = {
...DEFAULT_PLUGIN_OPTIONS,
...options,
};

const sentryPlugins = [];

if (mergedOptions.autoUploadSourceMaps) {
const pluginOptions = {
...mergedOptions.sourceMapsUploadOptions,
debug: mergedOptions.debug, // override the plugin's debug flag with the one from the top-level options
};
sentryPlugins.push(makeCustomSentryVitePlugin(pluginOptions));
}

return sentryPlugins;
}
151 changes: 151 additions & 0 deletions packages/sveltekit/src/vite/sourceMaps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import type { SentryVitePluginOptions } from '@sentry/vite-plugin';
import { sentryVitePlugin } from '@sentry/vite-plugin';
import * as fs from 'fs';
import * as path from 'path';
// @ts-ignore -sorcery has no types :(
// eslint-disable-next-line import/default
import * as sorcery from 'sorcery';
import type { Plugin } from 'vite';

const DEFAULT_PLUGIN_OPTIONS: SentryVitePluginOptions = {
// TODO: Read these values from the node adapter somehow as the out dir can be changed in the adapter options
include: ['build/server', 'build/client'],
};

// sorcery has no types, so these are some basic type definitions:
type Chain = {
write(): Promise<void>;
apply(): Promise<void>;
};
type Sorcery = {
load(filepath: string): Promise<Chain>;
};

type SentryVitePluginOptionsOptionalInclude = Omit<SentryVitePluginOptions, 'include'> & {
include?: SentryVitePluginOptions['include'];
};

/**
* Creates a new Vite plugin that uses the unplugin-based Sentry Vite plugin to create
* releases and upload source maps to Sentry.
*
* Because the unplugin-based Sentry Vite plugin doesn't work ootb with SvelteKit,
* we need to add some additional stuff to make source maps work:
*
* - the `config` hook needs to be added to generate source maps
* - the `configResolved` hook tells us when to upload source maps.
* We only want to upload once at the end, given that SvelteKit makes multiple builds
* - the `closeBundle` hook is used to flatten server source maps, which at the moment is necessary for SvelteKit.
* After the maps are flattened, they're uploaded to Sentry as in the original plugin.
* see: https://github.com/sveltejs/kit/discussions/9608
*
* @returns the custom Sentry Vite plugin
*/
export function makeCustomSentryVitePlugin(options?: SentryVitePluginOptionsOptionalInclude): Plugin {
const mergedOptions = {
...DEFAULT_PLUGIN_OPTIONS,
...options,
};
const sentryPlugin: Plugin = sentryVitePlugin(mergedOptions);

const { debug } = mergedOptions;
const { buildStart, resolveId, transform, renderChunk } = sentryPlugin;

let upload = true;

const customPlugin: Plugin = {
name: 'sentry-vite-plugin-custom',
apply: 'build', // only apply this plugin at build time
enforce: 'post',

// These hooks are copied from the original Sentry Vite plugin.
// They're mostly responsible for options parsing and release injection.
buildStart,
resolveId,
renderChunk,
transform,

// Modify the config to generate source maps
config: config => {
// eslint-disable-next-line no-console
debug && console.log('[Source Maps Plugin] Enabeling source map generation');
return {
...config,
build: {
...config.build,
sourcemap: true,
},
};
},

configResolved: config => {
// The SvelteKit plugins trigger additional builds within the main (SSR) build.
// We just need a mechanism to upload source maps only once.
// `config.build.ssr` is `true` for that first build and `false` in the other ones.
// Hence we can use it as a switch to upload source maps only once in main build.
if (!config.build.ssr) {
upload = false;
}
},

// We need to start uploading source maps later than in the original plugin
// because SvelteKit is still doing some stuff at closeBundle.
closeBundle: () => {
if (!upload) {
return;
}

// TODO: Read the out dir from the node adapter somehow as it can be changed in the adapter options
const outDir = path.resolve(process.cwd(), 'build');

const jsFiles = getFiles(outDir).filter(file => file.endsWith('.js'));
// eslint-disable-next-line no-console
debug && console.log('[Source Maps Plugin] Flattening source maps');

jsFiles.forEach(async file => {
try {
await (sorcery as Sorcery).load(file).then(async chain => {
if (!chain) {
// We end up here, if we don't have a source map for the file.
// This is fine, as we're not interested in files w/o source maps.
return;
}
// This flattens the source map
await chain.apply();
// Write it back to the original file
await chain.write();
});
} catch (e) {
// Sometimes sorcery fails to flatten the source map. While this isn't ideal, it seems to be mostly
// happening in Kit-internal files which is fine as they're not in-app.
// This mostly happens when sorcery tries to resolve a source map while flattening that doesn't exist.
const isKnownError = e instanceof Error && e.message.includes('ENOENT: no such file or directory, open');
if (debug && !isKnownError) {
// eslint-disable-next-line no-console
console.error('[Source Maps Plugin] error while flattening', file, e);
}
}
});

// @ts-ignore - this hook exists on the plugin!
sentryPlugin.writeBundle();
},
};

return customPlugin;
}

function getFiles(dir: string): string[] {
if (!fs.existsSync(dir)) {
return [];
}
const dirents = fs.readdirSync(dir, { withFileTypes: true });
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const files: string[] = dirents.map(dirent => {
const resFileOrDir = path.resolve(dir, dirent.name);
return dirent.isDirectory() ? getFiles(resFileOrDir) : resFileOrDir;
});

return Array.prototype.concat(...files);
}
12 changes: 0 additions & 12 deletions packages/sveltekit/test/vite/sentrySvelteKitPlugin.test.ts

This file was deleted.

41 changes: 41 additions & 0 deletions packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { vi } from 'vitest';

import { sentryVite } from '../../src/vite/sentryVitePlugins';
import * as sourceMaps from '../../src/vite/sourceMaps';

describe('sentryVite()', () => {
it('returns an array of Vite plugins', () => {
const plugins = sentryVite();
expect(plugins).toBeInstanceOf(Array);
expect(plugins).toHaveLength(1);
});

it('returns the custom sentry source maps plugin by default', () => {
const plugins = sentryVite();
const plugin = plugins[0];
expect(plugin.name).toEqual('sentry-vite-plugin-custom');
});

it("doesn't return the custom sentry source maps plugin if autoUploadSourcemaps is `false`", () => {
const plugins = sentryVite({ autoUploadSourceMaps: false });
expect(plugins).toHaveLength(0);
});

it('passes user-specified vite pugin options to the custom sentry source maps plugin', () => {
const makePluginSpy = vi.spyOn(sourceMaps, 'makeCustomSentryVitePlugin');
const plugins = sentryVite({
debug: true,
sourceMapsUploadOptions: {
include: ['foo.js'],
ignore: ['bar.js'],
},
});
const plugin = plugins[0];
expect(plugin.name).toEqual('sentry-vite-plugin-custom');
expect(makePluginSpy).toHaveBeenCalledWith({
debug: true,
ignore: ['bar.js'],
include: ['foo.js'],
});
});
});
Loading