Skip to content

Commit d2c8ab0

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 two SDK gaps on vinext: `openai` and `ioredis` get their diagnostics channels from the build-time loader of webpack or Turbopack, which vinext does not run. 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 f3f2a5f commit d2c8ab0

29 files changed

Lines changed: 348 additions & 30 deletions

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@
2323
"test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack",
2424
"test:assert:bun": "RUNTIME=bun pnpm test:prod",
2525
"test:assert:deno": "RUNTIME=deno pnpm test:prod",
26-
"test:build:cloudflare": "pnpm install && opennextjs-cloudflare build",
27-
"test:build-latest:cloudflare": "pnpm install && pnpm add next@latest && opennextjs-cloudflare build",
26+
"test:build:cloudflare": "pnpm install && opennextjs-cloudflare build --config wrangler.opennext.jsonc",
27+
"test:build-latest:cloudflare": "pnpm install && pnpm add next@latest && opennextjs-cloudflare build --config wrangler.opennext.jsonc",
2828
"test:assert:cloudflare": "RUNTIME=cloudflare pnpm test:prod",
29-
"start:cloudflare": "opennextjs-cloudflare preview --var \"E2E_TEST_DSN:$E2E_TEST_DSN\""
29+
"start:cloudflare": "opennextjs-cloudflare preview --config wrangler.opennext.jsonc --var \"E2E_TEST_DSN:$E2E_TEST_DSN\"",
30+
"test:build:vinext": "pnpm install && pnpm add react@19.3.0 react-dom@19.3.0 && npm pkg set type=module && vinext build",
31+
"test:assert:vinext": "VINEXT=1 pnpm test:prod",
32+
"test:assert:vinext:bun": "VINEXT=1 RUNTIME=bun pnpm test:prod"
3033
},
3134
"//": "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`.",
3235
"dependencies": {
@@ -56,7 +59,12 @@
5659
"@types/react-dom": "^19",
5760
"eslint": "^9",
5861
"eslint-config-next": "^16",
62+
"@vitejs/plugin-react": "^6.1.1",
63+
"@vitejs/plugin-rsc": "^0.5.35",
64+
"react-server-dom-webpack": "^19.3.0",
5965
"typescript": "^5",
66+
"vinext": "1.0.0-beta.13",
67+
"vite": "^8.3.0",
6068
"wrangler": "^4.61.0"
6169
},
6270
"volta": {
@@ -108,6 +116,16 @@
108116
"assert-command": "pnpm test:assert:deno",
109117
"deno-version": "v2.9.0",
110118
"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+
"build-command": "pnpm test:build:vinext",
127+
"assert-command": "pnpm test:assert:vinext:bun",
128+
"label": "nextjs-16 (vinext, bun)"
111129
}
112130
]
113131
}

‎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') });
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');
@@ -109,6 +110,10 @@ test('Faulty middlewares inside the request segment of the Worker', async ({ req
109110

110111
test('Should trace outgoing fetch requests inside middleware', async ({ request }) => {
111112
test.skip(isDevMode, 'The fetch requests ends up in a separate tx in dev atm');
113+
test.skip(
114+
isVinext && getRuntime() === 'bun',
115+
'vinext replaces `globalThis.fetch` with a wrapper of the `fetch` from before `Sentry.init`, so `@sentry/bun` creates no fetch spans',
116+
);
112117

113118
// In some builds (especially webpack) the fetch span is not a child of the middleware segment but a
114119
// 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: 7 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.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import vinext from 'vinext';
2+
import { defineConfig } from 'vite';
3+
4+
// Only for the `nextjs-16 (vinext)` variants, which build this app with vinext instead of Next.js. Their build command
5+
// sets `"type": "module"`, because `vinext start` does not find the `.mjs` server files that Vite writes otherwise, and
6+
// React 19.3, because vinext needs React 19.2.6 or newer.
7+
export default defineConfig({ plugins: [vinext()] });

dev-packages/e2e-tests/test-applications/nextjs-16/wrangler.jsonc renamed to dev-packages/e2e-tests/test-applications/nextjs-16/wrangler.opennext.jsonc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Not `wrangler.jsonc`: vinext refuses to build without the Cloudflare plugin when the project root has a wrangler
2+
// config, and the `nextjs-16 (vinext)` variants build this app for Node.js and Bun.
13
{
24
"$schema": "node_modules/wrangler/config-schema.json",
35
"name": "nextjs-16",

0 commit comments

Comments
 (0)