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
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 } };
}
};