Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
53acde0
feat(solidstart): Serve the managed tunnel on SolidStart v1
logaretm Oct 15, 2026
9714075
fix(solidstart): Serve the managed tunnel under `server.baseURL`
logaretm Oct 16, 2026
2492392
test(solidstart): Check the managed tunnel on SolidStart 2
logaretm Oct 16, 2026
55fde50
test(solidstart): Check the managed tunnel under the SolidStart 2 dev…
logaretm Oct 16, 2026
6e7ab1e
test(solidstart): Check the managed tunnel under `vinxi dev` too
logaretm Oct 16, 2026
b30d216
fix(solidstart): Normalize the tunnel base path with the core util
logaretm Oct 16, 2026
5e51eb9
fix(solidstart): Read the tunnel request with h3 so serverless preset…
logaretm Oct 18, 2026
af77521
fix(solidstart): Serve the tunnel from the first h3 layer so route ru…
logaretm Oct 19, 2026
0fa52ad
ref(solidstart): Report the base path once with setManagedTunnelDefaults
logaretm Oct 19, 2026
bd38d8a
test(solidstart): Send Content-Length with the tunnel test requests
logaretm Oct 19, 2026
71d60ae
test(solidstart): Race the tunnel span with the shared helper, and dr…
logaretm Oct 19, 2026
8745a90
ref(solidstart): Let core skip an empty base path, and drop the test …
logaretm Oct 19, 2026
ccd94ce
ref: Import the internal tunnel helpers under their `_INTERNAL_` names
logaretm Oct 22, 2026
4ba1bc1
fix(solidstart): Declare h3 as a dependency for the tunnel plugin
logaretm Oct 23, 2026
ff0b8c6
test(solidstart): Declare node-mock-http for the tunnel plugin test
logaretm Oct 23, 2026
4a268dd
test(solidstart): Serve the managed tunnel with `serveTunnelIntegration`
logaretm Oct 27, 2026
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 @@ -5,6 +5,13 @@ 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),
},
},
},
{
sentryUrl: 'http://localhost:3032',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0, // Capture 100% of the transactions
tunnel: 'http://localhost:3031/', // proxy server
integrations: process.env.E2E_TEST_TUNNEL ? [Sentry.serveTunnelIntegration()] : [],
debug: !!process.env.DEBUG,
});
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": "node assert-build.ts && 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 @@ -4,13 +4,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://[email protected]/1337',
dsn: __E2E_TEST_TUNNEL__ ? 'http://public@localhost:3031/1337' : 'https://[email protected]/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 @@ -5,5 +5,6 @@ Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0, // Capture 100% of the transactions
tunnel: 'http://localhost:3031/', // proxy server
integrations: process.env.E2E_TEST_TUNNEL ? [Sentry.serveTunnelIntegration()] : [],
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');
});
4 changes: 3 additions & 1 deletion packages/solidstart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@
"@sentry/server-utils": "11.9.0",
"@sentry/solid": "11.9.0",
"@sentry/bundler-plugins": "11.9.0",
"@sentry/conventions": "0.29.0"
"@sentry/conventions": "0.29.0",
"h3": "^1.15.10"
},
"devDependencies": {
"@sentry-internal/rollup-utils": "11.9.0",
Expand All @@ -99,6 +100,7 @@
"@testing-library/user-event": "^14.5.2",
"@types/node": "^20.19.0",
"nitropack": "^2.13.4",
"node-mock-http": "^1.0.4",
"solid-js": "^1.9.11",
"vinxi": "^0.5.11",
"vite": "^8.3.1",
Expand Down
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 { _INTERNAL_applyTunnelDefaults as 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, _INTERNAL_normalizeTunnelBasePath as 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;
}
37 changes: 37 additions & 0 deletions packages/solidstart/src/nitro/tunnelPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { GLOBAL_OBJ } from '@sentry/core';
import {
handleTunnelRequestIfMatched,
isTunnelRequest,
_INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults,
} from '@sentry/core/server';
import { eventHandler, toWebRequest } from 'h3';
Comment thread
logaretm marked this conversation as resolved.
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({
Comment thread
logaretm marked this conversation as resolved.
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);
});
});
Loading
Loading