Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,12 @@ import { defineConfig } from '@solidjs/start/config';
export default defineConfig(
withSentry({
middleware: './src/middleware.ts',
// The base-path variant serves the app, and so the tunnel, under `/app`.
...(process.env.E2E_TEST_BASE_PATH && { server: { baseURL: `${process.env.E2E_TEST_BASE_PATH}/` } }),
vite: {
define: {
__E2E_TEST_TUNNEL__: JSON.stringify(!!process.env.E2E_TEST_TUNNEL),
},
},
}),
);
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ Sentry.init({
dsn: process.env.E2E_TEST_DSN,
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0, // Capture 100% of the transactions
tunnel: 'http://localhost:3031/', // proxy server
tunnel: process.env.E2E_TEST_TUNNEL ? true : 'http://localhost:3031/', // proxy server
debug: !!process.env.DEBUG,
});
23 changes: 20 additions & 3 deletions dev-packages/e2e-tests/test-applications/solidstart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
"test:prod": "TEST_ENV=production playwright test",
"test:build": "pnpm install && pnpm build",
"test:assert": "pnpm test:prod && pnpm test:dev",
"test:dev": "TEST_ENV=development playwright test db"
"test:dev": "TEST_ENV=development playwright test db",
"test:build:tunnel-basepath": "pnpm install && E2E_TEST_TUNNEL=1 E2E_TEST_BASE_PATH=/app pnpm build",
"test:assert:tunnel-basepath": "E2E_TEST_TUNNEL=1 E2E_TEST_BASE_PATH=/app E2E_TEST_DSN=http://public@localhost:3031/1337 TEST_ENV=production playwright test tunnel",
"test:assert:tunnel-basepath-dev": "E2E_TEST_TUNNEL=1 E2E_TEST_BASE_PATH=/app E2E_TEST_DSN=http://public@localhost:3031/1337 TEST_ENV=development playwright test tunnel"
},
"type": "module",
"dependencies": {
Expand All @@ -30,11 +33,25 @@
"jsdom": "^24.0.0",
"solid-js": "1.9.5",
"typescript": "^5.4.5",
"vinxi": "^0.4.0",
"vite": "^5.4.11",
"vinxi": "^0.5.11",
"vite": "^6.4.4",
"vite-plugin-solid": "^2.11.6"
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"variants": [
{
"build-command": "pnpm test:build:tunnel-basepath",
"assert-command": "pnpm test:assert:tunnel-basepath",
"label": "solidstart (tunnel-basepath)"
},
{
"build-command": "pnpm install",
"assert-command": "pnpm test:assert:tunnel-basepath-dev",
"label": "solidstart (tunnel-basepath-dev)"
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import * as Sentry from '@sentry/solidstart';
import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter';
import { StartClient, mount } from '@solidjs/start/client';

declare const __E2E_TEST_TUNNEL__: boolean;

Sentry.init({
// We can't use env variables here, seems like they are stripped
// out in production builds.
dsn: 'https://public@dsn.ingest.sentry.io/1337',
dsn: __E2E_TEST_TUNNEL__ ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337',
environment: 'qa', // dynamic sampling bias to keep transactions
integrations: [solidRouterBrowserTracingIntegration()],
tunnel: 'http://localhost:3031/', // proxy server
tunnel: __E2E_TEST_TUNNEL__ ? true : 'http://localhost:3031/', // proxy server
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
debug: !!import.meta.env.DEBUG,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ Sentry.init({
dsn: process.env.E2E_TEST_DSN,
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0, // Capture 100% of the transactions
tunnel: 'http://localhost:3031/', // proxy server
tunnel: process.env.E2E_TEST_TUNNEL ? true : 'http://localhost:3031/', // proxy server
debug: !!process.env.DEBUG,
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,14 @@ import { sentryBeforeResponseMiddleware } from '@sentry/solidstart';
import { createMiddleware } from '@solidjs/start/middleware';

export default createMiddleware({
onRequest: [
// The tunnel variant checks that the managed tunnel answers before an auth middleware like this one.
event => {
const request = event.request;
if (process.env.E2E_TEST_TUNNEL && request.method === 'POST' && request.headers.get('x-auth') !== 'ok') {
return new Response('blocked by user auth', { status: 401 });
}
},
],
onBeforeResponse: [sentryBeforeResponseMiddleware()],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect, test } from '@playwright/test';
import { isServerSpanAt, watchForTunnelSpan } from '@sentry-internal/test-utils';

test.skip(!process.env.E2E_TEST_TUNNEL, 'Only runs in the tunnel variant');

const APP_NAME = 'solidstart';
const BASE_PATH = process.env.E2E_TEST_BASE_PATH ?? '';
const TUNNEL_PATH = new RegExp(`^${BASE_PATH}/[a-z0-9]{1,7}$`);

test('answers the managed tunnel before the app middleware', async ({ page }) => {
// The variant's auth middleware rejects every other POST.
expect((await page.request.post(`${BASE_PATH}/`, { data: 'x' })).status()).toBe(401);

const raceTunnelSpan = watchForTunnelSpan(APP_NAME, isServerSpanAt(TUNNEL_PATH));
const tunnelResponsePromise = page.waitForResponse(
response => response.request().method() === 'POST' && TUNNEL_PATH.test(new URL(response.url()).pathname),
);

await page.goto(`${BASE_PATH}/`);

expect((await tunnelResponsePromise).status()).toBe(200);
expect(
await raceTunnelSpan(isServerSpanAt(`${BASE_PATH}/users/6`), () => page.request.get(`${BASE_PATH}/users/6`)),
).toBe('anchor');
});
3 changes: 2 additions & 1 deletion packages/solidstart/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ export default makeNPMConfigVariants(
'src/client/solidrouter.ts',
'src/server/solidrouter.ts',
'src/vite/index.ts',
'src/nitro/tunnelPlugin.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)
packageSpecificConfig: {
external: ['solid-js/web', 'solid-js', '@sentry/solid', '@sentry/solid/solidrouter'],
external: ['solid-js/web', 'solid-js', '@sentry/solid', '@sentry/solid/solidrouter', 'h3'],
output: {
dynamicImportInCjs: true,
},
Expand Down
2 changes: 2 additions & 0 deletions packages/solidstart/src/client/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Client, Integration } from '@sentry/core';
import { applySdkMetadata } from '@sentry/core';
import type { BrowserOptions } from '@sentry/solid';
import { applyTunnelBasePath } from '../common/tunnel';
import {
browserTracingIntegration,
getDefaultIntegrations as getDefaultSolidIntegrations,
Expand All @@ -19,6 +20,7 @@ export function init(options: BrowserOptions): Client | undefined {
...options,
};

applyTunnelBasePath(opts);
applySdkMetadata(opts, 'solidstart', ['solidstart', 'solid']);

return initSolidSDK(opts);
Expand Down
17 changes: 17 additions & 0 deletions packages/solidstart/src/common/tunnel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Options } from '@sentry/core';
import { applyTunnelDefaults } from '@sentry/core';

/** Set on the server by `withSentry` to the app's `server.baseURL`. */
export const TUNNEL_BASE_PATH_GLOBAL = '__sentry_solidstart_base_path';

declare const __SENTRY_SOLIDSTART_TUNNEL_BASE_PATH__: string | undefined;

/**
* Serves the browser's managed tunnel under the app's `server.baseURL`, which `withSentry` defines at build time.
* A `basePath` the user set wins.
*/
export function applyTunnelBasePath(options: Pick<Options, 'tunnel'>): void {
const basePath =
typeof __SENTRY_SOLIDSTART_TUNNEL_BASE_PATH__ !== 'undefined' ? __SENTRY_SOLIDSTART_TUNNEL_BASE_PATH__ : '';
options.tunnel = applyTunnelDefaults(options.tunnel, { basePath });
}
1 change: 1 addition & 0 deletions packages/solidstart/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type RollupConfig = {
export type SolidStartInlineConfig = NonNullable<Parameters<typeof defineConfig>[0]>;

export type SolidStartInlineServerConfig = {
plugins?: string[];
hooks?: {
close?: () => unknown;
'rollup:before'?: (nitro: Nitro) => unknown;
Expand Down
35 changes: 32 additions & 3 deletions packages/solidstart/src/config/withSentry.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { debug } from '@sentry/core';
import { fileURLToPath } from 'node:url';
import { debug, GLOBAL_OBJ, normalizeTunnelBasePath } from '@sentry/core';
import { INSTRUMENTED_MODULE_NAMES } from '@sentry/server-utils/orchestrion/config';
import {
commonJSInteropOptions,
sentryCommonJSInteropPlugin,
sentryOrchestrionPlugin,
} from '@sentry/server-utils/orchestrion/rollup';
import type { Nitro, NitroConfig } from 'nitropack';
import { TUNNEL_BASE_PATH_GLOBAL } from '../common/tunnel';
import { addSentryPluginToVite } from '../vite/sentrySolidStartVite';
import type { SentrySolidStartPluginOptions } from '../vite/types';
import {
Expand Down Expand Up @@ -46,11 +48,21 @@ export function withSentry(
};

const server = (solidStartConfig.server || {}) as SolidStartInlineServerConfig;
// Requests reach the tunnel with `server.baseURL` still in front, so both sides serve it under that.
const tunnelBasePath = normalizeTunnelBasePath(
(server as SolidStartInlineServerConfig & { baseURL?: string }).baseURL,
);
// vinxi's dev server runs Nitro in this process, so the tunnel plugin can read it from here in dev.
if (tunnelBasePath) {
(GLOBAL_OBJ as { [TUNNEL_BASE_PATH_GLOBAL]?: string })[TUNNEL_BASE_PATH_GLOBAL] = tunnelBasePath;
}

const viteConfig = solidStartConfig.vite;
const vite =
typeof viteConfig === 'function'
? (...args: Parameters<typeof viteConfig>) => addSentryPluginToVite(viteConfig(...args), sentryPluginOptions)
: addSentryPluginToVite(viteConfig, sentryPluginOptions);
? (...args: Parameters<typeof viteConfig>) =>
addSentryPluginToVite(viteConfig(...args), sentryPluginOptions, tunnelBasePath)
: addSentryPluginToVite(viteConfig, sentryPluginOptions, tunnelBasePath);

const addBuildTimeInstrumentation = sentryPluginOptions.buildTimeInstrumentation !== false;

Expand All @@ -61,6 +73,14 @@ export function withSentry(
// but at runtime it holds resolved plugin objects we can push onto, so narrow to our own shape.
const sentryRollupConfig = rollupConfig as unknown as RollupConfig;

if (tunnelBasePath) {
// The built server runs in another process, so the tunnel plugin reads the base path from the bundle.
sentryRollupConfig.plugins.push({
name: 'sentry-solidstart-tunnel-base-path',
banner: `globalThis.${TUNNEL_BASE_PATH_GLOBAL} = ${JSON.stringify(tunnelBasePath)};`,
});
}

if (addBuildTimeInstrumentation) {
sentryRollupConfig.plugins.push(
sentryOrchestrionPlugin({ buildTimeInstrumentation: sentryPluginOptions.buildTimeInstrumentation }),
Expand Down Expand Up @@ -116,6 +136,15 @@ export function withSentry(
externals,
commonJS,
modules: [...existingModules, sentryNitroModule],
// Through `server.plugins`, since vinxi's dev server ignores plugins that Nitro modules add.
plugins: [...(server.plugins || []), getTunnelPluginPath()],
},
};
}

function getTunnelPluginPath(): string {
const url = new URL('../nitro/tunnelPlugin.js', import.meta.url);

// `import.meta.url` is a file URL in the built package, but not in every test environment.
return url.protocol === 'file:' ? fileURLToPath(url) : url.pathname;
}
33 changes: 33 additions & 0 deletions packages/solidstart/src/nitro/tunnelPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { GLOBAL_OBJ } from '@sentry/core';
import { handleTunnelRequestIfMatched, isTunnelRequest, setManagedTunnelDefaults } from '@sentry/core/server';
import { eventHandler, toWebRequest } from 'h3';
import type { NitroAppPlugin } from 'nitropack';
import { TUNNEL_BASE_PATH_GLOBAL } from '../common/tunnel';

/**
* Answers managed tunnel requests on SolidStart v1 (Nitro v2), from the first layer of the h3 app. That runs before
* route rules, the app's middleware and routing. `withSentry` registers it.
*/
export default (nitroApp => {
// The first layer sees the path before Nitro strips `server.baseURL`, so the tunnel is served under it. The server
// bundle's banner sets the global before this plugin runs.
setManagedTunnelDefaults({
basePath: (GLOBAL_OBJ as { [TUNNEL_BASE_PATH_GLOBAL]?: string })[TUNNEL_BASE_PATH_GLOBAL],
});

// Not the `request` hook: h3 still runs the first layer after a hook answered, so route rules would act on the
// sent response.
nitroApp.h3App.stack.unshift({
route: '',
handler: eventHandler(event => {
// Checked on the method and path first, so no other request's body gets wrapped in a web `Request`.
if (!isTunnelRequest({ method: event.method, url: event.path })) {
return undefined;
}

// `toWebRequest` also reads the body of the mock requests serverless presets pass through `localFetch`, which
// aren't Node streams. A returned `Response` is sent and ends the request, `undefined` hands it on.
return handleTunnelRequestIfMatched(toWebRequest(event));
}),
});
}) satisfies NitroAppPlugin;
24 changes: 20 additions & 4 deletions packages/solidstart/src/vite/sentrySolidStartVite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,22 @@ import type { SentrySolidStartPluginOptions } from './types';
/**
* Various Sentry vite plugins to be used for SolidStart.
*/
export const sentrySolidStartVite = (options: SentrySolidStartPluginOptions = {}, viteConfig: UserConfig): Plugin[] => {
export const sentrySolidStartVite = (
options: SentrySolidStartPluginOptions = {},
viteConfig: UserConfig,
tunnelBasePath = '',
): Plugin[] => {
warnOnRemovedBuildOptions(options, ['unstable_sentryVitePluginOptions']);

const sentryPlugins: Plugin[] = [];
const sentryPlugins: Plugin[] = [
{
name: 'sentry-solidstart-tunnel-base-path',
// Serves the browser's managed tunnel under `server.baseURL`, like every other route of the app.
config: () => ({
define: { __SENTRY_SOLIDSTART_TUNNEL_BASE_PATH__: JSON.stringify(tunnelBasePath) },
}),
},
];

if (options.autoInjectServerSentry !== 'experimental_dynamic-import') {
sentryPlugins.push(makeBuildInstrumentationFilePlugin(options));
Expand All @@ -31,9 +43,13 @@ export const sentrySolidStartVite = (options: SentrySolidStartPluginOptions = {}
/**
* Helper to add the Sentry SolidStart vite plugin to a vite config.
*/
export const addSentryPluginToVite = (config: UserConfig = {}, options: SentrySolidStartPluginOptions): UserConfig => {
export const addSentryPluginToVite = (
config: UserConfig = {},
options: SentrySolidStartPluginOptions,
tunnelBasePath?: string,
): UserConfig => {
const plugins = Array.isArray(config.plugins) ? [...config.plugins] : [];
plugins.unshift(sentrySolidStartVite(options, config));
plugins.unshift(sentrySolidStartVite(options, config, tunnelBasePath));

return {
...config,
Expand Down
25 changes: 25 additions & 0 deletions packages/solidstart/test/common/tunnel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { applyTunnelBasePath } from '../../src/common/tunnel';

describe('applyTunnelBasePath', () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it('serves the managed tunnel under the defined base path', () => {
vi.stubGlobal('__SENTRY_SOLIDSTART_TUNNEL_BASE_PATH__', '/app');
const options: Parameters<typeof applyTunnelBasePath>[0] = { tunnel: true };

applyTunnelBasePath(options);

expect(options.tunnel).toEqual({ basePath: '/app' });
});

it('leaves the tunnel alone without a base path', () => {
const options: Parameters<typeof applyTunnelBasePath>[0] = { tunnel: true };

applyTunnelBasePath(options);

expect(options.tunnel).toBe(true);
});
});
13 changes: 13 additions & 0 deletions packages/solidstart/test/config/withSentry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ describe('withSentry()', () => {
expect(addInstrumentationFileToBuildMock).toHaveBeenCalledWith(nitroOptions);
});

it('registers the managed tunnel plugin through server.plugins and keeps user plugins', () => {
const config = withSentry({ server: { plugins: ['./user-plugin.ts'] } }, {});

expect((config?.server as { plugins?: string[] }).plugins).toEqual([
'./user-plugin.ts',
// Vitest serves the source file, the built package the transpiled one.
expect.stringMatching(/\/nitro\/tunnelPlugin\.[jt]s$/),
]);
});

it('does not override user-defined hooks in server.hooks', () => {
const config = withSentry(solidStartConfig, {});
expect(config?.server.hooks?.['rollup:before']).toBe(userDefinedNitroRollupBeforeHookMock);
Expand Down Expand Up @@ -116,6 +126,7 @@ describe('withSentry()', () => {
});
const names = config?.vite.plugins.flat().map((plugin: Plugin) => plugin.name);
expect(names).toEqual([
'sentry-solidstart-tunnel-base-path',
'sentry-solidstart-build-instrumentation-file',
'sentry-vite-plugin',
'sentry-solidstart-update-source-map-setting',
Expand All @@ -139,6 +150,7 @@ describe('withSentry()', () => {

const names = config?.vite.plugins.flat().map((plugin: Plugin) => plugin.name);
expect(names).toEqual([
'sentry-solidstart-tunnel-base-path',
'sentry-solidstart-build-instrumentation-file',
'sentry-vite-plugin',
'sentry-solidstart-update-source-map-setting',
Expand Down Expand Up @@ -166,6 +178,7 @@ describe('withSentry()', () => {
.plugins.flat()
.map((plugin: Plugin) => plugin.name);
expect(names).toEqual([
'sentry-solidstart-tunnel-base-path',
'sentry-solidstart-build-instrumentation-file',
'sentry-vite-plugin',
'sentry-solidstart-update-source-map-setting',
Expand Down
Loading
Loading