Skip to content

Commit

Permalink
feat(nextjs): Trace pageloads in App Router
Browse files Browse the repository at this point in the history
  • Loading branch information
lforst committed May 23, 2024
1 parent eec0687 commit 1af861f
Show file tree
Hide file tree
Showing 22 changed files with 405 additions and 20 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,7 @@ jobs:
'node-express-esm-without-loader',
'nextjs-app-dir',
'nextjs-14',
'nextjs-15',
'react-create-hash-router',
'react-router-6-use-routes',
'react-router-5',
Expand Down
45 changes: 45 additions & 0 deletions dev-packages/e2e-tests/test-applications/nextjs-15/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

!*.d.ts

# Sentry
.sentryclirc

.vscode

test-results
2 changes: 2 additions & 0 deletions dev-packages/e2e-tests/test-applications/nextjs-15/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@sentry:registry=http://127.0.0.1:4873
@sentry-internal:registry=http://127.0.0.1:4873
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { PropsWithChildren } from 'react';

export const dynamic = 'force-dynamic';

export default async function Layout({ children }: PropsWithChildren<unknown>) {
await new Promise(resolve => setTimeout(resolve, 500));
return <>{children}</>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const dynamic = 'force-dynamic';

export default async function Page() {
await new Promise(resolve => setTimeout(resolve, 1000));
return <p>I am page 2</p>;
}

export async function generateMetadata() {
(await fetch('http://example.com/')).text();

return {
title: 'my title',
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface Window {
recordedTransactions?: string[];
capturedExceptionId?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
}

if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { withSentryConfig } = require('@sentry/nextjs');

/** @type {import('next').NextConfig} */
const nextConfig = {};

module.exports = withSentryConfig(nextConfig, {
silent: true,
});
46 changes: 46 additions & 0 deletions dev-packages/e2e-tests/test-applications/nextjs-15/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "create-next-app",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:prod": "TEST_ENV=production playwright test",
"test:dev": "TEST_ENV=development playwright test",
"test:build": "pnpm install && npx playwright install && pnpm build",
"test:build-canary": "pnpm install && pnpm add next@canary && npx playwright install && pnpm build",
"test:build-latest": "pnpm install && pnpm add next@latest && npx playwright install && pnpm build",
"test:assert": "pnpm test:prod && pnpm test:dev"
},
"dependencies": {
"@playwright/test": "^1.27.1",
"@sentry/nextjs": "latest || *",
"@types/node": "18.11.17",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
"next": "14.3.0-canary.73",
"react": "beta",
"react-dom": "beta",
"typescript": "4.9.5",
"wait-port": "1.0.4"
},
"devDependencies": {
"@sentry-internal/event-proxy-server": "link:../../../event-proxy-server",
"@sentry-internal/feedback": "latest || *",
"@sentry-internal/replay-canvas": "latest || *",
"@sentry-internal/browser-utils": "latest || *",
"@sentry/browser": "latest || *",
"@sentry/core": "latest || *",
"@sentry/nextjs": "latest || *",
"@sentry/node": "latest || *",
"@sentry/opentelemetry": "latest || *",
"@sentry/react": "latest || *",
"@sentry-internal/replay": "latest || *",
"@sentry/types": "latest || *",
"@sentry/utils": "latest || *",
"@sentry/vercel-edge": "latest || *"
},
"volta": {
"extends": "../../package.json"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import os from 'os';
import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test';

// Fix urls not resolving to localhost on Node v17+
// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575
import { setDefaultResultOrder } from 'dns';
setDefaultResultOrder('ipv4first');

const testEnv = process.env.TEST_ENV;

if (!testEnv) {
throw new Error('No test env defined');
}

const nextPort = 3030;
const eventProxyPort = 3031;

/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
timeout: 30_000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 10000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* 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. */
workers: os.cpus().length,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* `next dev` is incredibly buggy with the app dir */
retries: testEnv === 'development' ? 3 : 0,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'list',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: `http://localhost:${nextPort}`,
trace: 'retain-on-failure',
},

/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],

/* Run your local dev server before starting the tests */
webServer: [
{
command: 'node start-event-proxy.mjs',
port: eventProxyPort,
},
{
command:
testEnv === 'development'
? `pnpm wait-port ${eventProxyPort} && pnpm next dev -p ${nextPort}`
: `pnpm wait-port ${eventProxyPort} && pnpm next start -p ${nextPort}`,
port: nextPort,
stdout: 'pipe',
stderr: 'pipe',
},
],
};

export default config;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/nextjs';

Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
sendDefaultPii: true,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as Sentry from '@sentry/nextjs';

Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
sendDefaultPii: true,
transportOptions: {
// We are doing a lot of events at once in this test
bufferSize: 1000,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as Sentry from '@sentry/nextjs';

Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
sendDefaultPii: true,
transportOptions: {
// We are doing a lot of events at once in this test
bufferSize: 1000,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { startEventProxyServer } from '@sentry-internal/event-proxy-server';

startEventProxyServer({
port: 3031,
proxyServerName: 'nextjs-15',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/event-proxy-server';

test('all server component transactions should be attached to the pageload request span', async ({ page }) => {
const pageServerComponentTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
return transactionEvent?.transaction === 'Page Server Component (/pageload-tracing)';
});

const layoutServerComponentTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
return transactionEvent?.transaction === 'Layout Server Component (/pageload-tracing)';
});

const metadataTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
return transactionEvent?.transaction === 'Page.generateMetadata (/pageload-tracing)';
});

const pageloadTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
return transactionEvent?.transaction === '/pageload-tracing';
});

await page.goto(`/pageload-tracing`);

const [pageServerComponentTransaction, layoutServerComponentTransaction, metadataTransaction, pageloadTransaction] =
await Promise.all([
pageServerComponentTransactionPromise,
layoutServerComponentTransactionPromise,
metadataTransactionPromise,
pageloadTransactionPromise,
]);

const pageloadTraceId = pageloadTransaction.contexts?.trace?.trace_id;

expect(pageloadTraceId).toBeTruthy();
expect(pageServerComponentTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId);
expect(layoutServerComponentTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId);
expect(metadataTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId);
});
25 changes: 25 additions & 0 deletions dev-packages/e2e-tests/test-applications/nextjs-15/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es2018",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
],
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js", ".next/types/**/*.ts"],
"exclude": ["node_modules", "playwright.config.ts"]
}
Loading

0 comments on commit 1af861f

Please sign in to comment.