Skip to content

Commit

Permalink
feat(solidstart): Add sentrySolidStartVite plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
andreiborza committed Aug 28, 2024
1 parent 5b9d3bb commit 3c437d1
Show file tree
Hide file tree
Showing 12 changed files with 301 additions and 40 deletions.
1 change: 1 addition & 0 deletions packages/solidstart/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
files: ['src/vite/**', 'src/server/**'],
rules: {
'@sentry-internal/sdk/no-optional-chaining': 'off',
'@sentry-internal/sdk/no-nullish-coalescing': 'off',
},
},
],
Expand Down
54 changes: 16 additions & 38 deletions packages/solidstart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,58 +157,36 @@ render(
);
```

# Sourcemaps and Releases
## Uploading Source Maps

To generate and upload source maps of your Solid Start app use our Vite bundler plugin.

1. Install the Sentry Vite plugin

```bash
# Using npm
npm install @sentry/vite-plugin --save-dev

# Using yarn
yarn add @sentry/vite-plugin --dev
```

2. Configure the vite plugin

To upload source maps you have to configure an auth token. Auth tokens can be passed to the plugin explicitly with the
`authToken` option, with a `SENTRY_AUTH_TOKEN` environment variable, or with an `.env.sentry-build-plugin` file in the
working directory when building your project. We recommend you add the auth token to your CI/CD environment as an
environment variable.
To upload source maps, add the `sentrySolidStartVite` plugin from `@sentry/solidstart` to your `app.config.ts` and configure
an auth token. Auth tokens can be passed to the plugin explicitly with the `authToken` option, with a
`SENTRY_AUTH_TOKEN` environment variable, or with an `.env.sentry-build-plugin` file in the working directory when
building your project. We recommend you add the auth token to your CI/CD environment as an environment variable.

Learn more about configuring the plugin in our
[Sentry Vite Plugin documentation](https://www.npmjs.com/package/@sentry/vite-plugin).

```bash
// .env.sentry-build-plugin
SENTRY_AUTH_TOKEN=<your auth token>
SENTRY_ORG=<your org>
SENTRY_PROJECT=<your project name>
```

3. Finally, add the plugin to your `app.config.ts` file.

```javascript
```typescript
// app.config.ts
import { defineConfig } from '@solidjs/start/config';
import { sentryVitePlugin } from '@sentry/vite-plugin';
import { sentrySolidStartVite } from '@sentry/solidstart';

export default defineConfig({
// rest of your config
// ...

vite: {
build: {
sourcemap: true,
},
plugins: [
sentryVitePlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
sentrySolidStartVite({
sourceMapsUploadOptions: {
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
},
debug: true,
}),
],
},
// ...
});
```
2 changes: 1 addition & 1 deletion packages/solidstart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"@sentry/solid": "8.26.0",
"@sentry/types": "8.26.0",
"@sentry/utils": "8.26.0",
"@sentry/vite-plugin": "2.19.0"
"@sentry/vite-plugin": "2.22.2"
},
"devDependencies": {
"@solidjs/router": "^0.13.4",
Expand Down
1 change: 1 addition & 0 deletions packages/solidstart/src/index.server.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './server';
export * from './vite';
1 change: 1 addition & 0 deletions packages/solidstart/src/index.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// exports in this file - which we do below.
export * from './client';
export * from './server';
export * from './vite';

import type { Integration, Options, StackParser } from '@sentry/types';

Expand Down
1 change: 1 addition & 0 deletions packages/solidstart/src/vite/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './sentrySolidStartVite';
18 changes: 18 additions & 0 deletions packages/solidstart/src/vite/sentrySolidStartVite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Plugin } from 'vite';
import { makeSourceMapsVitePlugin } from './sourceMaps';
import type { SentrySolidStartPluginOptions } from './types';

/**
* Various Sentry vite plugins to be used for SolidStart.
*/
export const sentrySolidStartVite = (options: SentrySolidStartPluginOptions): Plugin[] => {
const sentryPlugins: Plugin[] = [];

if (process.env.NODE_ENV !== 'development') {
if (options.sourceMapsUploadOptions?.enabled ?? true) {
sentryPlugins.push(...makeSourceMapsVitePlugin({ ...options.sourceMapsUploadOptions, debug: options.debug }));
}
}

return sentryPlugins;
};
56 changes: 56 additions & 0 deletions packages/solidstart/src/vite/sourceMaps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { sentryVitePlugin } from '@sentry/vite-plugin';
import type { Plugin } from 'vite';
import type { SourceMapsOptions } from './types';

/**
* A Sentry plugin for SolidStart to enable source maps and use
* @sentry/vite-plugin to automatically upload source maps to Sentry.
* @param {SourceMapsOptions} options
*/
export function makeSourceMapsVitePlugin(options: SourceMapsOptions): Plugin[] {
return [
{
name: 'sentry-solidstart-source-maps',
apply: 'build',
enforce: 'post',
config(config) {
const sourceMapsPreviouslyNotEnabled = !config.build?.sourcemap;
if (options.debug && sourceMapsPreviouslyNotEnabled) {
// eslint-disable-next-line no-console
console.log('[Sentry SolidStart Plugin] Enabling source map generation');
if (!options.sourcemaps?.filesToDeleteAfterUpload) {
// eslint-disable-next-line no-console
console.warn(
`[Sentry SolidStart PLugin] We recommend setting the \`sourceMapsUploadOptions.sourcemaps.filesToDeleteAfterUpload\` option to clean up source maps after uploading.
[Sentry SolidStart Plugin] Otherwise, source maps might be deployed to production, depending on your configuration`,
);
}
}
return {
...config,
build: {
...config.build,
sourcemap: true,
},
};
},
},
...sentryVitePlugin({
org: options.org ?? process.env.SENTRY_ORG,
project: options.project ?? process.env.SENTRY_PROJECT,
authToken: options.authToken ?? process.env.SENTRY_AUTH_TOKEN,
telemetry: options.telemetry ?? true,
sourcemaps: {
assets: options.sourcemaps?.assets ?? undefined,
ignore: options.sourcemaps?.ignore ?? undefined,
filesToDeleteAfterUpload: options.sourcemaps?.filesToDeleteAfterUpload ?? undefined,
},
_metaOptions: {
telemetry: {
metaFramework: 'solidstart',
},
},
debug: options.debug ?? false,
}),
];
}
94 changes: 94 additions & 0 deletions packages/solidstart/src/vite/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
export type SourceMapsOptions = {
/**
* If this flag is `true`, and an auth token is detected, the Sentry SDK will
* automatically generate and upload source maps to Sentry during a production build.
*
* @default true
*/
enabled?: boolean;

/**
* The auth token to use when uploading source maps to Sentry.
*
* Instead of specifying this option, you can also set the `SENTRY_AUTH_TOKEN` environment variable.
*
* To create an auth token, follow this guide:
* @see https://docs.sentry.io/product/accounts/auth-tokens/#organization-auth-tokens
*/
authToken?: string;

/**
* The organization slug of your Sentry organization.
* Instead of specifying this option, you can also set the `SENTRY_ORG` environment variable.
*/
org?: string;

/**
* The project slug of your Sentry project.
* Instead of specifying this option, you can also set the `SENTRY_PROJECT` environment variable.
*/
project?: string;

/**
* If this flag is `true`, the Sentry plugin will collect some telemetry data and send it to Sentry.
* It will not collect any sensitive or user-specific data.
*
* @default true
*/
telemetry?: boolean;

/**
* Options related to sourcemaps
*/
sourcemaps?: {
/**
* A glob or an array of globs that specify the build artifacts and source maps that will be uploaded to Sentry.
*
* The globbing patterns must follow the implementation of the `glob` package.
* @see https://www.npmjs.com/package/glob#glob-primer
*/
assets?: string | Array<string>;

/**
* A glob or an array of globs that specifies which build artifacts should not be uploaded to Sentry.
*
* @default [] - By default no files are ignored. Thus, all files matching the `assets` glob
* or the default value for `assets` are uploaded.
*
* The globbing patterns follow the implementation of the glob package. (https://www.npmjs.com/package/glob)
*/
ignore?: string | Array<string>;

/**
* A glob or an array of globs that specifies the build artifacts that should be deleted after the artifact
* upload to Sentry has been completed.
*
* @default [] - By default no files are deleted.
*
* The globbing patterns follow the implementation of the glob package. (https://www.npmjs.com/package/glob)
*/
filesToDeleteAfterUpload?: string | Array<string>;
};

/**
* Enable debug functionality of the SDK during build-time.
* Enabling this will give you logs about source maps.
*/
debug?: boolean;
};

/**
* Build options for the Sentry module. These options are used during build-time by the Sentry SDK.
*/
export type SentrySolidStartPluginOptions = {
/**
* Options for the Sentry Vite plugin to customize the source maps upload process.
*/
sourceMapsUploadOptions?: SourceMapsOptions;

/**
* Enable debug functionality of the SDK during build-time.
* Enabling this will give you, for example logs about source maps.
*/
debug?: boolean;
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
spanToJSON,
} from '@sentry/node';
import { NodeClient } from '@sentry/node';
import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter';
import { redirect } from '@solidjs/router';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { solidRouterBrowserTracingIntegration } from '../../src/client/solidrouter';

const mockCaptureException = vi.spyOn(SentryNode, 'captureException').mockImplementation(() => '');
const mockFlush = vi.spyOn(SentryNode, 'flush').mockImplementation(async () => true);
Expand Down
53 changes: 53 additions & 0 deletions packages/solidstart/test/vite/sentrySolidStartVite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Plugin } from 'vite';
import { describe, expect, it, vi } from 'vitest';
import { sentrySolidStartVite } from '../../src/vite/sentrySolidStartVite';

vi.spyOn(console, 'log').mockImplementation(() => {
/* noop */
});
vi.spyOn(console, 'warn').mockImplementation(() => {
/* noop */
});

function getSentrySolidStartVitePlugins(options?: Parameters<typeof sentrySolidStartVite>[0]): Plugin[] {
return sentrySolidStartVite({
sourceMapsUploadOptions: {
authToken: 'token',
org: 'org',
project: 'project',
...options?.sourceMapsUploadOptions,
},
...options,
});
}

describe('sentrySolidStartVite()', () => {
it('returns an array of vite plugins', () => {
const plugins = getSentrySolidStartVitePlugins();
const names = plugins.map(plugin => plugin.name);
expect(names).toEqual([
'sentry-solidstart-source-maps',
'sentry-telemetry-plugin',
'sentry-vite-release-injection-plugin',
'sentry-debug-id-upload-plugin',
'sentry-vite-debug-id-injection-plugin',
'sentry-file-deletion-plugin',
'sentry-vite-debug-id-upload-plugin',
]);
});

it("returns an empty array if source maps upload isn't enabled", () => {
const plugins = getSentrySolidStartVitePlugins({ sourceMapsUploadOptions: { enabled: false } });
expect(plugins).toHaveLength(0);
});

it('returns an empty array if `NODE_ENV` is development', async () => {
const previousEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';

const plugins = getSentrySolidStartVitePlugins({ sourceMapsUploadOptions: { enabled: true } });
expect(plugins).toHaveLength(0);

process.env.NODE_ENV = previousEnv;
});
});
58 changes: 58 additions & 0 deletions packages/solidstart/test/vite/sourceMaps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { makeSourceMapsVitePlugin } from '../../src/vite/sourceMaps';
import * as sourceMaps from '../../src/vite/sourceMaps';

const mockedSentryVitePlugin = {
name: 'sentry-vite-debug-id-upload-plugin',
writeBundle: vi.fn(),
};

vi.mock('@sentry/vite-plugin', async () => {
const original = (await vi.importActual('@sentry/vite-plugin')) as any;

return {
...original,
sentryVitePlugin: () => [mockedSentryVitePlugin],
};
});

beforeEach(() => {
vi.clearAllMocks();
});

describe('makeSourceMapsVitePlugin()', () => {
it('returns a plugin to set `sourcemaps` to `true`', async () => {
const [sourceMapsConfigPlugin, sentryVitePlugin] = makeSourceMapsVitePlugin({});

expect(sourceMapsConfigPlugin?.name).toEqual('sentry-solidstart-source-maps');
expect(sourceMapsConfigPlugin?.apply).toEqual('build');
expect(sourceMapsConfigPlugin?.enforce).toEqual('post');
expect(sourceMapsConfigPlugin?.config).toEqual(expect.any(Function));

expect(sentryVitePlugin).toEqual(mockedSentryVitePlugin);
});

it('passes user-specified vite plugin options to vite plugin plugin', async () => {
const makePluginSpy = vi.spyOn(sourceMaps, 'makeSourceMapsVitePlugin');

makeSourceMapsVitePlugin({
org: 'my-org',
authToken: 'my-token',
sourcemaps: {
assets: ['foo/*.js'],
ignore: ['bar/*.js'],
filesToDeleteAfterUpload: ['baz/*.js'],
},
});

expect(makePluginSpy).toHaveBeenCalledWith({
org: 'my-org',
authToken: 'my-token',
sourcemaps: {
assets: ['foo/*.js'],
ignore: ['bar/*.js'],
filesToDeleteAfterUpload: ['baz/*.js'],
},
});
});
});

0 comments on commit 3c437d1

Please sign in to comment.