Skip to content

Commit

Permalink
Next.js integration tests for Server and Browser (#3632)
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilogorek committed Jun 2, 2021
1 parent d712e13 commit 17b12e1
Show file tree
Hide file tree
Showing 51 changed files with 3,977 additions and 3 deletions.
2 changes: 1 addition & 1 deletion packages/nextjs/.eslintrc.js
Expand Up @@ -9,7 +9,7 @@ module.exports = {
ecmaVersion: 2018,
},
extends: ['@sentry-internal/sdk'],
ignorePatterns: ['build/**', 'dist/**', 'esm/**', 'examples/**', 'scripts/**'],
ignorePatterns: ['build/**', 'dist/**', 'esm/**', 'examples/**', 'scripts/**', 'test/integration/**'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.d.ts'],
Expand Down
7 changes: 6 additions & 1 deletion packages/nextjs/package.json
Expand Up @@ -52,8 +52,13 @@
"fix": "run-s fix:eslint fix:prettier",
"fix:eslint": "eslint . --format stylish --fix",
"fix:prettier": "prettier --write \"{src,test}/**/*.ts\"",
"test": "jest",
"test": "run-s test:unit test:integration",
"test:watch": "jest --watch",
"test:unit": "jest",
"test:integration": "run-s test:integration:build test:integration:server test:integration:client",
"test:integration:build": "cd test/integration && yarn && yarn build && cd ../..",
"test:integration:server": "node test/integration/test/server.js --silent",
"test:integration:client": "node test/integration/test/client.js --silent",
"pack": "npm pack",
"vercel:branch": "source vercel/set-up-branch-for-test-app-use.sh",
"vercel:project": "source vercel/make-project-use-current-branch.sh"
Expand Down
2 changes: 2 additions & 0 deletions packages/nextjs/src/performance/client.ts
@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { Primitive, Transaction, TransactionContext } from '@sentry/types';
import { fill, getGlobalObject, stripUrlQueryAndFragment } from '@sentry/utils';
import { default as Router } from 'next/router';
Expand Down
34 changes: 34 additions & 0 deletions packages/nextjs/test/integration/.gitignore
@@ -0,0 +1,34 @@
# 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*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# vercel
.vercel
41 changes: 41 additions & 0 deletions packages/nextjs/test/integration/components/Layout.tsx
@@ -0,0 +1,41 @@
import React, { ReactNode } from 'react';
import Link from 'next/link';
import Head from 'next/head';

type Props = {
children?: ReactNode;
title?: string;
};

const Layout = ({ children, title = 'This is the default title' }: Props) => (
<div>
<Head>
<title>{title}</title>
<meta charSet="utf-8" />
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<header>
<nav>
<Link href="/">
<a>Home</a>
</Link>{' '}
|{' '}
<Link href="/about">
<a>About</a>
</Link>{' '}
|{' '}
<Link href="/users">
<a>Users List</a>
</Link>{' '}
| <a href="/api/users">Users API</a>
</nav>
</header>
{children}
<footer>
<hr />
<span>I'm here to stay (Footer)</span>
</footer>
</div>
);

export default Layout;
19 changes: 19 additions & 0 deletions packages/nextjs/test/integration/components/List.tsx
@@ -0,0 +1,19 @@
import * as React from 'react';
import ListItem from './ListItem';
import { User } from '../interfaces';

type Props = {
items: User[];
};

const List = ({ items }: Props) => (
<ul>
{items.map(item => (
<li key={item.id}>
<ListItem data={item} />
</li>
))}
</ul>
);

export default List;
16 changes: 16 additions & 0 deletions packages/nextjs/test/integration/components/ListDetail.tsx
@@ -0,0 +1,16 @@
import * as React from 'react';

import { User } from '../interfaces';

type ListDetailProps = {
item: User;
};

const ListDetail = ({ item: user }: ListDetailProps) => (
<div>
<h1>Detail for {user.name}</h1>
<p>ID: {user.id}</p>
</div>
);

export default ListDetail;
18 changes: 18 additions & 0 deletions packages/nextjs/test/integration/components/ListItem.tsx
@@ -0,0 +1,18 @@
import React from 'react';
import Link from 'next/link';

import { User } from '../interfaces';

type Props = {
data: User;
};

const ListItem = ({ data }: Props) => (
<Link href="/users/[id]" as={`/users/${data.id}`}>
<a>
{data.id}: {data.name}
</a>
</Link>
);

export default ListItem;
10 changes: 10 additions & 0 deletions packages/nextjs/test/integration/interfaces/index.ts
@@ -0,0 +1,10 @@
// You can include shared interfaces/types in a separate file
// and then use them in any component by importing them. For
// example, to import the interface below do:
//
// import { User } from 'path/to/interfaces';

export type User = {
id: number;
name: string;
};
2 changes: 2 additions & 0 deletions packages/nextjs/test/integration/next-env.d.ts
@@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
9 changes: 9 additions & 0 deletions packages/nextjs/test/integration/next.config.js
@@ -0,0 +1,9 @@
const { withSentryConfig } = require('@sentry/nextjs');

const moduleExports = {};
const SentryWebpackPluginOptions = {
dryRun: true,
silent: true,
};

module.exports = withSentryConfig(moduleExports, SentryWebpackPluginOptions);
27 changes: 27 additions & 0 deletions packages/nextjs/test/integration/package.json
@@ -0,0 +1,27 @@
{
"name": "with-typescript",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"type-check": "tsc"
},
"dependencies": {
"@sentry/nextjs": "file:../../",
"next": "latest",
"react": "^17.0.1",
"react-dom": "^17.0.1"
},
"devDependencies": {
"@types/node": "^15.3.1",
"@types/puppeteer": "^5.4.3",
"@types/react": "^17.0.6",
"@types/react-dom": "^17.0.5",
"nock": "^13.1.0",
"puppeteer": "^9.1.1",
"typescript": "^4.2.4",
"yargs": "^16.2.0"
},
"license": "MIT"
}
3 changes: 3 additions & 0 deletions packages/nextjs/test/integration/pages/about.tsx
@@ -0,0 +1,3 @@
const AboutPage = () => <h1>About</h1>;

export default AboutPage;
9 changes: 9 additions & 0 deletions packages/nextjs/test/integration/pages/alsoHealthy.tsx
@@ -0,0 +1,9 @@
import Link from 'next/link';

const HealthyPage = (): JSX.Element => (
<Link href="/healthy">
<a id="healthy">Healthy</a>
</Link>
);

export default HealthyPage;
8 changes: 8 additions & 0 deletions packages/nextjs/test/integration/pages/api/broken/index.ts
@@ -0,0 +1,8 @@
import { withSentry } from '@sentry/nextjs';
import { NextApiRequest, NextApiResponse } from 'next';

const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise<void> => {
res.status(500).json({ statusCode: 500, message: 'Something went wrong' });
};

export default withSentry(handler);
8 changes: 8 additions & 0 deletions packages/nextjs/test/integration/pages/api/error/index.ts
@@ -0,0 +1,8 @@
import { withSentry } from '@sentry/nextjs';
import { NextApiRequest, NextApiResponse } from 'next';

const handler = async (_req: NextApiRequest, _res: NextApiResponse): Promise<void> => {
throw new Error('API Error');
};

export default withSentry(handler);
10 changes: 10 additions & 0 deletions packages/nextjs/test/integration/pages/api/http/index.ts
@@ -0,0 +1,10 @@
import { withSentry } from '@sentry/nextjs';
import { get } from 'http';
import { NextApiRequest, NextApiResponse } from 'next';

const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise<void> => {
await new Promise(resolve => get('http://example.com', resolve));
res.status(200).json({});
};

export default withSentry(handler);
18 changes: 18 additions & 0 deletions packages/nextjs/test/integration/pages/api/users/index.ts
@@ -0,0 +1,18 @@
import { withSentry } from '@sentry/nextjs';
import { NextApiRequest, NextApiResponse } from 'next';

import { sampleUserData } from '../../../utils/sample-data';

const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise<void> => {
try {
if (!Array.isArray(sampleUserData)) {
throw new Error('Cannot find user data');
}

res.status(200).json(sampleUserData);
} catch (err) {
res.status(500).json({ statusCode: 500, message: (err as Error).message });
}
};

export default withSentry(handler);
16 changes: 16 additions & 0 deletions packages/nextjs/test/integration/pages/crashed.tsx
@@ -0,0 +1,16 @@
const CrashedPage = (): JSX.Element => {
// Magic to naively trigger onerror to make session crashed and allow for SSR
try {
// @ts-ignore
if (typeof window !== 'undefined' && typeof window.onerror === 'function') {
// Lovely oldschool browsers syntax with 5 arguments <3
// @ts-ignore
window.onerror(null, null, null, null, new Error('Crashed'));
}
} catch (_e) {
// no-empty
}
return <h1>Crashed</h1>;
};

export default CrashedPage;
11 changes: 11 additions & 0 deletions packages/nextjs/test/integration/pages/errorClick.tsx
@@ -0,0 +1,11 @@
const ButtonPage = (): JSX.Element => (
<button
onClick={() => {
throw new Error('Sentry Frontend Error');
}}
>
Throw Error
</button>
);

export default ButtonPage;
13 changes: 13 additions & 0 deletions packages/nextjs/test/integration/pages/fetch.tsx
@@ -0,0 +1,13 @@
const ButtonPage = (): JSX.Element => (
<button
onClick={() => {
fetch('http://example.com').catch(() => {
// no-empty
});
}}
>
Send Request
</button>
);

export default ButtonPage;
9 changes: 9 additions & 0 deletions packages/nextjs/test/integration/pages/healthy.tsx
@@ -0,0 +1,9 @@
import Link from 'next/link';

const HealthyPage = (): JSX.Element => (
<Link href="/alsoHealthy">
<a id="alsoHealthy">AlsoHealthy</a>
</Link>
);

export default HealthyPage;
3 changes: 3 additions & 0 deletions packages/nextjs/test/integration/pages/index.tsx
@@ -0,0 +1,3 @@
const IndexPage = (): JSX.Element => <h1>Hello Next.js</h1>;

export default IndexPage;
57 changes: 57 additions & 0 deletions packages/nextjs/test/integration/pages/users/[id].tsx
@@ -0,0 +1,57 @@
import { GetStaticProps, GetStaticPaths } from 'next';

import { User } from '../../interfaces';
import { sampleUserData } from '../../utils/sample-data';
import Layout from '../../components/Layout';
import ListDetail from '../../components/ListDetail';

type Props = {
item?: User;
errors?: string;
};

const StaticPropsDetail = ({ item, errors }: Props) => {
if (errors) {
return (
<Layout title="Error | Next.js + TypeScript Example">
<p>
<span style={{ color: 'red' }}>Error:</span> {errors}
</p>
</Layout>
);
}

return (
<Layout title={`${item ? item.name : 'User Detail'} | Next.js + TypeScript Example`}>
{item && <ListDetail item={item} />}
</Layout>
);
};

export default StaticPropsDetail;

export const getStaticPaths: GetStaticPaths = async () => {
// Get the paths we want to pre-render based on users
const paths = sampleUserData.map(user => ({
params: { id: user.id.toString() },
}));

// We'll pre-render only these paths at build time.
// { fallback: false } means other routes should 404.
return { paths, fallback: false };
};

// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do
// direct database queries.
export const getStaticProps: GetStaticProps = async ({ params }) => {
try {
const id = params?.id;
const item = sampleUserData.find(data => data.id === Number(id));
// By returning { props: item }, the StaticPropsDetail component
// will receive `item` as a prop at build time
return { props: { item } };
} catch (err) {
return { props: { errors: err.message } };
}
};

0 comments on commit 17b12e1

Please sign in to comment.