Skip to content

Commit 7e9472f

Browse files
committed
Add Next.js reference integration example
1 parent 5c8cb7c commit 7e9472f

21 files changed

+1887
-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 locally aliases `source-map` to `false` in `next.config.mjs` so an unused `stacktrace-gps` AMD branch does not leak a `source-map` dependency into `@exceptionless/browser`.
35+
- The helper files import the built ESM bundles from `packages/browser/dist/index.bundle.js` and `packages/node/dist/index.bundle.js` because the package entrypoints still re-export internal `#/*` imports. The example also uses `--webpack` because Turbopack currently rejects the node bundle during page data collection on `node-localstorage`'s dynamic `require`.
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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { after } from "next/server";
2+
3+
import { Exceptionless, KnownEventDataKeys, 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+
await startup();
16+
17+
const builder = Exceptionless.createLog("nextjs.route", "Route handler log from the demo page", "info").addTags("route-handler");
18+
19+
builder.setContextProperty(KnownEventDataKeys.RequestInfo, buildRequestContextFromRequest(request, body));
20+
21+
await builder.submit();
22+
23+
after(async () => {
24+
await Exceptionless.processQueue();
25+
});
26+
27+
return Response.json({
28+
ok: true,
29+
message: "Server route log submitted. The queue will flush in next/after()."
30+
});
31+
}

example/nextjs/app/error.jsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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)
15+
.addTags("error-boundary")
16+
.setProperty("handledBy", "app/error.jsx")
17+
.setProperty("digest", error.digest)
18+
.submit();
19+
} catch (submitError) {
20+
console.error("Exceptionless route boundary capture failed", submitError);
21+
}
22+
})();
23+
}
24+
}, [error]);
25+
26+
return (
27+
<main className="error-shell">
28+
<section className="panel error-card">
29+
<div className="panel-body">
30+
<p className="eyebrow">Route Error Boundary</p>
31+
<h1>Something inside this route broke.</h1>
32+
<p>
33+
Client-only render errors are submitted here. Server-rendered failures already have a digest and are captured by
34+
`instrumentation.js` through `onRequestError`.
35+
</p>
36+
<div className="error-actions">
37+
<button type="button" onClick={() => reset()}>
38+
Retry this route
39+
</button>
40+
<Link href="/">Back to the example</Link>
41+
</div>
42+
{error.digest ? <p className="error-digest">Server digest: {error.digest}</p> : null}
43+
</div>
44+
</section>
45+
</main>
46+
);
47+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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)
15+
.addTags("error-boundary")
16+
.setProperty("handledBy", "app/global-error.jsx")
17+
.setProperty("digest", error.digest)
18+
.submit();
19+
} catch (submitError) {
20+
console.error("Exceptionless global boundary capture failed", submitError);
21+
}
22+
})();
23+
}
24+
}, [error]);
25+
26+
return (
27+
<html lang="en">
28+
<body>
29+
<main className="error-shell">
30+
<section className="panel error-card">
31+
<div className="panel-body">
32+
<p className="eyebrow">Global Error Boundary</p>
33+
<h1>The root layout failed.</h1>
34+
<p>
35+
This is the last-resort client boundary for the App Router. In normal server-rendered failures we still prefer the
36+
richer `onRequestError` path.
37+
</p>
38+
<div className="error-actions">
39+
<button type="button" onClick={() => reset()}>
40+
Retry the app shell
41+
</button>
42+
<Link href="/">Back to the example</Link>
43+
</div>
44+
{error.digest ? <p className="error-digest">Server digest: {error.digest}</p> : null}
45+
</div>
46+
</section>
47+
</main>
48+
</body>
49+
</html>
50+
);
51+
}

0 commit comments

Comments
 (0)