Skip to content

Commit 3d03e4d

Browse files
niemyjskiclaude
andauthored
Add a simple Next.js reference integration example (#153)
* Add Next.js reference integration example * Format Next.js example files * Export bundles as package entry points and update Next.js example Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add resolve-package-imports build step and update Next.js example Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Revert "Add resolve-package-imports build step and update Next.js example" This reverts commit b462d7e. * pr feedback * Apply suggestions from code review Co-authored-by: Blake Niemyjski <bniemyjski@gmail.com> * pr feedback * PR feedback * lint fix --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e1efd78 commit 3d03e4d

20 files changed

+1860
-26
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ packages/node/test-data
1212

1313
yarn.lock
1414
.exceptionless
15+
16+
example/nextjs/.next/

example/nextjs/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
## Exceptionless for Next.js
2+
3+
This example is a very small App Router site that shows the Exceptionless integration shape we want for a Next.js app: simple setup, rich metadata, and clear client/server error coverage.
4+
5+
- `instrumentation-client.js` for browser startup and navigation logging
6+
- `instrumentation.js` for server startup and `onRequestError`
7+
- `app/error.jsx` for route-level client render failures
8+
- `app/global-error.jsx` for root-level client render failures
9+
- `app/api/demo/route.js` for explicit server-side logging from a route handler
10+
11+
### What it covers
12+
13+
- Manual client logs with structured data
14+
- Handled client exceptions submitted from a `try`/`catch`
15+
- Unhandled client promise rejections captured by the browser global handler
16+
- A client transition crash that lands in `app/error.jsx`
17+
- A server route log enriched with request headers, IP, path, query string, and JSON body
18+
- An unhandled route handler error captured by `onRequestError`
19+
- A server component render error captured by `onRequestError`
20+
21+
### Why it is shaped this way
22+
23+
This sticks to the native Next.js file boundaries instead of inventing another framework layer:
24+
25+
- `instrumentation-client.js` is where client-side monitoring starts before the app becomes interactive.
26+
- `instrumentation.js` and `onRequestError` are where uncaught server render, route handler, server action, and proxy errors are captured.
27+
- `app/error.jsx` and `app/global-error.jsx` stay responsible for client render failures inside the App Router.
28+
- Route handlers submit logs directly with `Exceptionless.createLog(...)`, the environment module memoizes `Exceptionless.startup(...)`, and the server flushes with `Exceptionless.processQueue()` when needed.
29+
30+
### Vercel-specific notes
31+
32+
- The server helper flushes the Exceptionless queue explicitly. That matters for short-lived serverless runtimes where a background timer may not get enough time to send queued events.
33+
- The route handler uses `after()` so normal server logs flush after the response is sent.
34+
- The example imports `@exceptionless/browser` and `@exceptionless/node` directly and uses the default Next.js bundler behavior, which is Turbopack on Next 16.
35+
- Because this is a workspace example, you still need the root `npm run build` step before starting it locally so the SDK packages have fresh `dist/` output.
36+
- If we later package this for production ergonomics, the clean split is likely a very thin `@exceptionless/nextjs` helper for framework hooks plus an optional `@exceptionless/vercel` add-on for `@vercel/otel`, deployment metadata, and queue-flush helpers.
37+
38+
### Environment variables
39+
40+
Set the env vars you want the example to use:
41+
42+
- `NEXT_PUBLIC_EXCEPTIONLESS_API_KEY`
43+
- `NEXT_PUBLIC_EXCEPTIONLESS_SERVER_URL`
44+
- `EXCEPTIONLESS_API_KEY`
45+
- `EXCEPTIONLESS_SERVER_URL`
46+
47+
### Run locally
48+
49+
1. `npm install`
50+
2. `npm run build`
51+
3. `cd example/nextjs`
52+
4. `npm run dev`
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { after } from "next/server";
2+
3+
import { startup } from "../../../lib/exceptionless-server.js";
4+
import { buildRequestContextFromRequest } from "../../../lib/next-request.js";
5+
6+
export async function POST(request) {
7+
const parsedBody = await request.json().catch(() => ({}));
8+
const body = typeof parsedBody === "object" && parsedBody !== null ? parsedBody : { value: parsedBody };
9+
const mode = typeof body.mode === "string" ? body.mode : "log";
10+
11+
if (mode === "error") {
12+
throw new Error("Route handler crash from the Exceptionless Next.js demo");
13+
}
14+
15+
const { Exceptionless, KnownEventDataKeys } = await startup();
16+
17+
const builder = Exceptionless.createLog("nextjs.route", "Route handler log from the demo page", "info").addTags("route-handler");
18+
builder.setContextProperty(KnownEventDataKeys.RequestInfo, buildRequestContextFromRequest(request, body));
19+
await builder.submit();
20+
21+
after(async () => {
22+
const { Exceptionless } = await startup();
23+
await Exceptionless.processQueue();
24+
});
25+
26+
return Response.json({
27+
ok: true,
28+
message: "Server route log submitted. The queue will flush in next/after()."
29+
});
30+
}

example/nextjs/app/error.jsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { useEffect } from "react";
5+
6+
import { Exceptionless, startup } from "../lib/exceptionless-browser.js";
7+
8+
export default function ErrorPage({ error, reset }) {
9+
useEffect(() => {
10+
if (!error.digest) {
11+
void (async () => {
12+
try {
13+
await startup();
14+
await Exceptionless.createException(error).addTags("error-boundary").setProperty("handledBy", "app/error.jsx").submit();
15+
} catch (submitError) {
16+
console.error("Exceptionless route boundary capture failed", submitError);
17+
}
18+
})();
19+
}
20+
}, [error]);
21+
22+
return (
23+
<main className="error-shell">
24+
<section className="panel error-card">
25+
<div className="panel-body">
26+
<p className="eyebrow">Route Error Boundary</p>
27+
<h1>Something inside this route broke.</h1>
28+
<p>
29+
Client-only render errors are submitted here. Server-rendered failures already have a digest and are captured by `instrumentation.js` through
30+
`onRequestError`.
31+
</p>
32+
<div className="error-actions">
33+
<button type="button" onClick={() => reset()}>
34+
Retry this route
35+
</button>
36+
<Link href="/">Back to the example</Link>
37+
</div>
38+
{error.digest ? <p className="error-digest">Server digest: {error.digest}</p> : null}
39+
</div>
40+
</section>
41+
</main>
42+
);
43+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { useEffect } from "react";
5+
6+
import { Exceptionless, startup } from "../lib/exceptionless-browser.js";
7+
8+
export default function GlobalError({ error, reset }) {
9+
useEffect(() => {
10+
if (!error.digest) {
11+
void (async () => {
12+
try {
13+
await startup();
14+
await Exceptionless.createException(error).addTags("error-boundary").setProperty("handledBy", "app/global-error.jsx").submit();
15+
} catch (submitError) {
16+
console.error("Exceptionless global boundary capture failed", submitError);
17+
}
18+
})();
19+
}
20+
}, [error]);
21+
22+
return (
23+
<html lang="en">
24+
<body>
25+
<main className="error-shell">
26+
<section className="panel error-card">
27+
<div className="panel-body">
28+
<p className="eyebrow">Global Error Boundary</p>
29+
<h1>The root layout failed.</h1>
30+
<p>
31+
This is the last-resort client boundary for the App Router. In normal server-rendered failures we still prefer the richer `onRequestError` path.
32+
</p>
33+
<div className="error-actions">
34+
<button type="button" onClick={() => reset()}>
35+
Retry the app shell
36+
</button>
37+
<Link href="/">Back to the example</Link>
38+
</div>
39+
{error.digest ? <p className="error-digest">Server digest: {error.digest}</p> : null}
40+
</div>
41+
</section>
42+
</main>
43+
</body>
44+
</html>
45+
);
46+
}

0 commit comments

Comments
 (0)