Skip to content
Draft
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
19 changes: 17 additions & 2 deletions dev-packages/e2e-tests/test-applications/solidstart-2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
"start": "HOST=localhost PORT=3030 node .output/server/index.mjs",
"test:prod": "TEST_ENV=production playwright test",
"test:build": "pnpm install && pnpm build",
"test:assert": "pnpm test:prod"
"test:assert": "pnpm test:prod",
"test:build:tunnel": "pnpm install && E2E_TEST_TUNNEL=1 pnpm build",
"test:assert:tunnel": "E2E_TEST_TUNNEL=1 E2E_TEST_DSN=http://public@localhost:3031/1337 TEST_ENV=production playwright test tunnel",
"test:assert:tunnel-dev": "E2E_TEST_TUNNEL=1 E2E_TEST_DSN=http://public@localhost:3031/1337 TEST_ENV=development playwright test tunnel"
},
"type": "module",
"dependencies": {
Expand Down Expand Up @@ -38,6 +41,18 @@
"node": ">=24"
},
"sentryTest": {
"optional": true
"optional": true,
"variants": [
{
"build-command": "pnpm test:build:tunnel",
"assert-command": "pnpm test:assert:tunnel",
"label": "solidstart-2 (tunnel)"
},
{
"label": "solidstart-2 (tunnel-dev)",
"build-command": "pnpm install",
"assert-command": "pnpm test:assert:tunnel-dev"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getPlaywrightConfig } from '@sentry-internal/test-utils';

const config = getPlaywrightConfig({
startCommand: 'pnpm start',
startCommand: process.env.TEST_ENV === 'development' ? 'pnpm exec vite dev --port 3030' : 'pnpm start',
port: 3030,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default definePlugin(() => {
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 @@ -3,13 +3,16 @@ 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',
// The tunnel variant forwards to the test proxy, so the DSN has to point there.
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
@@ -0,0 +1,37 @@
import { expect, test } from '@playwright/test';
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';

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

const APP_NAME = 'solidstart-2';
const TUNNEL_PATH = /^\/[a-z0-9]{1,7}$/;

test('answers the managed tunnel and drops its server span', async ({ page }) => {
const tunnelServerSpanPromise = waitForStreamedSpan(
APP_NAME,
span => getSpanOp(span) === 'http.server' && TUNNEL_PATH.test(String(span.attributes['url.path']?.value ?? '')),
);
const tunnelResponsePromise = page.waitForResponse(
response => response.request().method() === 'POST' && TUNNEL_PATH.test(new URL(response.url()).pathname),
);

await page.goto('/');
const tunnelResponse = await tunnelResponsePromise;

expect(tunnelResponse.status()).toBe(200);
expect(new URL(tunnelResponse.url()).origin).toBe(new URL(page.url()).origin);

// Anchor on a regular server span requested after the tunnel traffic: spans flush in order, so a tunnel span
// would arrive first.
const anchorSpanPromise = waitForStreamedSpan(
APP_NAME,
span => getSpanOp(span) === 'http.server' && String(span.attributes['url.path']?.value ?? '') === '/users/6',
);
await page.request.get('/users/6');

const winner = await Promise.race([
tunnelServerSpanPromise.then(() => 'tunnel' as const),
anchorSpanPromise.then(() => 'anchor' as const),
]);
expect(winner).toBe('anchor');
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { nitro } from 'nitro/vite';
import { defineConfig } from 'vite';

export default defineConfig({
define: {
__E2E_TEST_TUNNEL__: JSON.stringify(!!process.env.E2E_TEST_TUNNEL),
},
plugins: [
solidStart({
appRoot: './src',
Expand Down
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,
});
36 changes: 33 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,13 @@
"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": "pnpm install && E2E_TEST_TUNNEL=1 pnpm build",
"test:build:tunnel-basepath": "pnpm install && E2E_TEST_TUNNEL=1 E2E_TEST_BASE_PATH=/app pnpm build",
"test:assert:tunnel": "E2E_TEST_TUNNEL=1 E2E_TEST_DSN=http://public@localhost:3031/1337 TEST_ENV=production playwright test tunnel",
"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-dev": "E2E_TEST_TUNNEL=1 E2E_TEST_DSN=http://public@localhost:3031/1337 TEST_ENV=development 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 +36,35 @@
"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",
"assert-command": "pnpm test:assert:tunnel",
"label": "solidstart (tunnel)"
},
{
"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-dev",
"label": "solidstart (tunnel-dev)"
},
{
"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,42 @@
import { expect, test } from '@playwright/test';
import { getSpanOp, waitForStreamedSpan } 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 tunnelServerSpanPromise = waitForStreamedSpan(
APP_NAME,
span => getSpanOp(span) === 'http.server' && TUNNEL_PATH.test(String(span.attributes['url.path']?.value ?? '')),
);
const tunnelResponsePromise = page.waitForResponse(
response => response.request().method() === 'POST' && TUNNEL_PATH.test(new URL(response.url()).pathname),
);

await page.goto(`${BASE_PATH}/`);
const tunnelResponse = await tunnelResponsePromise;

expect(tunnelResponse.status()).toBe(200);
expect(new URL(tunnelResponse.url()).origin).toBe(new URL(page.url()).origin);

// Anchor on a regular server span requested after the tunnel traffic: spans flush in order, so a tunnel span
// would arrive first.
const anchorSpanPromise = waitForStreamedSpan(
APP_NAME,
span =>
getSpanOp(span) === 'http.server' && String(span.attributes['url.path']?.value ?? '') === `${BASE_PATH}/users/6`,
);
await page.request.get(`${BASE_PATH}/users/6`);

const winner = await Promise.race([
tunnelServerSpanPromise.then(() => 'tunnel' as const),
anchorSpanPromise.then(() => 'anchor' as const),
]);
expect(winner).toBe('anchor');
});
1 change: 1 addition & 0 deletions packages/solidstart/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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)
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 ? { basePath } : undefined);
}
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;
}
Loading
Loading