Skip to content

Commit 72b0a18

Browse files
authored
test(nextjs): Add e2e test for orpc (#16462)
Basic e2e for making sure tracing looks ok for orpc calls
1 parent ae76a85 commit 72b0a18

26 files changed

+584
-1
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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
44+
45+
test-results
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/image-types/global" />
3+
4+
// NOTE: This file should not be edited
5+
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** @type {import("next").NextConfig} */
2+
const config = {};
3+
4+
import { withSentryConfig } from '@sentry/nextjs';
5+
6+
export default withSentryConfig(config, {
7+
disableLogger: true,
8+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "next-orpc",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "next build",
8+
"dev": "next dev -p 3030",
9+
"start": "next start -p 3030",
10+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
11+
"test:prod": "TEST_ENV=production playwright test",
12+
"test:dev": "TEST_ENV=development playwright test",
13+
"test:build": "pnpm install && pnpm build",
14+
"test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && pnpm build",
15+
"test:build-latest": "pnpm install && pnpm add next@rc && pnpm add react@beta && pnpm add react-dom@beta && pnpm build",
16+
"test:assert": "pnpm test:prod && pnpm test:dev"
17+
},
18+
"dependencies": {
19+
"@sentry/nextjs": "latest || *",
20+
"@orpc/server": "latest",
21+
"@orpc/client": "latest",
22+
"next": "14.2.29",
23+
"react": "18.3.1",
24+
"react-dom": "18.3.1",
25+
"server-only": "^0.0.1"
26+
},
27+
"devDependencies": {
28+
"@playwright/test": "~1.50.0",
29+
"@sentry-internal/test-utils": "link:../../../test-utils",
30+
"@types/eslint": "^8.56.10",
31+
"@types/node": "^18.19.1",
32+
"@types/react": "18.3.1",
33+
"@types/react-dom": "^18.3.0",
34+
"@typescript-eslint/eslint-plugin": "^8.1.0",
35+
"@typescript-eslint/parser": "^8.1.0",
36+
"eslint": "^8.57.0",
37+
"eslint-config-next": "^14.2.4",
38+
"postcss": "^8.4.39",
39+
"prettier": "^3.3.2",
40+
"typescript": "^5.5.3"
41+
},
42+
"volta": {
43+
"extends": "../../package.json"
44+
}
45+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
const testEnv = process.env.TEST_ENV;
3+
4+
if (!testEnv) {
5+
throw new Error('No test env defined');
6+
}
7+
8+
const config = getPlaywrightConfig(
9+
{
10+
startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030',
11+
port: 3030,
12+
},
13+
{
14+
// This comes with the risk of tests leaking into each other but the tests run quite slow so we should parallelize
15+
workers: '100%',
16+
},
17+
);
18+
19+
export default config;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
2+
// The config you add here will be used whenever one of the edge features is loaded.
3+
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
4+
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
5+
6+
import * as Sentry from '@sentry/nextjs';
7+
8+
Sentry.init({
9+
environment: 'qa', // dynamic sampling bias to keep transactions
10+
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
11+
tunnel: `http://localhost:3031/`, // proxy server
12+
tracesSampleRate: 1.0,
13+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
3+
Sentry.init({
4+
environment: 'qa', // dynamic sampling bias to keep transactions
5+
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
6+
tunnel: `http://localhost:3031/`, // proxy server
7+
tracesSampleRate: 1.0,
8+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { FindPlanet } from '~/components/FindPlanet';
2+
3+
export default async function ClientErrorPage() {
4+
return (
5+
<main>
6+
<FindPlanet withError />
7+
</main>
8+
);
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { FindPlanet } from '~/components/FindPlanet';
2+
3+
export default async function ClientPage() {
4+
return (
5+
<main>
6+
<FindPlanet />
7+
</main>
8+
);
9+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use client';
2+
3+
import * as Sentry from '@sentry/nextjs';
4+
import NextError from 'next/error';
5+
import { useEffect } from 'react';
6+
7+
export default function GlobalError({
8+
error,
9+
}: {
10+
error: Error & { digest?: string };
11+
}) {
12+
useEffect(() => {
13+
Sentry.captureException(error);
14+
}, [error]);
15+
16+
return (
17+
<html>
18+
<body>
19+
{/* `NextError` is the default Next.js error page component. Its type
20+
definition requires a `statusCode` prop. However, since the App Router
21+
does not expose status codes for errors, we simply pass 0 to render a
22+
generic error message. */}
23+
<NextError statusCode={0} />
24+
</body>
25+
</html>
26+
);
27+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import '../orpc/server';
2+
import * as Sentry from '@sentry/nextjs';
3+
4+
import { type Metadata } from 'next';
5+
6+
export function generateMetadata(): Metadata {
7+
return {
8+
other: {
9+
...Sentry.getTraceData(),
10+
},
11+
};
12+
}
13+
14+
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
15+
return (
16+
<html lang="en">
17+
<body>{children}</body>
18+
</html>
19+
);
20+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Link from 'next/link';
2+
import { client } from '~/orpc/client';
3+
4+
export default async function Home() {
5+
const planets = await client.planet.list({ limit: 10 });
6+
7+
return (
8+
<main>
9+
<h1>Planets</h1>
10+
<ul>
11+
{planets.map(planet => (
12+
<li key={planet.id}>{planet.name}</li>
13+
))}
14+
</ul>
15+
<Link href={'/client'}>Client</Link>
16+
<Link href={'/client-error'}>Error</Link>
17+
</main>
18+
);
19+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { RPCHandler } from '@orpc/server/fetch';
2+
import { router } from '~/orpc/router';
3+
4+
const handler = new RPCHandler(router);
5+
6+
async function handleRequest(request: Request) {
7+
const { response } = await handler.handle(request, {
8+
prefix: '/rpc',
9+
context: {
10+
headers: Object.fromEntries(request.headers.entries()),
11+
},
12+
});
13+
14+
return response ?? new Response('Not found', { status: 404 });
15+
}
16+
17+
export const HEAD = handleRequest;
18+
export const GET = handleRequest;
19+
export const POST = handleRequest;
20+
export const PUT = handleRequest;
21+
export const PATCH = handleRequest;
22+
export const DELETE = handleRequest;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use client';
2+
3+
import { client } from '~/orpc/client';
4+
import { useEffect, useState } from 'react';
5+
6+
type Planet = {
7+
id: number;
8+
name: string;
9+
description?: string;
10+
};
11+
12+
export function FindPlanet({ withError = false }: { withError?: boolean }) {
13+
const [planet, setPlanet] = useState<Planet>();
14+
const [loading, setLoading] = useState(true);
15+
const [error, setError] = useState<string | null>(null);
16+
17+
useEffect(() => {
18+
async function fetchPlanet() {
19+
const data = withError ? await client.planet.findWithError({ id: 1 }) : await client.planet.find({ id: 1 });
20+
setPlanet(data);
21+
}
22+
23+
setLoading(true);
24+
fetchPlanet();
25+
setLoading(false);
26+
}, []);
27+
28+
if (loading) {
29+
return <div>Loading planet...</div>;
30+
}
31+
32+
if (error) {
33+
return <div>Error: {error}</div>;
34+
}
35+
36+
return (
37+
<div>
38+
<h1>Planet</h1>
39+
<div>{planet?.name}</div>
40+
</div>
41+
);
42+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
3+
Sentry.init({
4+
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
5+
tunnel: `http://localhost:3031/`, // proxy server
6+
tracesSampleRate: 1,
7+
debug: false,
8+
});
9+
10+
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
3+
export async function register() {
4+
if (process.env.NEXT_RUNTIME === 'nodejs') {
5+
await import('../sentry.server.config');
6+
}
7+
8+
if (process.env.NEXT_RUNTIME === 'edge') {
9+
await import('../sentry.edge.config');
10+
}
11+
}
12+
13+
export const onRequestError = Sentry.captureRequestError;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createORPCClient } from '@orpc/client';
2+
import { RPCLink } from '@orpc/client/fetch';
3+
import { RouterClient } from '@orpc/server';
4+
import type { headers } from 'next/headers';
5+
import { router } from './router';
6+
7+
declare global {
8+
var $headers: typeof headers;
9+
}
10+
11+
const link = new RPCLink({
12+
url: new URL('/rpc', typeof window !== 'undefined' ? window.location.href : 'http://localhost:3030'),
13+
headers: async () => {
14+
return globalThis.$headers
15+
? Object.fromEntries(await globalThis.$headers()) // ssr
16+
: {}; // browser
17+
},
18+
});
19+
20+
export const client: RouterClient<typeof router> = createORPCClient(link);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { ORPCError, os } from '@orpc/server';
2+
import { z } from 'zod';
3+
import { sentryTracingMiddleware } from './sentry-middleware';
4+
5+
const PlanetSchema = z.object({
6+
id: z.number().int().min(1),
7+
name: z.string(),
8+
description: z.string().optional(),
9+
});
10+
11+
export const base = os.use(sentryTracingMiddleware);
12+
13+
export const listPlanet = base
14+
.input(
15+
z.object({
16+
limit: z.number().int().min(1).max(100).optional(),
17+
cursor: z.number().int().min(0).default(0),
18+
}),
19+
)
20+
.handler(async ({ input }) => {
21+
return [
22+
{ id: 1, name: 'name' },
23+
{ id: 2, name: 'another name' },
24+
];
25+
});
26+
27+
export const findPlanet = base.input(PlanetSchema.pick({ id: true })).handler(async ({ input }) => {
28+
await new Promise(resolve => setTimeout(resolve, 500));
29+
return { id: 1, name: 'name' };
30+
});
31+
32+
export const throwingFindPlanet = base.input(PlanetSchema.pick({ id: true })).handler(async ({ input }) => {
33+
throw new ORPCError('OH_OH', {
34+
message: 'You are hitting an error',
35+
data: { some: 'data' },
36+
});
37+
});
38+
39+
export const router = {
40+
planet: {
41+
list: listPlanet,
42+
find: findPlanet,
43+
findWithError: throwingFindPlanet,
44+
},
45+
};

0 commit comments

Comments
 (0)