Skip to content

Commit 6b83234

Browse files
authored
test(nextjs): Add next@16 e2e test (#17922)
- adds a new next 16 test - removes canary testing for app dir and pages dir tests (this is becoming too cumbersome to maintain across canary/bundler versions etc. – will instead move these tests into next 16 going forward) Note that next@canary will now always resolve to next@16
1 parent da08d49 commit 6b83234

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1174
-26
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
16+
# next.js
17+
/.next/
18+
/out/
19+
20+
# production
21+
/build
22+
23+
# misc
24+
.DS_Store
25+
*.pem
26+
27+
# debug
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
.pnpm-debug.log*
32+
33+
# env files (can opt-in for committing if needed)
34+
.env*
35+
36+
# vercel
37+
.vercel
38+
39+
# typescript
40+
*.tsbuildinfo
41+
next-env.d.ts
42+
43+
# Sentry Config File
44+
.env.sentry-build-plugin
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { generateText } from 'ai';
2+
import { MockLanguageModelV1 } from 'ai/test';
3+
import { z } from 'zod';
4+
import * as Sentry from '@sentry/nextjs';
5+
6+
export const dynamic = 'force-dynamic';
7+
8+
// Error trace handling in tool calls
9+
async function runAITest() {
10+
const result = await generateText({
11+
experimental_telemetry: { isEnabled: true },
12+
model: new MockLanguageModelV1({
13+
doGenerate: async () => ({
14+
rawCall: { rawPrompt: null, rawSettings: {} },
15+
finishReason: 'tool-calls',
16+
usage: { promptTokens: 15, completionTokens: 25 },
17+
text: 'Tool call completed!',
18+
toolCalls: [
19+
{
20+
toolCallType: 'function',
21+
toolCallId: 'call-1',
22+
toolName: 'getWeather',
23+
args: '{ "location": "San Francisco" }',
24+
},
25+
],
26+
}),
27+
}),
28+
tools: {
29+
getWeather: {
30+
parameters: z.object({ location: z.string() }),
31+
execute: async args => {
32+
throw new Error('Tool call failed');
33+
},
34+
},
35+
},
36+
prompt: 'What is the weather in San Francisco?',
37+
});
38+
}
39+
40+
export default async function Page() {
41+
await Sentry.startSpan({ op: 'function', name: 'ai-error-test' }, async () => {
42+
return await runAITest();
43+
});
44+
45+
return (
46+
<div>
47+
<h1>AI Test Results</h1>
48+
</div>
49+
);
50+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { generateText } from 'ai';
2+
import { MockLanguageModelV1 } from 'ai/test';
3+
import { z } from 'zod';
4+
import * as Sentry from '@sentry/nextjs';
5+
6+
export const dynamic = 'force-dynamic';
7+
8+
async function runAITest() {
9+
// First span - telemetry should be enabled automatically but no input/output recorded when sendDefaultPii: true
10+
const result1 = await generateText({
11+
model: new MockLanguageModelV1({
12+
doGenerate: async () => ({
13+
rawCall: { rawPrompt: null, rawSettings: {} },
14+
finishReason: 'stop',
15+
usage: { promptTokens: 10, completionTokens: 20 },
16+
text: 'First span here!',
17+
}),
18+
}),
19+
prompt: 'Where is the first span?',
20+
});
21+
22+
// Second span - explicitly enabled telemetry, should record inputs/outputs
23+
const result2 = await generateText({
24+
experimental_telemetry: { isEnabled: true },
25+
model: new MockLanguageModelV1({
26+
doGenerate: async () => ({
27+
rawCall: { rawPrompt: null, rawSettings: {} },
28+
finishReason: 'stop',
29+
usage: { promptTokens: 10, completionTokens: 20 },
30+
text: 'Second span here!',
31+
}),
32+
}),
33+
prompt: 'Where is the second span?',
34+
});
35+
36+
// Third span - with tool calls and tool results
37+
const result3 = await generateText({
38+
model: new MockLanguageModelV1({
39+
doGenerate: async () => ({
40+
rawCall: { rawPrompt: null, rawSettings: {} },
41+
finishReason: 'tool-calls',
42+
usage: { promptTokens: 15, completionTokens: 25 },
43+
text: 'Tool call completed!',
44+
toolCalls: [
45+
{
46+
toolCallType: 'function',
47+
toolCallId: 'call-1',
48+
toolName: 'getWeather',
49+
args: '{ "location": "San Francisco" }',
50+
},
51+
],
52+
}),
53+
}),
54+
tools: {
55+
getWeather: {
56+
parameters: z.object({ location: z.string() }),
57+
execute: async args => {
58+
return `Weather in ${args.location}: Sunny, 72°F`;
59+
},
60+
},
61+
},
62+
prompt: 'What is the weather in San Francisco?',
63+
});
64+
65+
// Fourth span - explicitly disabled telemetry, should not be captured
66+
const result4 = await generateText({
67+
experimental_telemetry: { isEnabled: false },
68+
model: new MockLanguageModelV1({
69+
doGenerate: async () => ({
70+
rawCall: { rawPrompt: null, rawSettings: {} },
71+
finishReason: 'stop',
72+
usage: { promptTokens: 10, completionTokens: 20 },
73+
text: 'Third span here!',
74+
}),
75+
}),
76+
prompt: 'Where is the third span?',
77+
});
78+
79+
return {
80+
result1: result1.text,
81+
result2: result2.text,
82+
result3: result3.text,
83+
result4: result4.text,
84+
};
85+
}
86+
87+
export default async function Page() {
88+
const results = await Sentry.startSpan({ op: 'function', name: 'ai-test' }, async () => {
89+
return await runAITest();
90+
});
91+
92+
return (
93+
<div>
94+
<h1>AI Test Results</h1>
95+
<pre id="ai-results">{JSON.stringify(results, null, 2)}</pre>
96+
</div>
97+
);
98+
}
Binary file not shown.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client';
2+
3+
import * as Sentry from '@sentry/nextjs';
4+
import NextError from 'next/error';
5+
import { useEffect } from 'react';
6+
7+
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
8+
useEffect(() => {
9+
Sentry.captureException(error);
10+
}, [error]);
11+
12+
return (
13+
<html>
14+
<body>
15+
{/* `NextError` is the default Next.js error page component. Its type
16+
definition requires a `statusCode` prop. However, since the App Router
17+
does not expose status codes for errors, we simply pass 0 to render a
18+
generic error message. */}
19+
<NextError statusCode={0} />
20+
</body>
21+
</html>
22+
);
23+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Layout({ children }: { children: React.ReactNode }) {
2+
return (
3+
<html lang="en">
4+
<body>{children}</body>
5+
</html>
6+
);
7+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Suspense } from 'react';
2+
3+
export const dynamic = 'force-dynamic';
4+
5+
export default async function Page() {
6+
return (
7+
<Suspense fallback={<p>Loading...</p>}>
8+
{/* @ts-ignore */}
9+
<Crash />;
10+
</Suspense>
11+
);
12+
}
13+
14+
async function Crash() {
15+
throw new Error('I am technically uncatchable');
16+
return <p>unreachable</p>;
17+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>Next 16 test app</p>;
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { PropsWithChildren } from 'react';
2+
3+
export const dynamic = 'force-dynamic';
4+
5+
export default async function Layout({ children }: PropsWithChildren<unknown>) {
6+
await new Promise(resolve => setTimeout(resolve, 500));
7+
return <>{children}</>;
8+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const dynamic = 'force-dynamic';
2+
3+
export default async function Page() {
4+
await new Promise(resolve => setTimeout(resolve, 1000));
5+
return <p>I am page 2</p>;
6+
}
7+
8+
export async function generateMetadata() {
9+
(await fetch('https://example.com/', { cache: 'no-store' })).text();
10+
11+
return {
12+
title: 'my title',
13+
};
14+
}

0 commit comments

Comments
 (0)