Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next.js integration tests for Server and Browser #3632

Merged
merged 17 commits into from
Jun 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/nextjs/.eslintrc.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 } };
}
};