Skip to content

Commit be78108

Browse files
committed
feat(nextjs): Trace pageloads in App Router
1 parent eec0687 commit be78108

22 files changed

+409
-20
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,7 @@ jobs:
10071007
'node-express-esm-without-loader',
10081008
'nextjs-app-dir',
10091009
'nextjs-14',
1010+
'nextjs-15',
10101011
'react-create-hash-router',
10111012
'react-router-6-use-routes',
10121013
'react-router-5',
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
.pnpm-debug.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts
37+
38+
!*.d.ts
39+
40+
# Sentry
41+
.sentryclirc
42+
43+
.vscode
44+
45+
test-results
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
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: 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('http://example.com/')).text();
10+
11+
return {
12+
title: 'my title',
13+
};
14+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
interface Window {
2+
recordedTransactions?: string[];
3+
capturedExceptionId?: string;
4+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export async function register() {
2+
if (process.env.NEXT_RUNTIME === 'nodejs') {
3+
await import('./sentry.server.config');
4+
}
5+
6+
if (process.env.NEXT_RUNTIME === 'edge') {
7+
await import('./sentry.edge.config');
8+
}
9+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/image-types/global" />
3+
4+
// NOTE: This file should not be edited
5+
// see https://nextjs.org/docs/basic-features/typescript for more information.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const { withSentryConfig } = require('@sentry/nextjs');
2+
3+
/** @type {import('next').NextConfig} */
4+
const nextConfig = {};
5+
6+
module.exports = withSentryConfig(nextConfig, {
7+
silent: true,
8+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"name": "create-next-app",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)",
7+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
8+
"test:prod": "TEST_ENV=production playwright test",
9+
"test:dev": "TEST_ENV=development playwright test",
10+
"test:build": "pnpm install && npx playwright install && pnpm build",
11+
"test:build-canary": "pnpm install && pnpm add next@canary && npx playwright install && pnpm build",
12+
"test:build-latest": "pnpm install && pnpm add next@latest && npx playwright install && pnpm build",
13+
"test:assert": "pnpm test:prod && pnpm test:dev"
14+
},
15+
"dependencies": {
16+
"@playwright/test": "^1.27.1",
17+
"@sentry/nextjs": "latest || *",
18+
"@types/node": "18.11.17",
19+
"@types/react": "18.0.26",
20+
"@types/react-dom": "18.0.9",
21+
"next": "14.3.0-canary.73",
22+
"react": "beta",
23+
"react-dom": "beta",
24+
"typescript": "4.9.5",
25+
"wait-port": "1.0.4"
26+
},
27+
"devDependencies": {
28+
"@sentry-internal/event-proxy-server": "link:../../../event-proxy-server",
29+
"@sentry-internal/feedback": "latest || *",
30+
"@sentry-internal/replay-canvas": "latest || *",
31+
"@sentry-internal/browser-utils": "latest || *",
32+
"@sentry/browser": "latest || *",
33+
"@sentry/core": "latest || *",
34+
"@sentry/nextjs": "latest || *",
35+
"@sentry/node": "latest || *",
36+
"@sentry/opentelemetry": "latest || *",
37+
"@sentry/react": "latest || *",
38+
"@sentry-internal/replay": "latest || *",
39+
"@sentry/types": "latest || *",
40+
"@sentry/utils": "latest || *",
41+
"@sentry/vercel-edge": "latest || *"
42+
},
43+
"volta": {
44+
"extends": "../../package.json"
45+
}
46+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import os from 'os';
2+
import type { PlaywrightTestConfig } from '@playwright/test';
3+
import { devices } from '@playwright/test';
4+
5+
// Fix urls not resolving to localhost on Node v17+
6+
// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575
7+
import { setDefaultResultOrder } from 'dns';
8+
setDefaultResultOrder('ipv4first');
9+
10+
const testEnv = process.env.TEST_ENV;
11+
12+
if (!testEnv) {
13+
throw new Error('No test env defined');
14+
}
15+
16+
const nextPort = 3030;
17+
const eventProxyPort = 3031;
18+
19+
/**
20+
* See https://playwright.dev/docs/test-configuration.
21+
*/
22+
const config: PlaywrightTestConfig = {
23+
testDir: './tests',
24+
/* Maximum time one test can run for. */
25+
timeout: 30_000,
26+
expect: {
27+
/**
28+
* Maximum time expect() should wait for the condition to be met.
29+
* For example in `await expect(locator).toHaveText();`
30+
*/
31+
timeout: 10000,
32+
},
33+
/* Run tests in files in parallel */
34+
fullyParallel: true,
35+
/* Defaults to half the number of CPUs. The tests are not really CPU-bound but rather I/O-bound with all the polling we do so we increase the concurrency to the CPU count. */
36+
workers: os.cpus().length,
37+
/* Fail the build on CI if you accidentally left test.only in the source code. */
38+
forbidOnly: !!process.env.CI,
39+
/* `next dev` is incredibly buggy with the app dir */
40+
retries: testEnv === 'development' ? 3 : 0,
41+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
42+
reporter: 'list',
43+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
44+
use: {
45+
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
46+
actionTimeout: 0,
47+
/* Base URL to use in actions like `await page.goto('/')`. */
48+
baseURL: `http://localhost:${nextPort}`,
49+
trace: 'retain-on-failure',
50+
},
51+
52+
/* Configure projects for major browsers */
53+
projects: [
54+
{
55+
name: 'chromium',
56+
use: {
57+
...devices['Desktop Chrome'],
58+
},
59+
},
60+
],
61+
62+
/* Run your local dev server before starting the tests */
63+
webServer: [
64+
{
65+
command: 'node start-event-proxy.mjs',
66+
port: eventProxyPort,
67+
},
68+
{
69+
command:
70+
testEnv === 'development'
71+
? `pnpm wait-port ${eventProxyPort} && pnpm next dev -p ${nextPort}`
72+
: `pnpm wait-port ${eventProxyPort} && pnpm next start -p ${nextPort}`,
73+
port: nextPort,
74+
stdout: 'pipe',
75+
stderr: 'pipe',
76+
},
77+
],
78+
};
79+
80+
export default config;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
3+
Sentry.init({
4+
environment: 'qa', // dynamic sampling bias to keep transactions
5+
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
6+
tunnel: `http://localhost:3031/`, // proxy server
7+
tracesSampleRate: 1.0,
8+
sendDefaultPii: true,
9+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
3+
Sentry.init({
4+
environment: 'qa', // dynamic sampling bias to keep transactions
5+
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
6+
tunnel: `http://localhost:3031/`, // proxy server
7+
tracesSampleRate: 1.0,
8+
sendDefaultPii: true,
9+
transportOptions: {
10+
// We are doing a lot of events at once in this test
11+
bufferSize: 1000,
12+
},
13+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
3+
Sentry.init({
4+
environment: 'qa', // dynamic sampling bias to keep transactions
5+
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
6+
tunnel: `http://localhost:3031/`, // proxy server
7+
tracesSampleRate: 1.0,
8+
sendDefaultPii: true,
9+
transportOptions: {
10+
// We are doing a lot of events at once in this test
11+
bufferSize: 1000,
12+
},
13+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer } from '@sentry-internal/event-proxy-server';
2+
3+
startEventProxyServer({
4+
port: 3031,
5+
proxyServerName: 'nextjs-15',
6+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForTransaction } from '@sentry-internal/event-proxy-server';
3+
4+
test('all server component transactions should be attached to the pageload request span', async ({ page }) => {
5+
const pageServerComponentTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
6+
return transactionEvent?.transaction === 'Page Server Component (/pageload-tracing)';
7+
});
8+
9+
const layoutServerComponentTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
10+
return transactionEvent?.transaction === 'Layout Server Component (/pageload-tracing)';
11+
});
12+
13+
const metadataTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
14+
return transactionEvent?.transaction === 'Page.generateMetadata (/pageload-tracing)';
15+
});
16+
17+
const pageloadTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
18+
return transactionEvent?.transaction === '/pageload-tracing';
19+
});
20+
21+
await page.goto(`/pageload-tracing`);
22+
23+
const [pageServerComponentTransaction, layoutServerComponentTransaction, metadataTransaction, pageloadTransaction] =
24+
await Promise.all([
25+
pageServerComponentTransactionPromise,
26+
layoutServerComponentTransactionPromise,
27+
metadataTransactionPromise,
28+
pageloadTransactionPromise,
29+
]);
30+
31+
const pageloadTraceId = pageloadTransaction.contexts?.trace?.trace_id;
32+
33+
expect(pageloadTraceId).toBeTruthy();
34+
expect(pageServerComponentTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId);
35+
expect(layoutServerComponentTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId);
36+
expect(metadataTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId);
37+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2018",
4+
"lib": ["dom", "dom.iterable", "esnext"],
5+
"allowJs": true,
6+
"skipLibCheck": true,
7+
"strict": true,
8+
"forceConsistentCasingInFileNames": true,
9+
"noEmit": true,
10+
"esModuleInterop": true,
11+
"module": "esnext",
12+
"moduleResolution": "node",
13+
"resolveJsonModule": true,
14+
"isolatedModules": true,
15+
"jsx": "preserve",
16+
"plugins": [
17+
{
18+
"name": "next"
19+
}
20+
],
21+
"incremental": true
22+
},
23+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js", ".next/types/**/*.ts"],
24+
"exclude": ["node_modules", "playwright.config.ts"]
25+
}

0 commit comments

Comments
 (0)