From 3c437d18f0184b103baab76aefab0cacfa896bc7 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 27 Aug 2024 15:56:45 +0200 Subject: [PATCH] feat(solidstart): Add `sentrySolidStartVite` plugin --- packages/solidstart/.eslintrc.js | 1 + packages/solidstart/README.md | 54 ++++------- packages/solidstart/package.json | 2 +- packages/solidstart/src/index.server.ts | 1 + packages/solidstart/src/index.types.ts | 1 + packages/solidstart/src/vite/index.ts | 1 + .../src/vite/sentrySolidStartVite.ts | 18 ++++ packages/solidstart/src/vite/sourceMaps.ts | 56 +++++++++++ packages/solidstart/src/vite/types.ts | 94 +++++++++++++++++++ .../withServerActionInstrumentation.test.ts | 2 +- .../test/vite/sentrySolidStartVite.test.ts | 53 +++++++++++ .../solidstart/test/vite/sourceMaps.test.ts | 58 ++++++++++++ 12 files changed, 301 insertions(+), 40 deletions(-) create mode 100644 packages/solidstart/src/vite/index.ts create mode 100644 packages/solidstart/src/vite/sentrySolidStartVite.ts create mode 100644 packages/solidstart/src/vite/sourceMaps.ts create mode 100644 packages/solidstart/src/vite/types.ts create mode 100644 packages/solidstart/test/vite/sentrySolidStartVite.test.ts create mode 100644 packages/solidstart/test/vite/sourceMaps.test.ts diff --git a/packages/solidstart/.eslintrc.js b/packages/solidstart/.eslintrc.js index c1f55c94aadf..d567b12530d0 100644 --- a/packages/solidstart/.eslintrc.js +++ b/packages/solidstart/.eslintrc.js @@ -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', }, }, ], diff --git a/packages/solidstart/README.md b/packages/solidstart/README.md index b5d775781875..d12567b01f2f 100644 --- a/packages/solidstart/README.md +++ b/packages/solidstart/README.md @@ -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= -SENTRY_ORG= -SENTRY_PROJECT= -``` - -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, }), ], }, + // ... }); ``` diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index 3251549a62b5..87be083c08f2 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -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", diff --git a/packages/solidstart/src/index.server.ts b/packages/solidstart/src/index.server.ts index 0ce5251aa327..d675a1c72820 100644 --- a/packages/solidstart/src/index.server.ts +++ b/packages/solidstart/src/index.server.ts @@ -1 +1,2 @@ export * from './server'; +export * from './vite'; diff --git a/packages/solidstart/src/index.types.ts b/packages/solidstart/src/index.types.ts index 89eaa14662e3..51adf848775a 100644 --- a/packages/solidstart/src/index.types.ts +++ b/packages/solidstart/src/index.types.ts @@ -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'; diff --git a/packages/solidstart/src/vite/index.ts b/packages/solidstart/src/vite/index.ts new file mode 100644 index 000000000000..464bbd604fbe --- /dev/null +++ b/packages/solidstart/src/vite/index.ts @@ -0,0 +1 @@ +export * from './sentrySolidStartVite'; diff --git a/packages/solidstart/src/vite/sentrySolidStartVite.ts b/packages/solidstart/src/vite/sentrySolidStartVite.ts new file mode 100644 index 000000000000..28c1125a47b2 --- /dev/null +++ b/packages/solidstart/src/vite/sentrySolidStartVite.ts @@ -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; +}; diff --git a/packages/solidstart/src/vite/sourceMaps.ts b/packages/solidstart/src/vite/sourceMaps.ts new file mode 100644 index 000000000000..2133edbfa9dd --- /dev/null +++ b/packages/solidstart/src/vite/sourceMaps.ts @@ -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, + }), + ]; +} diff --git a/packages/solidstart/src/vite/types.ts b/packages/solidstart/src/vite/types.ts new file mode 100644 index 000000000000..0a1c6dc81acb --- /dev/null +++ b/packages/solidstart/src/vite/types.ts @@ -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; + + /** + * 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; + + /** + * 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; + }; + + /** + * 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; +}; diff --git a/packages/solidstart/test/server/withServerActionInstrumentation.test.ts b/packages/solidstart/test/server/withServerActionInstrumentation.test.ts index 9a5b1e0c2b51..6bc70d30ae07 100644 --- a/packages/solidstart/test/server/withServerActionInstrumentation.test.ts +++ b/packages/solidstart/test/server/withServerActionInstrumentation.test.ts @@ -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); diff --git a/packages/solidstart/test/vite/sentrySolidStartVite.test.ts b/packages/solidstart/test/vite/sentrySolidStartVite.test.ts new file mode 100644 index 000000000000..76b81fff3edd --- /dev/null +++ b/packages/solidstart/test/vite/sentrySolidStartVite.test.ts @@ -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[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; + }); +}); diff --git a/packages/solidstart/test/vite/sourceMaps.test.ts b/packages/solidstart/test/vite/sourceMaps.test.ts new file mode 100644 index 000000000000..7aea5bb24978 --- /dev/null +++ b/packages/solidstart/test/vite/sourceMaps.test.ts @@ -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'], + }, + }); + }); +});