Skip to content

Commit 6465550

Browse files
committed
feat(test-utils): Add Spotlight support for E2E tests
- Add @spotlightjs/spotlight dependency to test-utils - Create spotlight.ts with startSpotlight() and event waiting helpers - Add Spotlight mode to getPlaywrightConfig() via useSpotlight option - Migrate react-router-7-lazy-routes test app as proof of concept - Use DSN workaround (http://spotlight@localhost:8969/0) - Remove custom event proxy in favor of Spotlight - Update tests to use waitForSpotlightTransaction This migration eliminates the custom event proxy server which had timing issues causing flaky tests. Spotlight provides a more robust solution for capturing and streaming Sentry events during E2E tests.
1 parent f00a377 commit 6465550

File tree

10 files changed

+451
-79
lines changed

10 files changed

+451
-79
lines changed

dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"devDependencies": {
4545
"@playwright/test": "~1.56.0",
4646
"@sentry-internal/test-utils": "link:../../../test-utils",
47+
"@spotlightjs/spotlight": "^4.7.2",
4748
"serve": "14.0.1",
4849
"npm-run-all2": "^6.2.0"
4950
},
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
22

33
const config = getPlaywrightConfig({
4-
startCommand: `pnpm start`,
4+
useSpotlight: true,
55
});
66

77
export default config;

dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/index.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,13 @@ function getRuntimeConfig(): { lazyRouteTimeout?: number; idleTimeout?: number }
5555

5656
const runtimeConfig = getRuntimeConfig();
5757

58+
// Use Spotlight DSN workaround - this makes the SDK send events directly to Spotlight
59+
// Spotlight runs on port 8969 by default and captures all Sentry events
60+
const SPOTLIGHT_DSN = 'http://spotlight@localhost:8969/0';
61+
5862
Sentry.init({
5963
environment: 'qa', // dynamic sampling bias to keep transactions
60-
dsn: process.env.REACT_APP_E2E_TEST_DSN,
64+
dsn: SPOTLIGHT_DSN,
6165
integrations: [
6266
Sentry.reactRouterV7BrowserTracingIntegration({
6367
useEffect: React.useEffect,
@@ -75,8 +79,6 @@ Sentry.init({
7579
// for finer control
7680
tracesSampleRate: 1.0,
7781
release: 'e2e-test',
78-
79-
tunnel: 'http://localhost:3031',
8082
});
8183

8284
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV7(createBrowserRouter);

dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/start-event-proxy.mjs

Lines changed: 0 additions & 6 deletions
This file was deleted.

dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/timeout-behaviour.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForTransaction } from '@sentry-internal/test-utils';
2+
import { waitForSpotlightTransaction } from '@sentry-internal/test-utils';
33

44
test('lazyRouteTimeout: Routes load within timeout window', async ({ page }) => {
5-
const transactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => {
5+
const transactionPromise = waitForSpotlightTransaction(async transactionEvent => {
66
return (
77
!!transactionEvent?.transaction &&
88
transactionEvent.contexts?.trace?.op === 'navigation' &&
@@ -27,7 +27,7 @@ test('lazyRouteTimeout: Routes load within timeout window', async ({ page }) =>
2727
});
2828

2929
test('lazyRouteTimeout: Infinity timeout always waits for routes', async ({ page }) => {
30-
const transactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => {
30+
const transactionPromise = waitForSpotlightTransaction(async transactionEvent => {
3131
return (
3232
!!transactionEvent?.transaction &&
3333
transactionEvent.contexts?.trace?.op === 'navigation' &&
@@ -51,7 +51,7 @@ test('lazyRouteTimeout: Infinity timeout always waits for routes', async ({ page
5151
});
5252

5353
test('idleTimeout: Captures all activity with increased timeout', async ({ page }) => {
54-
const transactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => {
54+
const transactionPromise = waitForSpotlightTransaction(async transactionEvent => {
5555
return (
5656
!!transactionEvent?.transaction &&
5757
transactionEvent.contexts?.trace?.op === 'navigation' &&
@@ -79,7 +79,7 @@ test('idleTimeout: Captures all activity with increased timeout', async ({ page
7979
});
8080

8181
test('idleTimeout: Finishes prematurely with low timeout', async ({ page }) => {
82-
const transactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => {
82+
const transactionPromise = waitForSpotlightTransaction(async transactionEvent => {
8383
return (
8484
!!transactionEvent?.transaction &&
8585
transactionEvent.contexts?.trace?.op === 'navigation' &&
@@ -107,7 +107,7 @@ test('idleTimeout: Finishes prematurely with low timeout', async ({ page }) => {
107107
});
108108

109109
test('idleTimeout: Pageload on deeply nested route', async ({ page }) => {
110-
const pageloadPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => {
110+
const pageloadPromise = waitForSpotlightTransaction(async transactionEvent => {
111111
return (
112112
!!transactionEvent?.transaction &&
113113
transactionEvent.contexts?.trace?.op === 'pageload' &&

dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts

Lines changed: 40 additions & 40 deletions
Large diffs are not rendered by default.

dev-packages/test-utils/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"devDependencies": {
5050
"@playwright/test": "~1.56.0",
5151
"@sentry/core": "10.30.0",
52+
"@spotlightjs/spotlight": "^4.7.2",
5253
"eslint-plugin-regexp": "^1.15.0"
5354
},
5455
"volta": {

dev-packages/test-utils/src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,16 @@ export {
99
waitForPlainRequest,
1010
} from './event-proxy-server';
1111

12+
export {
13+
startSpotlight,
14+
getSpotlightDsn,
15+
waitForEnvelopeItem as waitForSpotlightEnvelopeItem,
16+
waitForError as waitForSpotlightError,
17+
waitForSession as waitForSpotlightSession,
18+
waitForTransaction as waitForSpotlightTransaction,
19+
clearEventBuffer as clearSpotlightEventBuffer,
20+
getCurrentSpotlightInstance,
21+
} from './spotlight';
22+
1223
export { getPlaywrightConfig } from './playwright-config';
1324
export { createBasicSentryServer } from './server';

dev-packages/test-utils/src/playwright-config.ts

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,34 @@
11
import type { PlaywrightTestConfig } from '@playwright/test';
22

3+
interface PlaywrightConfigOptions {
4+
/** Command to start the test app (not needed when using Spotlight) */
5+
startCommand?: string;
6+
/** Port for the test app */
7+
port?: number;
8+
/** Port for the event proxy (legacy mode only) */
9+
eventProxyPort?: number;
10+
/** Path to the event proxy file (legacy mode only) */
11+
eventProxyFile?: string;
12+
/**
13+
* Use Spotlight instead of the custom event proxy server.
14+
* When enabled, Spotlight will automatically run the app and capture events.
15+
* The app's Sentry SDK should use the DSN workaround: http://spotlight@localhost:PORT/0
16+
*/
17+
useSpotlight?: boolean;
18+
/** Port for Spotlight sidecar. Defaults to 8969. Use 0 for dynamic port. */
19+
spotlightPort?: number;
20+
/** Enable debug output for Spotlight */
21+
spotlightDebug?: boolean;
22+
}
23+
324
/** Get a playwright config to use in an E2E test app. */
425
export function getPlaywrightConfig(
5-
options?: {
6-
startCommand?: string;
7-
port?: number;
8-
eventProxyPort?: number;
9-
eventProxyFile?: string;
10-
},
26+
options?: PlaywrightConfigOptions,
1127
overwriteConfig?: Partial<PlaywrightTestConfig>,
1228
): PlaywrightTestConfig {
1329
const testEnv = process.env['TEST_ENV'] || 'production';
1430
const appPort = options?.port || 3030;
15-
const eventProxyPort = options?.eventProxyPort || 3031;
16-
const eventProxyFile = options?.eventProxyFile || 'start-event-proxy.mjs';
17-
const startCommand = options?.startCommand;
31+
const useSpotlight = options?.useSpotlight || false;
1832

1933
/**
2034
* See https://playwright.dev/docs/test-configuration.
@@ -71,31 +85,63 @@ export function getPlaywrightConfig(
7185
],
7286

7387
/* Run your local dev server before starting the tests */
74-
webServer: [
75-
{
76-
command: `node ${eventProxyFile}`,
77-
port: eventProxyPort,
78-
stdout: 'pipe',
79-
stderr: 'pipe',
80-
},
81-
],
88+
webServer: [],
8289
};
8390

84-
if (startCommand) {
85-
// @ts-expect-error - we set `config.webserver` to an array above.
86-
// TS just can't infer that and thinks it could also be undefined or an object.
91+
if (useSpotlight) {
92+
// Spotlight mode: Use Spotlight to run the app and capture events
93+
// Spotlight auto-detects the start script from package.json
94+
const spotlightPort = options?.spotlightPort ?? 8969;
95+
const spotlightArgs = ['-f', 'json'];
96+
97+
if (spotlightPort !== 0) {
98+
spotlightArgs.push('-p', String(spotlightPort));
99+
}
100+
101+
if (options?.spotlightDebug) {
102+
spotlightArgs.push('-d');
103+
}
104+
105+
// @ts-expect-error - we set `config.webserver` to an array above
87106
config.webServer.push({
88-
command: startCommand,
107+
command: `yarn spotlight run ${spotlightArgs.join(' ')}`,
89108
port: appPort,
90109
stdout: 'pipe',
91110
stderr: 'pipe',
92111
env: {
93-
// Inherit all environment variables from the parent process
94-
// This is needed for env vars like NEXT_PUBLIC_SENTRY_SPOTLIGHT to be passed through
95112
...process.env,
96113
PORT: appPort.toString(),
97114
},
98115
});
116+
} else {
117+
// Legacy mode: Use custom event proxy server
118+
const eventProxyPort = options?.eventProxyPort || 3031;
119+
const eventProxyFile = options?.eventProxyFile || 'start-event-proxy.mjs';
120+
const startCommand = options?.startCommand;
121+
122+
// @ts-expect-error - we set `config.webserver` to an array above
123+
config.webServer.push({
124+
command: `node ${eventProxyFile}`,
125+
port: eventProxyPort,
126+
stdout: 'pipe',
127+
stderr: 'pipe',
128+
});
129+
130+
if (startCommand) {
131+
// @ts-expect-error - we set `config.webserver` to an array above
132+
config.webServer.push({
133+
command: startCommand,
134+
port: appPort,
135+
stdout: 'pipe',
136+
stderr: 'pipe',
137+
env: {
138+
// Inherit all environment variables from the parent process
139+
// This is needed for env vars like NEXT_PUBLIC_SENTRY_SPOTLIGHT to be passed through
140+
...process.env,
141+
PORT: appPort.toString(),
142+
},
143+
});
144+
}
99145
}
100146

101147
return {

0 commit comments

Comments
 (0)