Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
65 changes: 65 additions & 0 deletions packages/solidstart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ SDK is for [SolidStart](https://start.solidjs.com/). If you're using [Solid](htt
This package is a wrapper around `@sentry/node` for the server and `@sentry/solid` for the client side, with added
functionality related to SolidStart.

## SolidStart version support

The setup differs by SolidStart major, because SolidStart 2 dropped vinxi and `app.config.ts`:

- **SolidStart 1** — configure the SDK with `withSentry` in `app.config.ts`. This is what the "Manual Setup" section
below describes.
- **SolidStart 2** — configure the SDK with the `sentrySolidStart` Vite plugin in `vite.config.ts`. See
[SolidStart 2 setup](#solidstart-2-setup).

## Manual Setup

If the setup through the wizard doesn't work for you, you can also set up the SDK manually.
Expand Down Expand Up @@ -189,6 +198,62 @@ export default defineConfig(
This has a **fundamental restriction**: It only supports limited performance instrumentation. **Only basic http
instrumentation** will work, and no DB or framework-specific instrumentation will be available.

# SolidStart 2 setup

SolidStart 2 has no `app.config.ts`, so `withSentry` does not apply. Configure the SDK with the `sentrySolidStart`
Vite plugin instead. Client-side setup (`Sentry.init` in `entry-client.tsx`) and the Solid Router and
`ErrorBoundary` wrappers below are unchanged.

### 1. Add the Vite plugin

```typescript
// vite.config.ts
import { sentrySolidStart } from '@sentry/solidstart/vite';
import { solidStart } from '@solidjs/start/config';
import { nitro } from 'nitro/vite';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [
solidStart(),
sentrySolidStart({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
// `serverDir` is required for Nitro to pick up the Sentry plugin below.
nitro({ serverDir: './server' }),
],
});
```

### 2. Initialize Sentry on the server

Create a Nitro plugin. Nitro runs it once at server startup, before any request is handled:

```typescript
// server/plugins/sentry.ts
import * as Sentry from '@sentry/solidstart';
import { definePlugin } from 'nitro';

export default definePlugin(() => {
Sentry.init({
dsn: '__PUBLIC_DSN__',
tracesSampleRate: 1.0,
});
});
```

Nitro's `serverDir` defaults to `false`, which disables plugin scanning entirely. If it is not set (step 1), this
file is silently ignored and Sentry never initializes on the server.

Unlike SolidStart 1, there is **no `--import` flag and no instrumentation file to copy** into the build output. The
SDK instruments your server dependencies at build time, so a plain start command is all that is needed:

```bash
node .output/server/index.mjs
```

# Solid Router

The Solid Router instrumentation uses the Solid Router library to create navigation spans to ensure you collect
Expand Down
20 changes: 17 additions & 3 deletions packages/solidstart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
"node": {
"import": "./build/esm/index.server.js",
"require": "./build/cjs/index.server.js"
},
"default": {
"import": "./build/esm/index.server.js",
"require": "./build/cjs/index.server.js"
}
},
"./solidrouter": {
Expand All @@ -50,15 +54,25 @@
"types": "./solidrouter.d.ts",
"import": "./build/esm/solidrouter.server.js",
"require": "./build/cjs/solidrouter.server.js"
},
"default": {
"types": "./solidrouter.d.ts",
"import": "./build/esm/solidrouter.server.js",
"require": "./build/cjs/solidrouter.server.js"
}
},
"./vite": {
"types": "./build/types/vite/index.d.ts",
"import": "./build/esm/vite/index.js",
"require": "./build/cjs/vite/index.js"
}
},
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"@solidjs/router": "^0.13.4 || ^0.14.0 || ^0.15.0",
"@solidjs/start": "^1.0.0"
"@solidjs/router": "^0.13.4 || ^0.14.0 || ^0.15.0 || ^1.0.0",
"@solidjs/start": "^1.0.0 || ^2.0.0"
},
"peerDependenciesMeta": {
"@solidjs/router": {
Expand All @@ -67,11 +81,11 @@
},
"dependencies": {
"@sentry/core": "10.67.0",
"@sentry/nitro": "10.67.0",
"@sentry/node": "10.67.0",
"@sentry/server-utils": "10.67.0",
"@sentry/solid": "10.67.0",
"@sentry/bundler-plugins": "10.67.0",
"@sentry/server-utils": "10.67.0",
"@sentry/conventions": "0.16.0"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/solidstart/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default makeNPMConfigVariants(
'src/solidrouter.server.ts',
'src/client/solidrouter.ts',
'src/server/solidrouter.ts',
'src/vite/index.ts',
],
// prevent this internal code from ending up in our built package (this doesn't happen automatically because
// the name doesn't match an SDK dependency)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@sentry/core';
import { captureException, getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON, startSpan } from '@sentry/node';
import { isRedirect } from './utils';
import { HTTP_ROUTE, HTTP_TARGET } from '@sentry/conventions/attributes';
import { HTTP_ROUTE, HTTP_TARGET, URL_PATH } from '@sentry/conventions/attributes';
import { setHttpServerSpanRouteAttribute } from '@sentry/server-utils';

/**
Expand All @@ -27,8 +27,12 @@ export async function withServerActionInstrumentation<A extends (...args: unknow
// if the target is `/_server`, otherwise we'd overwrite pageloads on routes that use
// server actions (which are more meaningful, e.g. a request to `GET /users/5` is more
// meaningful than overwriting it with `GET doSomeFunctionCall`).
// `@sentry/node` HTTP spans only carry `url.path`; the deprecated `http.target` is kept as a
// fallback for instrumentation that still sets only that.
// oxlint-disable-next-line typescript/no-deprecated
if (spanData && !spanData[HTTP_ROUTE] && spanData[HTTP_TARGET] === '/_server') {
const requestPath = spanData?.[URL_PATH] ?? spanData?.[HTTP_TARGET];

if (spanData && !spanData[HTTP_ROUTE] && requestPath === '/_server') {
setHttpServerSpanRouteAttribute(serverActionName);
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/solidstart/src/vite/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { sentrySolidStart } from './sentrySolidStart';
export type { SentrySolidStartOptions } from './sentrySolidStart';
93 changes: 93 additions & 0 deletions packages/solidstart/src/vite/sentrySolidStart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { SentryVitePluginOptions } from '@sentry/bundler-plugins/vite';
import type { BuildTimeOptionsBase, UnstableVitePluginOptions } from '@sentry/core';
import { setupSentryNitroModule } from '@sentry/nitro';
import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/vite';
import type { Plugin, UserConfig } from 'vite';
import { makeAddSentryVitePluginSolidStart2, makeEnableSourceMapsVitePlugin } from './sourceMaps';

/**
* Build-time options for the Sentry SolidStart SDK on SolidStart 2.
*/
export type SentrySolidStartOptions = BuildTimeOptionsBase & UnstableVitePluginOptions<SentryVitePluginOptions>;

/**
* Vite plugins for the Sentry SolidStart SDK. Requires SolidStart 2.
*
* On SolidStart 1, use `withSentry` in `app.config.ts` instead.
*
* @example
* ```typescript
* // vite.config.ts
* import { sentrySolidStart } from '@sentry/solidstart/vite';
* import { solidStart } from '@solidjs/start/config';
* import { nitro } from 'nitro/vite';
* import { defineConfig } from 'vite';
*
* export default defineConfig({
* plugins: [
* solidStart(),
* sentrySolidStart({
* org: 'your-org',
* project: 'your-project',
* }),
* nitro(),
* ],
* });
* ```
*
* @param options - Options to configure the Sentry Vite plugins
* @returns An array of Vite plugins
*/
export function sentrySolidStart(options: SentrySolidStartOptions = {}): Plugin[] {
const plugins: Plugin[] = [makeSentryNitroPlugin(options)];

// Only the Nitro plugin is dev-safe; its module handles `dev` itself.
if (process.env.NODE_ENV === 'development') {
return plugins;
}

// Injects `diagnostics_channel` publishers into instrumented deps at build time, which is what
// lets `Sentry.init()` run from a bundled Nitro plugin rather than an `--import` preload.
plugins.push(sentryOrchestrionPlugin({ buildTimeInstrumentation: options.buildTimeInstrumentation }));

if (options.sourcemaps?.disable !== true) {
plugins.push(...makeAddSentryVitePluginSolidStart2(options), ...makeEnableSourceMapsVitePlugin(options));
}

return plugins;
}
Comment thread
chargome marked this conversation as resolved.

// Nitro's `NitroConfig` only matches `@sentry/nitro`'s when both resolve the same `nitro` install,
// which is not guaranteed, so the key stays opaque rather than coupling the two copies.
type ViteConfigWithNitro = UserConfig & { nitro?: Record<string, unknown> };

/**
* Delivers everything only Nitro can reach — server source maps, runtime hooks, the
* `sourcemapMinify` opt-out — through Vite's `nitro` key.
*
* `setupSentryNitroModule` is handed only the keys it reads, never the user's whole config, so what
* comes back is purely Sentry's additions. Vite concatenates arrays when merging a `config` return
* value, so echoing the user's own `modules`/`plugins` back would duplicate every entry.
*
* `enforce: 'pre'` is load-bearing: Nitro creates its instance inside its own `config` hook, so a
* normal-priority hook sorting after it would be read too late and silently ignored.
*/
function makeSentryNitroPlugin(options: SentrySolidStartOptions): Plugin {
return {
name: 'sentry-solidstart-nitro',
enforce: 'pre',
config(userConfig: ViteConfigWithNitro) {
const userNitro = userConfig.nitro;

return {
nitro: setupSentryNitroModule(
// `sourcemap` decides whether Sentry enables its own; `tracingChannel` is left as the user set it.
{ sourcemap: userNitro?.sourcemap, tracingChannel: userNitro?.tracingChannel } as Parameters<
typeof setupSentryNitroModule
>[0],
options,
) as Record<string, unknown>,
} as Omit<UserConfig, 'plugins'>;
},
Comment thread
chargome marked this conversation as resolved.
};
}
105 changes: 98 additions & 7 deletions packages/solidstart/src/vite/sourceMaps.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { sentryVitePlugin } from '@sentry/bundler-plugins/vite';
import type { Plugin, UserConfig } from 'vite';
import type { SentrySolidStartOptions } from './sentrySolidStart';
import type { SentrySolidStartPluginOptions } from './types';

// `debug` is all the source map setting logic needs, and all the two majors' option types share.
type SourceMapSettingOptions = { debug?: boolean };

type FilesToDeleteAfterUpload = string | string[] | undefined;

/**
* A Sentry plugin for adding the @sentry/bundler-plugins/vite plugin to automatically upload source maps to Sentry.
*/
Expand Down Expand Up @@ -55,19 +61,106 @@ export function makeAddSentryVitePlugin(options: SentrySolidStartPluginOptions,
}

/**
* A Sentry plugin for SolidStart to enable "hidden" source maps if they are unset.
* SolidStart 2 counterpart of `makeAddSentryVitePlugin`, reading the flat `BuildTimeOptionsBase`
* fields rather than the nested `sourceMapsUploadOptions`.
*
* Covers the Vite-built client assets only; Nitro emits the server bundle outside Vite's output dir
* and the Sentry Nitro module uploads that.
*/
export function makeEnableSourceMapsVitePlugin(options: SentrySolidStartPluginOptions): Plugin[] {
export function makeAddSentryVitePluginSolidStart2(options: SentrySolidStartOptions): Plugin[] {
// Everything not destructured out is field-for-field what `sentryVitePlugin` accepts, so it is
// spread through — a new shared option then reaches the plugin without editing a list here.
const {
authToken,
buildTimeInstrumentation: _buildTimeInstrumentation,
debug,
org,
project,
sentryUrl,
sourcemaps,
telemetry,
unstable_sentryVitePluginOptions,
...passthroughOptions
} = options;

// Deferred because the default depends on whether the user set `build.sourcemap` themselves,
// which is only known once Vite resolves its config. `PromiseLike` because the unstable spelling
// may itself be a promise.
let resolveFilesToDeleteAfterUpload:
| ((value: FilesToDeleteAfterUpload | PromiseLike<FilesToDeleteAfterUpload>) => void)
| undefined;
const filesToDeleteAfterUploadPromise = new Promise<FilesToDeleteAfterUpload>(resolve => {
resolveFilesToDeleteAfterUpload = resolve;
});

const configPlugin: Plugin = {
name: 'sentry-solidstart-files-to-delete-after-upload',
apply: 'build',
enforce: 'post',
config(config) {
// The promise always wins over the passed-in value, so the unstable spelling has to be read
// here too or it is silently replaced by the default below.
const userFilesToDelete =
sourcemaps?.filesToDeleteAfterUpload ?? unstable_sentryVitePluginOptions?.sourcemaps?.filesToDeleteAfterUpload;

// Only clean up source maps we turned on ourselves.
if (typeof userFilesToDelete === 'undefined' && typeof config.build?.sourcemap === 'undefined') {
if (debug) {
// eslint-disable-next-line no-console
console.log(
'[Sentry] Automatically setting `sourcemaps.filesToDeleteAfterUpload: ["./**/*.map"]` to delete generated source maps after they were uploaded to Sentry.',
);
}
resolveFilesToDeleteAfterUpload?.(['./**/*.map']);
} else {
resolveFilesToDeleteAfterUpload?.(userFilesToDelete);
}
},
};

const sentryPlugins = sentryVitePlugin({
...passthroughOptions,
authToken: authToken ?? process.env.SENTRY_AUTH_TOKEN,
debug: debug ?? false,
org: org ?? process.env.SENTRY_ORG,
project: project ?? process.env.SENTRY_PROJECT,
telemetry: telemetry ?? true,
url: sentryUrl,
// Spread here so it overrides the plain options above, but not the objects merged below —
// spreading replaces whole keys rather than deep-merging.
...unstable_sentryVitePluginOptions,
sourcemaps: {
...sourcemaps,
...unstable_sentryVitePluginOptions?.sourcemaps,
filesToDeleteAfterUpload: filesToDeleteAfterUploadPromise,
},
_metaOptions: {
...unstable_sentryVitePluginOptions?._metaOptions,
telemetry: {
...unstable_sentryVitePluginOptions?._metaOptions?.telemetry,
metaFramework: 'solidstart',
},
},
});

return [configPlugin, ...sentryPlugins];
}

/**
* A Sentry plugin for SolidStart to enable "hidden" source maps if they are unset. Used by both
* SolidStart majors.
*/
export function makeEnableSourceMapsVitePlugin(options: SourceMapSettingOptions): Plugin[] {
return [
{
name: 'sentry-solidstart-update-source-map-setting',
apply: 'build',
enforce: 'post',
config(viteConfig) {
// Return only what changed: Vite concatenates arrays when merging a `config` return value,
// so echoing the whole config back would duplicate every array the user had.
return {
...viteConfig,
build: {
...viteConfig.build,
sourcemap: getUpdatedSourceMapSettings(viteConfig, options),
},
};
Expand All @@ -93,10 +186,8 @@ export function makeEnableSourceMapsVitePlugin(options: SentrySolidStartPluginOp
*/
export function getUpdatedSourceMapSettings(
viteConfig: UserConfig,
sentryPluginOptions?: SentrySolidStartPluginOptions,
sentryPluginOptions?: SourceMapSettingOptions,
): boolean | 'inline' | 'hidden' {
viteConfig.build = viteConfig.build || {};

const viteSourceMap = viteConfig?.build?.sourcemap;
let updatedSourceMapSetting = viteSourceMap;

Expand Down
Loading
Loading