Skip to content

Commit 1efd6e7

Browse files
chargomeclaude
andauthored
test(e2e): Add a static trace lifecycle Next.js 16 test app (#23834)
Adds `nextjs-16-static`, a copy of `nextjs-16` that keeps the `traceLifecycle: 'static'` pins and the transaction-based specs, so the static trace lifecycle keeps end-to-end coverage once the main Next.js suites move to span streaming. Ref #23802 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 2c64580 commit 1efd6e7

96 files changed

Lines changed: 3069 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
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: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { createServer } from 'node:http';
2+
3+
// A single mock server standing in for the OpenAI, Anthropic and Google GenAI HTTP APIs, so the real
4+
// SDK clients emit gen_ai spans without any live credentials. Response bodies mirror the mock servers
5+
// in the node-integration tests (suites/tracing/{openai,anthropic,google-genai}). Uses raw `node:http`
6+
// (not express) so the mock doesn't itself get instrumented.
7+
8+
function readJson(req) {
9+
return new Promise(resolve => {
10+
let body = '';
11+
req.on('data', chunk => (body += chunk));
12+
req.on('end', () => {
13+
try {
14+
resolve(JSON.parse(body || '{}'));
15+
} catch {
16+
resolve({});
17+
}
18+
});
19+
});
20+
}
21+
22+
function sendJson(res, status, obj) {
23+
res.writeHead(status, { 'Content-Type': 'application/json' });
24+
res.end(JSON.stringify(obj));
25+
}
26+
27+
let serverPromise;
28+
29+
/** Lazily starts the shared mock server and resolves to its port. */
30+
export function getMockAiPort() {
31+
serverPromise ??= new Promise(resolve => {
32+
const server = createServer(async (req, res) => {
33+
const url = req.url || '';
34+
35+
// OpenAI: chat completions
36+
if (req.method === 'POST' && url.endsWith('/openai/chat/completions')) {
37+
const { model } = await readJson(req);
38+
sendJson(res, 200, {
39+
id: 'chatcmpl-mock123',
40+
object: 'chat.completion',
41+
created: 1677652288,
42+
model,
43+
choices: [
44+
{ index: 0, message: { role: 'assistant', content: 'Hello from OpenAI mock!' }, finish_reason: 'stop' },
45+
],
46+
usage: { prompt_tokens: 10, completion_tokens: 15, total_tokens: 25 },
47+
});
48+
return;
49+
}
50+
51+
// Anthropic: messages
52+
if (req.method === 'POST' && url.endsWith('/anthropic/v1/messages')) {
53+
const { model } = await readJson(req);
54+
sendJson(res, 200, {
55+
id: 'msg_mock123',
56+
type: 'message',
57+
model,
58+
role: 'assistant',
59+
content: [{ type: 'text', text: 'Hello from Anthropic mock!' }],
60+
stop_reason: 'end_turn',
61+
stop_sequence: null,
62+
usage: { input_tokens: 10, output_tokens: 15 },
63+
});
64+
return;
65+
}
66+
67+
// Google GenAI: generateContent (the model name is embedded in the path before `:generateContent`).
68+
// Plain string checks avoid the polynomial-backtracking risk of a `.+` regex on the URL.
69+
if (req.method === 'POST' && url.startsWith('/v1beta/models/') && url.endsWith(':generateContent')) {
70+
await readJson(req);
71+
sendJson(res, 200, {
72+
candidates: [
73+
{
74+
content: { parts: [{ text: 'Mock response from Google GenAI!' }], role: 'model' },
75+
finishReason: 'stop',
76+
index: 0,
77+
},
78+
],
79+
usageMetadata: { promptTokenCount: 8, candidatesTokenCount: 12, totalTokenCount: 20 },
80+
});
81+
return;
82+
}
83+
84+
res.writeHead(404).end();
85+
});
86+
87+
server.listen(0, () => {
88+
resolve(server.address().port);
89+
});
90+
});
91+
92+
return serverPromise;
93+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { PropsWithChildren } from 'react';
2+
3+
export const dynamic = 'force-dynamic';
4+
5+
export default function Layout({ children }: PropsWithChildren<{}>) {
6+
return (
7+
<div>
8+
<p>Layout</p>
9+
{children}
10+
</div>
11+
);
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { PropsWithChildren } from 'react';
2+
3+
export const dynamic = 'force-dynamic';
4+
5+
export default function Layout({ children }: PropsWithChildren<{}>) {
6+
return (
7+
<div>
8+
<p>DynamicLayout</p>
9+
{children}
10+
</div>
11+
);
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const dynamic = 'force-dynamic';
2+
3+
export default async function Page() {
4+
return (
5+
<div>
6+
<p>Dynamic Page</p>
7+
</div>
8+
);
9+
}
10+
11+
export async function generateMetadata() {
12+
return {
13+
title: 'I am dynamic page generated metadata',
14+
};
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { PropsWithChildren } from 'react';
2+
3+
export const dynamic = 'force-dynamic';
4+
5+
export default function Layout({ children }: PropsWithChildren<{}>) {
6+
return (
7+
<div>
8+
<p>Layout</p>
9+
{children}
10+
</div>
11+
);
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const dynamic = 'force-dynamic';
2+
3+
export default function Page() {
4+
return <p>Hello World!</p>;
5+
}
6+
7+
export async function generateMetadata() {
8+
return {
9+
title: 'I am generated metadata',
10+
};
11+
}
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 by default
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+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const dynamic = 'force-dynamic';
2+
3+
export async function GET() {
4+
throw new Error('Cron job error');
5+
}

0 commit comments

Comments
 (0)