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(solidstart): Add withSentry config wrapper #13784

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
27 changes: 21 additions & 6 deletions dev-packages/e2e-tests/test-applications/solidstart/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import { sentrySolidStartVite } from '@sentry/solidstart';
import { withSentry } from '@sentry/solidstart';
import { defineConfig } from '@solidjs/start/config';

export default defineConfig({
vite: {
plugins: [sentrySolidStartVite()],
},
});
export default defineConfig(
withSentry(
{},
{
// Typically we want to default to ./src/instrument.sever.ts
// `withSentry` would then build and copy the file over to
// the .output folder, but since we can't use the production
// server for our e2e tests, we have to delete the build folders
// prior to using the dev server for our tests. Which also gets
// rid of the instrument.server.mjs file that we need to --import.
// Therefore, we specify the .mjs file here and to ensure
// `withSentry` gets its file to build and we continue to reference
// the file from the `src` folder for --import without needing to
// transpile before.
// This can be removed once we get the production server to work
// with our e2e tests.
instrumentation: './src/instrument.server.mjs',
},
),
);
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ test('captures an exception', async ({ page }) => {
});

await page.goto('/error-boundary');
// The first page load causes a hydration error on the dev server sometimes - a reload works around this
await page.reload();
await page.locator('#caughtErrorBtn').click();
const errorEvent = await errorEventPromise;

Expand Down
2 changes: 1 addition & 1 deletion packages/solidstart/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module.exports = {
},
},
{
files: ['src/vite/**', 'src/server/**'],
files: ['src/vite/**', 'src/server/**', 'src/config/**'],
rules: {
'@sentry-internal/sdk/no-optional-chaining': 'off',
'@sentry-internal/sdk/no-nullish-coalescing': 'off',
Expand Down
120 changes: 83 additions & 37 deletions packages/solidstart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ mount(() => <StartClient />, document.getElementById('app'));

### 3. Server-side Setup

Create an instrument file named `instrument.server.mjs` and add your initialization code for the server-side SDK.
Create an instrument file named `src/instrument.server.ts` and add your initialization code for the server-side SDK.

```javascript
import * as Sentry from '@sentry/solidstart';
Expand Down Expand Up @@ -101,16 +101,94 @@ export default defineConfig({
The Sentry middleware enhances the data collected by Sentry on the server side by enabling distributed tracing between
the client and server.

### 5. Run your application
### 5. Configure your application

For Sentry to work properly, SolidStart's `app.config.ts` has to be modified. Wrap your config with `withSentry` and
configure it to upload source maps.

If your `instrument.server.ts` file is not located in the `src` folder, you can specify the path via the
`instrumentation` option to `withSentry`.

To upload source maps, configure an auth token. Auth tokens can be passed 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 adding 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).

```typescript
import { defineConfig } from '@solidjs/start/config';
import { withSentry } from '@sentry/solidstart';

export default defineConfig(
withSentry(
{
// SolidStart config
middleware: './src/middleware.ts',
},
{
// Sentry `withSentry` options
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
debug: true,
// optional: if your `instrument.server.ts` file is not located inside `src`
instrumentation: './mypath/instrument.server.ts',
},
),
);
```

### 6. Run your application

Then run your app

```bash
NODE_OPTIONS='--import=./instrument.server.mjs' yarn start
# or
NODE_OPTIONS='--require=./instrument.server.js' yarn start
NODE_OPTIONS='--import=./.output/server/instrument.server.mjs' yarn start
```

⚠️ **Note build presets** ⚠️
Depending on [build preset](https://nitro.unjs.io/deploy), the location of `instrument.server.mjs` differs. To find out
where `instrument.server.mjs` is located, monitor the build log output for

```bash
[Sentry SolidStart withSentry] Successfully created /my/project/path/.output/server/instrument.server.mjs.
```

⚠️ **Note for platforms without the ability to modify `NODE_OPTIONS` or use `--import`** ⚠️
Depending on where the application is deployed to, it might not be possible to modify or use `NODE_OPTIONS` to import
`instrument.server.mjs`.

For such platforms, we offer the `experimental_basicServerTracing` flag to add a top level import of
`instrument.server.mjs` to the server entry file.

```typescript
import { defineConfig } from '@solidjs/start/config';
import { withSentry } from '@sentry/solidstart';

export default defineConfig(
withSentry(
{
// ...
middleware: './src/middleware.ts',
},
{
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
debug: true,
// optional: if your `instrument.server.ts` file is not located inside `src`
instrumentation: './mypath/instrument.server.ts',
// optional: if NODE_OPTIONS or --import is not avaiable
experimental_basicServerTracing: true,
},
),
);
```

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.

# Solid Router

The Solid Router instrumentation uses the Solid Router library to create navigation spans to ensure you collect
Expand Down Expand Up @@ -156,35 +234,3 @@ render(
document.getElementById('root'),
);
```

## Uploading Source Maps

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).

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

export default defineConfig({
// ...

vite: {
plugins: [
sentrySolidStartVite({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
debug: true,
}),
],
},
// ...
});
```
95 changes: 95 additions & 0 deletions packages/solidstart/src/config/addInstrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as fs from 'fs';
import * as path from 'path';
import { consoleSandbox } from '@sentry/utils';
import type { Nitro } from './types';

// Nitro presets for hosts that only host static files
export const staticHostPresets = ['github_pages'];
// Nitro presets for hosts that use `server.mjs` as opposed to `index.mjs`
export const serverFilePresets = ['netlify'];

/**
* Adds the built `instrument.server.js` file to the output directory.
*
* This will no-op if no `instrument.server.js` file was found in the
* build directory. Make sure the `sentrySolidStartVite` plugin was
* added to `app.config.ts` to enable building the instrumentation file.
*/
export async function addInstrumentationFileToBuild(nitro: Nitro): Promise<void> {
// Static file hosts have no server component so there's nothing to do
if (staticHostPresets.includes(nitro.options.preset)) {
return;
}

const buildDir = nitro.options.buildDir;
const serverDir = nitro.options.output.serverDir;
const source = path.resolve(buildDir, 'build', 'ssr', 'instrument.server.js');
const destination = path.resolve(serverDir, 'instrument.server.mjs');

try {
await fs.promises.copyFile(source, destination);

consoleSandbox(() => {
// eslint-disable-next-line no-console
console.log(`[Sentry SolidStart withSentry] Successfully created ${destination}.`);
});
} catch (error) {
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.warn(`[Sentry SolidStart withSentry] Failed to create ${destination}.`, error);
});
}
}

/**
* Adds an `instrument.server.mjs` import to the top of the server entry file.
*
* This is meant as an escape hatch and should only be used in environments where
* it's not possible to `--import` the file instead as it comes with a limited
* tracing experience, only collecting http traces.
*/
export async function experimental_addInstrumentationFileTopLevelImportToServerEntry(
serverDir: string,
preset: string,
): Promise<void> {
// Static file hosts have no server component so there's nothing to do
if (staticHostPresets.includes(preset)) {
return;
}

const instrumentationFile = path.resolve(serverDir, 'instrument.server.mjs');
const serverEntryFileName = serverFilePresets.includes(preset) ? 'server.mjs' : 'index.mjs';
const serverEntryFile = path.resolve(serverDir, serverEntryFileName);

try {
await fs.promises.access(instrumentationFile, fs.constants.F_OK);
} catch (error) {
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.warn(
`[Sentry SolidStart withSentry] Failed to add \`${instrumentationFile}\` as top level import to \`${serverEntryFile}\`.`,
error,
);
});
return;
}

try {
const content = await fs.promises.readFile(serverEntryFile, 'utf-8');
const updatedContent = `import './instrument.server.mjs';\n${content}`;
await fs.promises.writeFile(serverEntryFile, updatedContent);

consoleSandbox(() => {
// eslint-disable-next-line no-console
console.log(
`[Sentry SolidStart withSentry] Added \`${instrumentationFile}\` as top level import to \`${serverEntryFile}\`.`,
);
});
} catch (error) {
// eslint-disable-next-line no-console
console.warn(
`[Sentry SolidStart withSentry] An error occurred when trying to add \`${instrumentationFile}\` as top level import to \`${serverEntryFile}\`.`,
error,
);
}
}
2 changes: 2 additions & 0 deletions packages/solidstart/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './withSentry';
export type { Nitro, SentrySolidStartConfigOptions } from './types';
35 changes: 35 additions & 0 deletions packages/solidstart/src/config/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { defineConfig } from '@solidjs/start/config';
// Types to avoid pulling in extra dependencies
// These are non-exhaustive
export type Nitro = {
options: {
buildDir: string;
output: {
serverDir: string;
};
preset: string;
};
};

export type SolidStartInlineConfig = Parameters<typeof defineConfig>[0];

export type SolidStartInlineServerConfig = {
hooks?: {
close?: () => unknown;
'rollup:before'?: (nitro: Nitro) => unknown;
};
};

export type SentrySolidStartConfigOptions = {
/**
* Enabling basic server tracing can be used for environments where modifying the node option `--import` is not possible.
* However, enabling this option only supports limited tracing instrumentation. Only http traces will be collected (but no database-specific traces etc.).
*
* If this option is `true`, the Sentry SDK will import the instrumentation.server.ts|js file at the top of the server entry file to load the SDK on the server.
*
* **DO NOT** enable this option if you've already added the node option `--import` in your node start script. This would initialize Sentry twice on the server-side and leads to unexpected issues.
*
* @default false
*/
experimental_basicServerTracing?: boolean;
};
63 changes: 63 additions & 0 deletions packages/solidstart/src/config/withSentry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { addSentryPluginToVite } from '../vite';
import type { SentrySolidStartPluginOptions } from '../vite/types';
import {
addInstrumentationFileToBuild,
experimental_addInstrumentationFileTopLevelImportToServerEntry,
} from './addInstrumentation';
import type { Nitro, SolidStartInlineConfig, SolidStartInlineServerConfig } from './types';

/**
* Modifies the passed in Solid Start configuration with build-time enhancements such as
* building the `instrument.server.ts` file into the appropriate build folder based on
* build preset.
*
* @param solidStartConfig A Solid Start configuration object, as usually passed to `defineConfig` in `app.config.ts|js`
* @param sentrySolidStartPluginOptions Options to configure the plugin
* @returns The modified config to be exported and passed back into `defineConfig`
*/
export const withSentry = (
solidStartConfig: SolidStartInlineConfig = {},
sentrySolidStartPluginOptions: SentrySolidStartPluginOptions = {},
): SolidStartInlineConfig => {
const server = (solidStartConfig.server || {}) as SolidStartInlineServerConfig;
const hooks = server.hooks || {};
const vite =
typeof solidStartConfig.vite === 'function'
? (...args: unknown[]) => addSentryPluginToVite(solidStartConfig.vite(...args), sentrySolidStartPluginOptions)
: addSentryPluginToVite(solidStartConfig.vite, sentrySolidStartPluginOptions);

let serverDir: string;
let buildPreset: string;

return {
...solidStartConfig,
vite,
server: {
...server,
hooks: {
...hooks,
async close() {
if (sentrySolidStartPluginOptions.experimental_basicServerTracing) {
await experimental_addInstrumentationFileTopLevelImportToServerEntry(serverDir, buildPreset);
}

// Run user provided hook
if (hooks.close) {
hooks.close();
}
},
async 'rollup:before'(nitro: Nitro) {
serverDir = nitro.options.output.serverDir;
buildPreset = nitro.options.preset;

await addInstrumentationFileToBuild(nitro);

// Run user provided hook
if (hooks['rollup:before']) {
hooks['rollup:before'](nitro);
}
},
},
},
};
};
1 change: 1 addition & 0 deletions packages/solidstart/src/index.server.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './server';
export * from './vite';
export * from './config';
Loading
Loading