Skip to content

Commit

Permalink
feat(sveltekit): Add Sentry Vite Plugin to upload source maps (#7811)
Browse files Browse the repository at this point in the history
Add a customized version of the Sentry Vite plugin to the SvelteKit SDK.

Rework the SDK's build API:

* Instead of having a super plugin that adds our other plugins (this doesn't work), we now export a factory function that returns an array of plugins. [SvelteKit also creates its plugins](https://github.com/Lms24/kit/blob/f7de9556319f652cabb89dd6f17b21e25326759c/packages/kit/src/exports/vite/index.js#L114-L143) this way.
* The currently only plugin in this array is the customized Vite plugin.

The customized Vite plugin differs from the Vite plugin as follows:

* It only runs on builds (not on the dev server)
* It tries to run as late as possible by setting `enforce: 'post'`
* It uses the `closeBundle` hook instead of the `writeBundle` hook to upload source maps.
  * This is because the SvelteKit adapters also run only at closeBundle but luckily before our plugin
* It uses the `configure` hook to enable source map generation  
* It flattens source maps before uploading them
  * We to flatten them (actually [`sorcery`](https://github.com/Rich-Harris/sorcery) does) to work around [weird source maps generation behaviour](sveltejs/kit#9608).
  • Loading branch information
Lms24 authored Apr 12, 2023
1 parent 84d007e commit 0e20bb5
Show file tree
Hide file tree
Showing 10 changed files with 470 additions and 71 deletions.
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 `sentrySvelteKit` 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 { sentrySvelteKit } from '@sentry/sveltekit';

export default {
plugins: [sentrySvelteKitPlugin(), sveltekit()],
plugins: [sentrySvelteKit(), sveltekit()],
// ... 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"
},
"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 { sentrySvelteKit } 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 sentrySvelteKit(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 { sentrySvelteKit } from '../../src/vite/sentryVitePlugins';
import * as sourceMaps from '../../src/vite/sourceMaps';

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

it('returns the custom sentry source maps plugin by default', () => {
const plugins = sentrySvelteKit();
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 = sentrySvelteKit({ 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 = sentrySvelteKit({
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

0 comments on commit 0e20bb5

Please sign in to comment.