Skip to content

Commit 17b12e1

Browse files
authored
Next.js integration tests for Server and Browser (#3632)
1 parent d712e13 commit 17b12e1

Some content is hidden

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

51 files changed

+3977
-3
lines changed

packages/nextjs/.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module.exports = {
99
ecmaVersion: 2018,
1010
},
1111
extends: ['@sentry-internal/sdk'],
12-
ignorePatterns: ['build/**', 'dist/**', 'esm/**', 'examples/**', 'scripts/**'],
12+
ignorePatterns: ['build/**', 'dist/**', 'esm/**', 'examples/**', 'scripts/**', 'test/integration/**'],
1313
overrides: [
1414
{
1515
files: ['*.ts', '*.tsx', '*.d.ts'],

packages/nextjs/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,13 @@
5252
"fix": "run-s fix:eslint fix:prettier",
5353
"fix:eslint": "eslint . --format stylish --fix",
5454
"fix:prettier": "prettier --write \"{src,test}/**/*.ts\"",
55-
"test": "jest",
55+
"test": "run-s test:unit test:integration",
5656
"test:watch": "jest --watch",
57+
"test:unit": "jest",
58+
"test:integration": "run-s test:integration:build test:integration:server test:integration:client",
59+
"test:integration:build": "cd test/integration && yarn && yarn build && cd ../..",
60+
"test:integration:server": "node test/integration/test/server.js --silent",
61+
"test:integration:client": "node test/integration/test/client.js --silent",
5762
"pack": "npm pack",
5863
"vercel:branch": "source vercel/set-up-branch-for-test-app-use.sh",
5964
"vercel:project": "source vercel/make-project-use-current-branch.sh"

packages/nextjs/src/performance/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
13
import { Primitive, Transaction, TransactionContext } from '@sentry/types';
24
import { fill, getGlobalObject, stripUrlQueryAndFragment } from '@sentry/utils';
35
import { default as Router } from 'next/router';
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
27+
# local env files
28+
.env.local
29+
.env.development.local
30+
.env.test.local
31+
.env.production.local
32+
33+
# vercel
34+
.vercel
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React, { ReactNode } from 'react';
2+
import Link from 'next/link';
3+
import Head from 'next/head';
4+
5+
type Props = {
6+
children?: ReactNode;
7+
title?: string;
8+
};
9+
10+
const Layout = ({ children, title = 'This is the default title' }: Props) => (
11+
<div>
12+
<Head>
13+
<title>{title}</title>
14+
<meta charSet="utf-8" />
15+
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
16+
</Head>
17+
<header>
18+
<nav>
19+
<Link href="/">
20+
<a>Home</a>
21+
</Link>{' '}
22+
|{' '}
23+
<Link href="/about">
24+
<a>About</a>
25+
</Link>{' '}
26+
|{' '}
27+
<Link href="/users">
28+
<a>Users List</a>
29+
</Link>{' '}
30+
| <a href="/api/users">Users API</a>
31+
</nav>
32+
</header>
33+
{children}
34+
<footer>
35+
<hr />
36+
<span>I'm here to stay (Footer)</span>
37+
</footer>
38+
</div>
39+
);
40+
41+
export default Layout;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as React from 'react';
2+
import ListItem from './ListItem';
3+
import { User } from '../interfaces';
4+
5+
type Props = {
6+
items: User[];
7+
};
8+
9+
const List = ({ items }: Props) => (
10+
<ul>
11+
{items.map(item => (
12+
<li key={item.id}>
13+
<ListItem data={item} />
14+
</li>
15+
))}
16+
</ul>
17+
);
18+
19+
export default List;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as React from 'react';
2+
3+
import { User } from '../interfaces';
4+
5+
type ListDetailProps = {
6+
item: User;
7+
};
8+
9+
const ListDetail = ({ item: user }: ListDetailProps) => (
10+
<div>
11+
<h1>Detail for {user.name}</h1>
12+
<p>ID: {user.id}</p>
13+
</div>
14+
);
15+
16+
export default ListDetail;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
import Link from 'next/link';
3+
4+
import { User } from '../interfaces';
5+
6+
type Props = {
7+
data: User;
8+
};
9+
10+
const ListItem = ({ data }: Props) => (
11+
<Link href="/users/[id]" as={`/users/${data.id}`}>
12+
<a>
13+
{data.id}: {data.name}
14+
</a>
15+
</Link>
16+
);
17+
18+
export default ListItem;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// You can include shared interfaces/types in a separate file
2+
// and then use them in any component by importing them. For
3+
// example, to import the interface below do:
4+
//
5+
// import { User } from 'path/to/interfaces';
6+
7+
export type User = {
8+
id: number;
9+
name: string;
10+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/types/global" />
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const { withSentryConfig } = require('@sentry/nextjs');
2+
3+
const moduleExports = {};
4+
const SentryWebpackPluginOptions = {
5+
dryRun: true,
6+
silent: true,
7+
};
8+
9+
module.exports = withSentryConfig(moduleExports, SentryWebpackPluginOptions);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "with-typescript",
3+
"version": "1.0.0",
4+
"scripts": {
5+
"dev": "next",
6+
"build": "next build",
7+
"start": "next start",
8+
"type-check": "tsc"
9+
},
10+
"dependencies": {
11+
"@sentry/nextjs": "file:../../",
12+
"next": "latest",
13+
"react": "^17.0.1",
14+
"react-dom": "^17.0.1"
15+
},
16+
"devDependencies": {
17+
"@types/node": "^15.3.1",
18+
"@types/puppeteer": "^5.4.3",
19+
"@types/react": "^17.0.6",
20+
"@types/react-dom": "^17.0.5",
21+
"nock": "^13.1.0",
22+
"puppeteer": "^9.1.1",
23+
"typescript": "^4.2.4",
24+
"yargs": "^16.2.0"
25+
},
26+
"license": "MIT"
27+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const AboutPage = () => <h1>About</h1>;
2+
3+
export default AboutPage;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Link from 'next/link';
2+
3+
const HealthyPage = (): JSX.Element => (
4+
<Link href="/healthy">
5+
<a id="healthy">Healthy</a>
6+
</Link>
7+
);
8+
9+
export default HealthyPage;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { withSentry } from '@sentry/nextjs';
2+
import { NextApiRequest, NextApiResponse } from 'next';
3+
4+
const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise<void> => {
5+
res.status(500).json({ statusCode: 500, message: 'Something went wrong' });
6+
};
7+
8+
export default withSentry(handler);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { withSentry } from '@sentry/nextjs';
2+
import { NextApiRequest, NextApiResponse } from 'next';
3+
4+
const handler = async (_req: NextApiRequest, _res: NextApiResponse): Promise<void> => {
5+
throw new Error('API Error');
6+
};
7+
8+
export default withSentry(handler);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { withSentry } from '@sentry/nextjs';
2+
import { get } from 'http';
3+
import { NextApiRequest, NextApiResponse } from 'next';
4+
5+
const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise<void> => {
6+
await new Promise(resolve => get('http://example.com', resolve));
7+
res.status(200).json({});
8+
};
9+
10+
export default withSentry(handler);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { withSentry } from '@sentry/nextjs';
2+
import { NextApiRequest, NextApiResponse } from 'next';
3+
4+
import { sampleUserData } from '../../../utils/sample-data';
5+
6+
const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise<void> => {
7+
try {
8+
if (!Array.isArray(sampleUserData)) {
9+
throw new Error('Cannot find user data');
10+
}
11+
12+
res.status(200).json(sampleUserData);
13+
} catch (err) {
14+
res.status(500).json({ statusCode: 500, message: (err as Error).message });
15+
}
16+
};
17+
18+
export default withSentry(handler);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const CrashedPage = (): JSX.Element => {
2+
// Magic to naively trigger onerror to make session crashed and allow for SSR
3+
try {
4+
// @ts-ignore
5+
if (typeof window !== 'undefined' && typeof window.onerror === 'function') {
6+
// Lovely oldschool browsers syntax with 5 arguments <3
7+
// @ts-ignore
8+
window.onerror(null, null, null, null, new Error('Crashed'));
9+
}
10+
} catch (_e) {
11+
// no-empty
12+
}
13+
return <h1>Crashed</h1>;
14+
};
15+
16+
export default CrashedPage;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const ButtonPage = (): JSX.Element => (
2+
<button
3+
onClick={() => {
4+
throw new Error('Sentry Frontend Error');
5+
}}
6+
>
7+
Throw Error
8+
</button>
9+
);
10+
11+
export default ButtonPage;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const ButtonPage = (): JSX.Element => (
2+
<button
3+
onClick={() => {
4+
fetch('http://example.com').catch(() => {
5+
// no-empty
6+
});
7+
}}
8+
>
9+
Send Request
10+
</button>
11+
);
12+
13+
export default ButtonPage;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Link from 'next/link';
2+
3+
const HealthyPage = (): JSX.Element => (
4+
<Link href="/alsoHealthy">
5+
<a id="alsoHealthy">AlsoHealthy</a>
6+
</Link>
7+
);
8+
9+
export default HealthyPage;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const IndexPage = (): JSX.Element => <h1>Hello Next.js</h1>;
2+
3+
export default IndexPage;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { GetStaticProps, GetStaticPaths } from 'next';
2+
3+
import { User } from '../../interfaces';
4+
import { sampleUserData } from '../../utils/sample-data';
5+
import Layout from '../../components/Layout';
6+
import ListDetail from '../../components/ListDetail';
7+
8+
type Props = {
9+
item?: User;
10+
errors?: string;
11+
};
12+
13+
const StaticPropsDetail = ({ item, errors }: Props) => {
14+
if (errors) {
15+
return (
16+
<Layout title="Error | Next.js + TypeScript Example">
17+
<p>
18+
<span style={{ color: 'red' }}>Error:</span> {errors}
19+
</p>
20+
</Layout>
21+
);
22+
}
23+
24+
return (
25+
<Layout title={`${item ? item.name : 'User Detail'} | Next.js + TypeScript Example`}>
26+
{item && <ListDetail item={item} />}
27+
</Layout>
28+
);
29+
};
30+
31+
export default StaticPropsDetail;
32+
33+
export const getStaticPaths: GetStaticPaths = async () => {
34+
// Get the paths we want to pre-render based on users
35+
const paths = sampleUserData.map(user => ({
36+
params: { id: user.id.toString() },
37+
}));
38+
39+
// We'll pre-render only these paths at build time.
40+
// { fallback: false } means other routes should 404.
41+
return { paths, fallback: false };
42+
};
43+
44+
// This function gets called at build time on server-side.
45+
// It won't be called on client-side, so you can even do
46+
// direct database queries.
47+
export const getStaticProps: GetStaticProps = async ({ params }) => {
48+
try {
49+
const id = params?.id;
50+
const item = sampleUserData.find(data => data.id === Number(id));
51+
// By returning { props: item }, the StaticPropsDetail component
52+
// will receive `item` as a prop at build time
53+
return { props: { item } };
54+
} catch (err) {
55+
return { props: { errors: err.message } };
56+
}
57+
};

0 commit comments

Comments
 (0)