Skip to content

Commit 4b95c04

Browse files
lforstAbhiPrasad
andauthored
feat(nextjs): Add performance monitoring to server components (#7242)
Co-authored-by: Abhijeet Prasad <[email protected]>
1 parent 87e919c commit 4b95c04

34 files changed

+935
-61
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://localhost:4873
2+
@sentry-internal:registry=http://localhost:4873
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export default function Head() {
2+
return (
3+
<>
4+
<title>Create Next App</title>
5+
<meta content="width=device-width, initial-scale=1" name="viewport" />
6+
<meta name="description" content="Generated by create next app" />
7+
<link rel="icon" href="/favicon.ico" />
8+
</>
9+
);
10+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function RootLayout({ children }: { children: React.ReactNode }) {
2+
return (
3+
<html lang="en">
4+
<body>{children}</body>
5+
</html>
6+
);
7+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use client';
2+
3+
import * as Sentry from '@sentry/nextjs';
4+
import Link from 'next/link';
5+
6+
export default function Home() {
7+
return (
8+
<main>
9+
<input
10+
type="button"
11+
value="Capture Exception"
12+
id="exception-button"
13+
onClick={() => {
14+
Sentry.captureException(new Error('I am a click error!'));
15+
}}
16+
/>
17+
<Link href="/user/5" id="navigation">
18+
navigate
19+
</Link>
20+
</main>
21+
);
22+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default async function Home() {
2+
const dynamid = await (await fetch('http://example.com', { cache: 'no-store' })).text(); // do a fetch request so that this server component is always rendered when requested
3+
return <p>I am a blank page :) {dynamid}</p>;
4+
}
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/image-types/global" />
3+
/// <reference types="next/navigation-types/navigation" />
4+
5+
// NOTE: This file should not be edited
6+
// see https://nextjs.org/docs/basic-features/typescript for more information.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// This file sets a custom webpack configuration to use your Next.js app
2+
// with Sentry.
3+
// https://nextjs.org/docs/api-reference/next.config.js/introduction
4+
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
5+
6+
const { withSentryConfig } = require('@sentry/nextjs');
7+
8+
const moduleExports = {
9+
experimental: {
10+
appDir: true,
11+
},
12+
};
13+
14+
const sentryWebpackPluginOptions = {
15+
// Additional config options for the Sentry Webpack plugin. Keep in mind that
16+
// the following options are set automatically, and overriding them is not
17+
// recommended:
18+
// release, url, org, project, authToken, configFile, stripPrefix,
19+
// urlPrefix, include, ignore
20+
21+
silent: true, // Suppresses all logs
22+
// For all available options, see:
23+
// https://github.com/getsentry/sentry-webpack-plugin#options.
24+
25+
// We're not testing source map uploads at the moment.
26+
dryRun: true,
27+
};
28+
29+
// Make sure adding Sentry options is the last code to run before exporting, to
30+
// ensure that your source maps include changes from all other Webpack plugins
31+
module.exports = withSentryConfig(moduleExports, sentryWebpackPluginOptions, {
32+
hideSourceMaps: true,
33+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "create-next-app",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "next dev",
7+
"build": "next build",
8+
"start": "next start",
9+
"lint": "next lint",
10+
"test": "playwright test"
11+
},
12+
"dependencies": {
13+
"@next/font": "13.0.7",
14+
"@sentry/nextjs": "*",
15+
"@types/node": "18.11.17",
16+
"@types/react": "18.0.26",
17+
"@types/react-dom": "18.0.9",
18+
"next": "13.2.1",
19+
"react": "18.2.0",
20+
"react-dom": "18.2.0",
21+
"typescript": "4.9.4"
22+
},
23+
"devDependencies": {
24+
"ts-node": "10.9.1",
25+
"@playwright/test": "^1.27.1"
26+
}
27+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { PlaywrightTestConfig } from '@playwright/test';
2+
import { devices } from '@playwright/test';
3+
4+
/**
5+
* See https://playwright.dev/docs/test-configuration.
6+
*/
7+
const config: PlaywrightTestConfig = {
8+
testDir: './tests',
9+
/* Maximum time one test can run for. */
10+
timeout: 60 * 1000,
11+
expect: {
12+
/**
13+
* Maximum time expect() should wait for the condition to be met.
14+
* For example in `await expect(locator).toHaveText();`
15+
*/
16+
timeout: 5000,
17+
},
18+
/* Run tests in files in parallel */
19+
fullyParallel: true,
20+
/* Fail the build on CI if you accidentally left test.only in the source code. */
21+
forbidOnly: !!process.env.CI,
22+
/* Retry on CI only */
23+
retries: 0,
24+
/* Opt out of parallel tests on CI. */
25+
workers: 1,
26+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
27+
reporter: 'dot',
28+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
29+
use: {
30+
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
31+
actionTimeout: 0,
32+
/* Base URL to use in actions like `await page.goto('/')`. */
33+
baseURL: 'http://localhost:3000',
34+
35+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
36+
trace: 'on-first-retry',
37+
},
38+
39+
/* Configure projects for major browsers */
40+
projects: [
41+
{
42+
name: 'chromium',
43+
use: {
44+
...devices['Desktop Chrome'],
45+
},
46+
},
47+
],
48+
49+
/* Run your local dev server before starting the tests */
50+
webServer: [
51+
{
52+
command: 'yarn start',
53+
port: 3000,
54+
},
55+
{
56+
command: 'yarn ts-node-script start-event-proxy.ts',
57+
port: 27496,
58+
},
59+
],
60+
};
61+
62+
export default config;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
3+
Sentry.init({
4+
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
5+
tunnel: 'http://localhost:27496/', // proxy server
6+
tracesSampleRate: 1.0,
7+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
3+
Sentry.init({
4+
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
5+
tunnel: 'http://localhost:27496/', // proxy server
6+
tracesSampleRate: 1.0,
7+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
3+
Sentry.init({
4+
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
5+
tunnel: 'http://localhost:27496/', // proxy server
6+
tracesSampleRate: 1.0,
7+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer, waitForTransaction } from '../../test-utils/event-proxy-server';
2+
3+
startEventProxyServer({
4+
port: 27496,
5+
proxyServerName: 'nextjs-13-app-dir',
6+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "../../test-recipe-schema.json",
3+
"testApplicationName": "nextjs-13-app-dir",
4+
"buildCommand": "yarn install --pure-lockfile && npx playwright install && yarn build",
5+
"tests": [
6+
{
7+
"testName": "Playwright tests",
8+
"testCommand": "yarn test"
9+
}
10+
]
11+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { test, expect } from '@playwright/test';
2+
import { waitForError } from '../../../test-utils/event-proxy-server';
3+
import axios, { AxiosError } from 'axios';
4+
5+
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
6+
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
7+
const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
8+
const EVENT_POLLING_TIMEOUT = 30_000;
9+
10+
test('Sends a client-side exception to Sentry', async ({ page }) => {
11+
await page.goto('/');
12+
13+
const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
14+
return errorEvent?.exception?.values?.[0]?.value === 'I am a click error!';
15+
});
16+
17+
const exceptionButton = page.locator('id=exception-button');
18+
await exceptionButton.click();
19+
20+
const errorEvent = await errorEventPromise;
21+
const exceptionEventId = errorEvent.event_id;
22+
23+
await expect
24+
.poll(
25+
async () => {
26+
try {
27+
const response = await axios.get(
28+
`https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`,
29+
{ headers: { Authorization: `Bearer ${authToken}` } },
30+
);
31+
32+
return response.status;
33+
} catch (e) {
34+
if (e instanceof AxiosError && e.response) {
35+
if (e.response.status !== 404) {
36+
throw e;
37+
} else {
38+
return e.response.status;
39+
}
40+
} else {
41+
throw e;
42+
}
43+
}
44+
},
45+
{
46+
timeout: EVENT_POLLING_TIMEOUT,
47+
},
48+
)
49+
.toBe(200);
50+
});

0 commit comments

Comments
 (0)