Skip to content

Commit 9af8da5

Browse files
JPeer264claude
andcommitted
test(nextjs): Add vinext e2e tests
vinext builds a Next.js app with Vite. Its e2e tests run in two places: - nextjs-16 gets the optional variants `nextjs-16 (vinext)` and `nextjs-16 (vinext, bun)`. vinext builds the same app, and the same tests run as for Next.js. They skip the checks of the Next.js build output, and what vinext can not do yet: `openai` and `ioredis` get their diagnostics channels from the build-time loader of webpack or Turbopack, which vinext does not run, and without a Sentry build plugin there is no component annotation and no module metadata for the third-party filter. The OpenNext wrangler config is now `wrangler.opennext.jsonc`, because vinext refuses to build without the Cloudflare plugin when the project root has a wrangler config. - `vinext-cf-workers` covers vinext on Workers. Its Worker entry has no Sentry calls, so `sentryCloudflareVitePlugin` must wrap it with `withSentry` from `@sentry/nextjs/cloudflare`. The app also has the server config of a Next.js app, whose `init` must create no second client in the Worker. The tests check that each request has one `http.server` span with the route of Next.js and the Next.js spans in its segment, that an incoming trace is continued in that segment, and that an error from `onRequestError` is in the trace of the request. There is no Deno variant. On Deno, the `node:http` server of vinext starts each request after the first one in the AsyncLocalStorage store of an earlier request, also without Sentry, so vinext creates no request span. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 08ff431 commit 9af8da5

33 files changed

Lines changed: 372 additions & 33 deletions

‎dev-packages/e2e-tests/README.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,11 @@ reference setup.
172172
`withSentry` and reads the init options from `instrument.server.ts` next to the entry. The start command runs
173173
`wrangler dev` on the build output. Code at module scope must not do I/O (for example open a database connection),
174174
because workerd does not allow it. `nextjs-16` has no Vite build: its Cloudflare variant builds with
175-
`opennextjs-cloudflare build` and wraps the generated Worker in `worker.ts` with `withSentry` from
176-
`@sentry/nextjs/cloudflare`.
175+
`opennextjs-cloudflare build --config wrangler.opennext.jsonc` and wraps the generated Worker in `worker.ts` with
176+
`withSentry` from `@sentry/nextjs/cloudflare`.
177+
- **Build flavors**: a variant can also build the same app with another tool, for example `nextjs-16 (vinext)` builds
178+
`nextjs-16` with vinext. A build flavor is not a runtime: its assert script sets its own env var (`VINEXT=1`, read by
179+
`tests/isVinext.ts`). It sets `runtime` only to run on another runtime, for example `nextjs-16 (vinext, bun)`.
177180
- **Runtime-specific files**: a file that has the runtime as a part of its name replaces the existing file without that
178181
part, for example `app/entry.server.cloudflare.tsx` replaces `app/entry.server.tsx` and `vite.cloudflare.config.ts`
179182
replaces `vite.config.ts`. For a variant with `runtime`, the runner copies these files over the others in the

‎dev-packages/e2e-tests/test-applications/nextjs-16/package.json‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
"test:build-canary-webpack": "pnpm install && pnpm add next@canary && pnpm build-webpack",
2222
"test:assert": "pnpm test:prod && pnpm test:dev",
2323
"test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack",
24-
"test:build:cloudflare": "pnpm install && opennextjs-cloudflare build",
25-
"test:build-latest:cloudflare": "pnpm install && pnpm add next@latest && opennextjs-cloudflare build",
26-
"start:cloudflare": "opennextjs-cloudflare preview --var \"E2E_TEST_DSN:$E2E_TEST_DSN\""
24+
"test:build:cloudflare": "pnpm install && opennextjs-cloudflare build --config wrangler.opennext.jsonc",
25+
"test:build-latest:cloudflare": "pnpm install && pnpm add next@latest && opennextjs-cloudflare build --config wrangler.opennext.jsonc",
26+
"start:cloudflare": "opennextjs-cloudflare preview --config wrangler.opennext.jsonc --var \"E2E_TEST_DSN:$E2E_TEST_DSN\"",
27+
"test:build:vinext": "pnpm install && pnpm add react@19.3.0 react-dom@19.3.0 && npm pkg set type=module && vinext build",
28+
"test:assert:vinext": "VINEXT=1 pnpm test:prod"
2729
},
2830
"//": "Pin `ioredis` to 5.10.1: it's the last version before the driver publishes its own native diagnostics channels; orchestrion's config covers `ioredis <5.11.0`.",
2931
"dependencies": {
@@ -53,7 +55,12 @@
5355
"@types/react-dom": "^19",
5456
"eslint": "^9",
5557
"eslint-config-next": "^16",
58+
"@vitejs/plugin-react": "^6.1.1",
59+
"@vitejs/plugin-rsc": "^0.5.35",
60+
"react-server-dom-webpack": "^19.3.0",
5661
"typescript": "^5",
62+
"vinext": "1.0.0-beta.13",
63+
"vite": "^8.3.0",
5764
"wrangler": "^4.61.0"
5865
},
5966
"volta": {
@@ -109,6 +116,17 @@
109116
"assert-command": "pnpm test:prod",
110117
"deno-version": "v2.9.0",
111118
"label": "nextjs-16 (deno)"
119+
},
120+
{
121+
"build-command": "pnpm test:build:vinext",
122+
"assert-command": "pnpm test:assert:vinext",
123+
"label": "nextjs-16 (vinext)"
124+
},
125+
{
126+
"runtime": "bun",
127+
"build-command": "pnpm test:build:vinext",
128+
"assert-command": "pnpm test:assert:vinext",
129+
"label": "nextjs-16 (vinext, bun)"
112130
}
113131
]
114132
}

‎dev-packages/e2e-tests/test-applications/nextjs-16/playwright.config.mjs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ const productionStartCommands = {
1212
cloudflare: 'pnpm start:cloudflare --port 3030',
1313
};
1414

15+
// The `nextjs-16 (vinext)` variants build the app with vinext instead of Next.js.
16+
const vinextStartCommands = {
17+
node: 'node ./node_modules/vinext/dist/cli.js start --port 3030',
18+
bun: 'bun --bun ./node_modules/vinext/dist/cli.js start --port 3030',
19+
};
20+
1521
const getStartCommand = () => {
1622
if (testEnv === 'development-webpack') {
1723
return 'pnpm next dev -p 3030 --webpack 2>&1 | tee .tmp_dev_server_logs';
@@ -22,7 +28,7 @@ const getStartCommand = () => {
2228
}
2329

2430
if (testEnv === 'production') {
25-
return productionStartCommands[getRuntime()];
31+
return (process.env.VINEXT ? vinextStartCommands : productionStartCommands)[getRuntime()];
2632
}
2733

2834
throw new Error(`Unknown test env: ${testEnv}`);

‎dev-packages/e2e-tests/test-applications/nextjs-16/tests/build-output.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { findAbsolutePathImports } from '@sentry-internal/test-utils';
33
import * as fs from 'fs';
44
import * as path from 'path';
55
import { isDevMode } from './isDevMode';
6+
import { isVinext } from './isVinext';
7+
8+
test.skip(isVinext, 'Checks the build output of Next.js');
69

710
test('emits no absolute-path imports into the server output', () => {
811
const leaks = findAbsolutePathImports({ outputDir: path.join(process.cwd(), '.next', 'server') });

‎dev-packages/e2e-tests/test-applications/nextjs-16/tests/component-annotation.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForError } from '@sentry-internal/test-utils';
3+
import { isVinext } from './isVinext';
34

45
const isWebpackDev = process.env.TEST_ENV === 'development-webpack';
56

67
test('React component annotation adds data-sentry-component attributes (Turbopack)', async ({ page }) => {
8+
test.skip(isVinext, 'vinext runs no Sentry build plugin, so components get no annotation');
79
test.skip(isWebpackDev, 'Only relevant for Turbopack builds');
810

911
await page.goto('/component-annotation');
Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { expect, test } from '@playwright/test';
22
import { collectStreamedSpansUntilSegment, getRuntime } from '@sentry-internal/test-utils';
3+
import { isVinext } from './isVinext';
34

45
test('Instruments DB calls made during server-side rendering of a page', async ({ page }) => {
6+
test.skip(isVinext && getRuntime() === 'bun', 'Bun has no runtime module hook and vinext no build-time loader');
7+
58
// The db spans are children of the segment span, which ends last.
69
const spansPromise = collectStreamedSpansUntilSegment('nextjs-16', 'GET /db-page');
710

@@ -13,7 +16,8 @@ test('Instruments DB calls made during server-side rendering of a page', async (
1316

1417
// One page render produces spans from both injection paths: pg (externalized → runtime module
1518
// hook) and ioredis (bundle-safe allowlisted → build-time loader). Bun and Workers have no runtime module
16-
// hook, so pg creates no spans there.
19+
// hook, so pg creates no spans there. vinext does not run the build-time loader of webpack or Turbopack, so ioredis
20+
// creates no spans there.
1721
if (getRuntime() !== 'bun' && getRuntime() !== 'cloudflare') {
1822
expect(spans).toContainEqual(
1923
expect.objectContaining({
@@ -28,30 +32,32 @@ test('Instruments DB calls made during server-side rendering of a page', async (
2832
}),
2933
);
3034
}
31-
expect(spans).toContainEqual(
32-
expect.objectContaining({
33-
name: 'set localhost:6379',
34-
status: 'ok',
35-
attributes: expect.objectContaining({
36-
'sentry.op': { value: 'db.query', type: 'string' },
37-
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
38-
'db.system.name': { value: 'redis', type: 'string' },
39-
'db.operation.name': { value: 'set', type: 'string' },
40-
'db.query.text': { value: 'set page-key [1 other arguments]', type: 'string' },
35+
if (!isVinext) {
36+
expect(spans).toContainEqual(
37+
expect.objectContaining({
38+
name: 'set localhost:6379',
39+
status: 'ok',
40+
attributes: expect.objectContaining({
41+
'sentry.op': { value: 'db.query', type: 'string' },
42+
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
43+
'db.system.name': { value: 'redis', type: 'string' },
44+
'db.operation.name': { value: 'set', type: 'string' },
45+
'db.query.text': { value: 'set page-key [1 other arguments]', type: 'string' },
46+
}),
4147
}),
42-
}),
43-
);
44-
expect(spans).toContainEqual(
45-
expect.objectContaining({
46-
name: 'get localhost:6379',
47-
status: 'ok',
48-
attributes: expect.objectContaining({
49-
'sentry.op': { value: 'db.query', type: 'string' },
50-
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
51-
'db.system.name': { value: 'redis', type: 'string' },
52-
'db.operation.name': { value: 'get', type: 'string' },
53-
'db.query.text': { value: 'get page-key', type: 'string' },
48+
);
49+
expect(spans).toContainEqual(
50+
expect.objectContaining({
51+
name: 'get localhost:6379',
52+
status: 'ok',
53+
attributes: expect.objectContaining({
54+
'sentry.op': { value: 'db.query', type: 'string' },
55+
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
56+
'db.system.name': { value: 'redis', type: 'string' },
57+
'db.operation.name': { value: 'get', type: 'string' },
58+
'db.query.text': { value: 'get page-key', type: 'string' },
59+
}),
5460
}),
55-
}),
56-
);
61+
);
62+
}
5763
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const isVinext = !!process.env.VINEXT;

‎dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
waitForStreamedSpan,
88
} from '@sentry-internal/test-utils';
99
import { isDevMode } from './isDevMode';
10+
import { isVinext } from './isVinext';
1011

1112
test('Should create a span for middleware', async ({ request }) => {
1213
test.skip(getRuntime() === 'cloudflare', 'On Workers the middleware span is a child of the request span');
@@ -120,6 +121,10 @@ test('Faulty middlewares inside the request segment of the Worker', async ({ req
120121

121122
test('Should trace outgoing fetch requests inside middleware', async ({ request }) => {
122123
test.skip(isDevMode, 'The fetch requests ends up in a separate tx in dev atm');
124+
test.skip(
125+
isVinext && getRuntime() === 'bun',
126+
'vinext replaces `globalThis.fetch` with a wrapper of the `fetch` from before `Sentry.init`, so `@sentry/bun` creates no fetch spans',
127+
);
123128

124129
// In some builds (especially webpack) the fetch span is not a child of the middleware segment but a
125130
// segment of its own, so this waits for either. `http.client` span names are low cardinality under

‎dev-packages/e2e-tests/test-applications/nextjs-16/tests/openai.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForStreamedSpans } from '@sentry-internal/test-utils';
3+
import { isVinext } from './isVinext';
4+
5+
test.skip(isVinext, 'vinext does not run the webpack or Turbopack loader that injects the OpenAI channel');
36

47
// gen_ai spans are emitted as a separate span-v2 envelope item (not inline on the transaction), so we
58
// assert on the streamed spans. Attribute values are wrapped as `{ value, type }` in the v2 format.

‎dev-packages/e2e-tests/test-applications/nextjs-16/tests/propagation.test.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { expect, test } from '@playwright/test';
2-
import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
2+
import { collectStreamedSpans, getRuntime, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
3+
import { isVinext } from './isVinext';
34

45
test('Propagates trace for outgoing fetch requests', async ({ baseURL, request }) => {
6+
test.skip(
7+
isVinext && getRuntime() === 'bun',
8+
'vinext replaces `globalThis.fetch` with a wrapper of the `fetch` from before `Sentry.init`, so `@sentry/bun` creates no fetch spans',
9+
);
10+
511
// Inbound span, outbound span and the http.client span in between all share one trace, and
612
// `collectStreamedSpans` evaluates a single trace at a time, so requiring all three together
713
// keeps them paired.
@@ -47,6 +53,11 @@ test('Does not propagate outgoing fetch requests not covered by tracePropagation
4753
baseURL,
4854
request,
4955
}) => {
56+
test.skip(
57+
isVinext && getRuntime() === 'bun',
58+
'vinext replaces `globalThis.fetch` with a wrapper of the `fetch` from before `Sentry.init`, so nothing propagates and the test would check nothing',
59+
);
60+
5061
// These two spans are deliberately in different traces, so they are matched by their unique names.
5162
const inboundSpanPromise = waitForStreamedSpan('nextjs-16', span => {
5263
return span.name === 'GET /propagation/test-outgoing-fetch-external-disallowed/check' && span.is_segment;

0 commit comments

Comments
 (0)