From 7e9472fd2fda8418159ba9fae423d1d03895d00b Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 25 Mar 2026 08:40:26 -0500 Subject: [PATCH 1/3] Add Next.js reference integration example --- .gitignore | 2 + example/nextjs/README.md | 52 + example/nextjs/app/api/demo/route.js | 31 + example/nextjs/app/error.jsx | 47 + example/nextjs/app/global-error.jsx | 51 + example/nextjs/app/globals.css | 255 +++++ example/nextjs/app/layout.jsx | 14 + example/nextjs/app/page.jsx | 43 + .../app/server-component-error/page.jsx | 5 + example/nextjs/components/ClientDemoPanel.jsx | 156 +++ example/nextjs/instrumentation-client.js | 23 + example/nextjs/instrumentation.js | 35 + example/nextjs/lib/exceptionless-browser.js | 52 + example/nextjs/lib/exceptionless-server.js | 56 ++ example/nextjs/lib/next-request.js | 58 ++ example/nextjs/next.config.mjs | 12 + example/nextjs/package.json | 21 + example/nextjs/test/next-request.test.js | 73 ++ package-lock.json | 918 +++++++++++++++++- package.json | 2 +- vitest.config.ts | 7 + 21 files changed, 1887 insertions(+), 26 deletions(-) create mode 100644 example/nextjs/README.md create mode 100644 example/nextjs/app/api/demo/route.js create mode 100644 example/nextjs/app/error.jsx create mode 100644 example/nextjs/app/global-error.jsx create mode 100644 example/nextjs/app/globals.css create mode 100644 example/nextjs/app/layout.jsx create mode 100644 example/nextjs/app/page.jsx create mode 100644 example/nextjs/app/server-component-error/page.jsx create mode 100644 example/nextjs/components/ClientDemoPanel.jsx create mode 100644 example/nextjs/instrumentation-client.js create mode 100644 example/nextjs/instrumentation.js create mode 100644 example/nextjs/lib/exceptionless-browser.js create mode 100644 example/nextjs/lib/exceptionless-server.js create mode 100644 example/nextjs/lib/next-request.js create mode 100644 example/nextjs/next.config.mjs create mode 100644 example/nextjs/package.json create mode 100644 example/nextjs/test/next-request.test.js diff --git a/.gitignore b/.gitignore index 6d114876..b139f739 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ packages/node/test-data yarn.lock .exceptionless + +example/nextjs/.next/ diff --git a/example/nextjs/README.md b/example/nextjs/README.md new file mode 100644 index 00000000..f906f807 --- /dev/null +++ b/example/nextjs/README.md @@ -0,0 +1,52 @@ +## Exceptionless for Next.js + +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. + +- `instrumentation-client.js` for browser startup and navigation logging +- `instrumentation.js` for server startup and `onRequestError` +- `app/error.jsx` for route-level client render failures +- `app/global-error.jsx` for root-level client render failures +- `app/api/demo/route.js` for explicit server-side logging from a route handler + +### What it covers + +- Manual client logs with structured data +- Handled client exceptions submitted from a `try`/`catch` +- Unhandled client promise rejections captured by the browser global handler +- A client transition crash that lands in `app/error.jsx` +- A server route log enriched with request headers, IP, path, query string, and JSON body +- An unhandled route handler error captured by `onRequestError` +- A server component render error captured by `onRequestError` + +### Why it is shaped this way + +This sticks to the native Next.js file boundaries instead of inventing another framework layer: + +- `instrumentation-client.js` is where client-side monitoring starts before the app becomes interactive. +- `instrumentation.js` and `onRequestError` are where uncaught server render, route handler, server action, and proxy errors are captured. +- `app/error.jsx` and `app/global-error.jsx` stay responsible for client render failures inside the App Router. +- Route handlers submit logs directly with `Exceptionless.createLog(...)`, the environment module memoizes `Exceptionless.startup(...)`, and the server flushes with `Exceptionless.processQueue()` when needed. + +### Vercel-specific notes + +- 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. +- The route handler uses `after()` so normal server logs flush after the response is sent. +- 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`. +- 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`. +- 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. + +### Environment variables + +Set the env vars you want the example to use: + +- `NEXT_PUBLIC_EXCEPTIONLESS_API_KEY` +- `NEXT_PUBLIC_EXCEPTIONLESS_SERVER_URL` +- `EXCEPTIONLESS_API_KEY` +- `EXCEPTIONLESS_SERVER_URL` + +### Run locally + +1. `npm install` +2. `npm run build` +3. `cd example/nextjs` +4. `npm run dev` diff --git a/example/nextjs/app/api/demo/route.js b/example/nextjs/app/api/demo/route.js new file mode 100644 index 00000000..74c9609a --- /dev/null +++ b/example/nextjs/app/api/demo/route.js @@ -0,0 +1,31 @@ +import { after } from "next/server"; + +import { Exceptionless, KnownEventDataKeys, startup } from "../../../lib/exceptionless-server.js"; +import { buildRequestContextFromRequest } from "../../../lib/next-request.js"; + +export async function POST(request) { + const parsedBody = await request.json().catch(() => ({})); + const body = typeof parsedBody === "object" && parsedBody !== null ? parsedBody : { value: parsedBody }; + const mode = typeof body.mode === "string" ? body.mode : "log"; + + if (mode === "error") { + throw new Error("Route handler crash from the Exceptionless Next.js demo"); + } + + await startup(); + + const builder = Exceptionless.createLog("nextjs.route", "Route handler log from the demo page", "info").addTags("route-handler"); + + builder.setContextProperty(KnownEventDataKeys.RequestInfo, buildRequestContextFromRequest(request, body)); + + await builder.submit(); + + after(async () => { + await Exceptionless.processQueue(); + }); + + return Response.json({ + ok: true, + message: "Server route log submitted. The queue will flush in next/after()." + }); +} diff --git a/example/nextjs/app/error.jsx b/example/nextjs/app/error.jsx new file mode 100644 index 00000000..e5381253 --- /dev/null +++ b/example/nextjs/app/error.jsx @@ -0,0 +1,47 @@ +"use client"; + +import Link from "next/link"; +import { useEffect } from "react"; + +import { Exceptionless, startup } from "../lib/exceptionless-browser.js"; + +export default function ErrorPage({ error, reset }) { + useEffect(() => { + if (!error.digest) { + void (async () => { + try { + await startup(); + await Exceptionless.createException(error) + .addTags("error-boundary") + .setProperty("handledBy", "app/error.jsx") + .setProperty("digest", error.digest) + .submit(); + } catch (submitError) { + console.error("Exceptionless route boundary capture failed", submitError); + } + })(); + } + }, [error]); + + return ( +
+
+
+

Route Error Boundary

+

Something inside this route broke.

+

+ Client-only render errors are submitted here. Server-rendered failures already have a digest and are captured by + `instrumentation.js` through `onRequestError`. +

+
+ + Back to the example +
+ {error.digest ?

Server digest: {error.digest}

: null} +
+
+
+ ); +} diff --git a/example/nextjs/app/global-error.jsx b/example/nextjs/app/global-error.jsx new file mode 100644 index 00000000..930aaf55 --- /dev/null +++ b/example/nextjs/app/global-error.jsx @@ -0,0 +1,51 @@ +"use client"; + +import Link from "next/link"; +import { useEffect } from "react"; + +import { Exceptionless, startup } from "../lib/exceptionless-browser.js"; + +export default function GlobalError({ error, reset }) { + useEffect(() => { + if (!error.digest) { + void (async () => { + try { + await startup(); + await Exceptionless.createException(error) + .addTags("error-boundary") + .setProperty("handledBy", "app/global-error.jsx") + .setProperty("digest", error.digest) + .submit(); + } catch (submitError) { + console.error("Exceptionless global boundary capture failed", submitError); + } + })(); + } + }, [error]); + + return ( + + +
+
+
+

Global Error Boundary

+

The root layout failed.

+

+ This is the last-resort client boundary for the App Router. In normal server-rendered failures we still prefer the + richer `onRequestError` path. +

+
+ + Back to the example +
+ {error.digest ?

Server digest: {error.digest}

: null} +
+
+
+ + + ); +} diff --git a/example/nextjs/app/globals.css b/example/nextjs/app/globals.css new file mode 100644 index 00000000..dc1e59fd --- /dev/null +++ b/example/nextjs/app/globals.css @@ -0,0 +1,255 @@ +:root { + color-scheme: light; + --bg: #f4efe7; + --surface: rgba(255, 255, 255, 0.78); + --surface-strong: rgba(255, 255, 255, 0.94); + --border: rgba(23, 29, 43, 0.12); + --text: #1b2130; + --muted: #5d6779; + --accent: #0f766e; + --accent-soft: rgba(15, 118, 110, 0.1); + --danger: #b42318; + --shadow: 0 20px 50px rgba(38, 45, 61, 0.12); +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + min-height: 100%; + font-family: + "Geist", + "IBM Plex Sans", + "Avenir Next", + system-ui, + sans-serif; + background: + radial-gradient(circle at top left, rgba(15, 118, 110, 0.14), transparent 35%), + radial-gradient(circle at top right, rgba(210, 84, 30, 0.12), transparent 30%), + var(--bg); + color: var(--text); +} + +body { + line-height: 1.5; +} + +a { + color: inherit; +} + +button { + font: inherit; +} + +.page { + width: min(1120px, calc(100vw - 32px)); + margin: 0 auto; + padding: 48px 0 64px; +} + +.hero { + display: grid; + gap: 16px; + margin-bottom: 28px; +} + +.eyebrow { + margin: 0; + color: var(--accent); + font-size: 0.85rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.hero h1, +.error-shell h1 { + margin: 0; + max-width: 12ch; + font-size: clamp(2.8rem, 7vw, 5.4rem); + line-height: 0.95; + letter-spacing: -0.05em; +} + +.hero p, +.error-shell p { + margin: 0; + max-width: 70ch; + color: var(--muted); + font-size: 1.02rem; +} + +.hero-meta { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.hero-meta span { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border: 1px solid var(--border); + border-radius: 999px; + background: rgba(255, 255, 255, 0.55); + color: var(--muted); + font-size: 0.92rem; +} + +.demo-grid { + display: grid; + gap: 20px; + grid-template-columns: minmax(0, 1.2fr) minmax(280px, 0.8fr); +} + +.panel { + border: 1px solid var(--border); + border-radius: 24px; + background: var(--surface); + backdrop-filter: blur(10px); + box-shadow: var(--shadow); +} + +.panel-body { + padding: 24px; +} + +.panel h2 { + margin: 0 0 8px; + font-size: 1.3rem; + letter-spacing: -0.03em; +} + +.panel p { + margin: 0; + color: var(--muted); +} + +.button-grid { + display: grid; + gap: 12px; + margin-top: 18px; +} + +.button-grid button, +.button-grid a { + display: flex; + width: 100%; + justify-content: space-between; + align-items: center; + gap: 16px; + padding: 14px 16px; + border: 1px solid var(--border); + border-radius: 18px; + background: var(--surface-strong); + color: inherit; + cursor: pointer; + text-decoration: none; + transition: + transform 160ms ease, + border-color 160ms ease, + box-shadow 160ms ease; +} + +.button-grid button:hover, +.button-grid a:hover { + transform: translateY(-1px); + border-color: rgba(15, 118, 110, 0.3); + box-shadow: 0 12px 28px rgba(30, 34, 45, 0.09); +} + +.button-grid button:disabled { + opacity: 0.7; + cursor: progress; + transform: none; +} + +.button-label { + display: grid; + gap: 3px; + text-align: left; +} + +.button-label strong { + font-size: 0.98rem; +} + +.button-label span { + color: var(--muted); + font-size: 0.9rem; +} + +.button-arrow { + color: var(--accent); + font-size: 1.2rem; +} + +.status { + margin-top: 18px; + padding: 14px 16px; + border-radius: 18px; + background: var(--accent-soft); + color: var(--text); +} + +.status strong { + display: block; + margin-bottom: 4px; + font-size: 0.95rem; +} + +.note-list { + margin: 18px 0 0; + padding-left: 18px; + color: var(--muted); +} + +.note-list li + li { + margin-top: 10px; +} + +.error-shell { + min-height: 100vh; + display: grid; + place-items: center; + padding: 24px; +} + +.error-card { + width: min(620px, 100%); +} + +.error-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 22px; +} + +.error-actions button, +.error-actions a { + padding: 12px 16px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--surface-strong); + color: inherit; + text-decoration: none; + cursor: pointer; +} + +.error-digest { + margin-top: 14px; + color: var(--muted); + font-size: 0.92rem; +} + +@media (max-width: 900px) { + .demo-grid { + grid-template-columns: 1fr; + } +} diff --git a/example/nextjs/app/layout.jsx b/example/nextjs/app/layout.jsx new file mode 100644 index 00000000..04740dab --- /dev/null +++ b/example/nextjs/app/layout.jsx @@ -0,0 +1,14 @@ +import "./globals.css"; + +export const metadata = { + title: "Exceptionless for Next.js", + description: "A small reference app for Exceptionless client and server monitoring in Next.js." +}; + +export default function RootLayout({ children }) { + return ( + + {children} + + ); +} diff --git a/example/nextjs/app/page.jsx b/example/nextjs/app/page.jsx new file mode 100644 index 00000000..95da3cb7 --- /dev/null +++ b/example/nextjs/app/page.jsx @@ -0,0 +1,43 @@ +import ClientDemoPanel from "../components/ClientDemoPanel.jsx"; + +export default function HomePage() { + const deploymentTarget = process.env.VERCEL_ENV ?? "local"; + + return ( +
+
+

Exceptionless for Next.js

+

Client and server monitoring for Next.js.

+

+ This reference app keeps the setup small, but still captures the important Exceptionless signals across browser and + server paths: logs, handled errors, unhandled errors, request metadata, and App Router error boundaries. +

+
+ Deployment target: {deploymentTarget} + App Router reference integration +
+
+ +
+ + + +
+
+ ); +} diff --git a/example/nextjs/app/server-component-error/page.jsx b/example/nextjs/app/server-component-error/page.jsx new file mode 100644 index 00000000..a52d196a --- /dev/null +++ b/example/nextjs/app/server-component-error/page.jsx @@ -0,0 +1,5 @@ +export const dynamic = "force-dynamic"; + +export default function ServerComponentErrorPage() { + throw new Error("Server component crash from the Exceptionless Next.js demo"); +} diff --git a/example/nextjs/components/ClientDemoPanel.jsx b/example/nextjs/components/ClientDemoPanel.jsx new file mode 100644 index 00000000..4af4fa64 --- /dev/null +++ b/example/nextjs/components/ClientDemoPanel.jsx @@ -0,0 +1,156 @@ +"use client"; + +import Link from "next/link"; +import { useState, useTransition } from "react"; + +import { Exceptionless, startup } from "../lib/exceptionless-browser.js"; + +export default function ClientDemoPanel() { + const [status, setStatus] = useState("Ready. Pick any path below to generate client or server telemetry."); + const [pending, startTransition] = useTransition(); + + async function sendClientLog() { + setStatus("Submitting a structured client log..."); + + await startup(); + + await Exceptionless.createLog("nextjs.client", "Client log from the demo page", "info") + .addTags("manual-log") + .setProperty("currentUrl", window.location.href) + .setProperty("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone) + .submit(); + + setStatus("Client log submitted."); + } + + async function sendHandledClientError() { + setStatus("Submitting a handled client error..."); + + try { + throw new Error("Handled client error from the Exceptionless Next.js demo"); + } catch (error) { + await startup(); + + await Exceptionless.createException(error) + .addTags("handled-error") + .setProperty("handledBy", "ClientDemoPanel.handleTryCatch") + .setProperty("currentUrl", window.location.href) + .submit(); + } + + setStatus("Handled client error submitted."); + } + + function triggerUnhandledRejection() { + setStatus("Triggered an unhandled promise rejection. The browser global handler should capture it."); + Promise.reject(new Error("Unhandled promise rejection from the Exceptionless Next.js demo")); + } + + function triggerBoundaryCrash() { + setStatus("Crashing the route boundary..."); + + startTransition(() => { + throw new Error("Client transition crash from the Exceptionless Next.js demo"); + }); + } + + async function callServerRoute(mode) { + setStatus(mode === "error" ? "Triggering a route handler crash..." : "Submitting a route handler log..."); + + const response = await fetch("/api/demo", { + method: "POST", + headers: { + "content-type": "application/json" + }, + body: JSON.stringify({ + mode, + triggeredFrom: "ClientDemoPanel", + currentUrl: window.location.href, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone + }) + }); + + if (!response.ok) { + setStatus(`Route handler error triggered with HTTP ${response.status}. The server onRequestError hook should capture it.`); + return; + } + + const payload = await response.json(); + setStatus(payload.message); + } + + return ( +
+
+

Try the integration

+

+ The first four buttons stay in the browser. The next two go through a real Next route handler. The link at the bottom + opens a route that throws during server rendering. +

+ +
+ + + + + + + + + + + + + + + Open the server component error route + Exercises the App Router render path and `onRequestError`. + + + +
+ +
+ Latest status + {status} +
+
+
+ ); +} diff --git a/example/nextjs/instrumentation-client.js b/example/nextjs/instrumentation-client.js new file mode 100644 index 00000000..224c4294 --- /dev/null +++ b/example/nextjs/instrumentation-client.js @@ -0,0 +1,23 @@ +import { Exceptionless, startup } from "./lib/exceptionless-browser.js"; + +void startup().catch((error) => { + console.error("Exceptionless browser startup failed", error); +}); + +export function onRouterTransitionStart(url, navigationType) { + void recordRouterTransitionStart(url, navigationType); +} + +async function recordRouterTransitionStart(url, navigationType) { + try { + await startup(); + + await Exceptionless.createLog("nextjs.navigation", "Route transition started", "info") + .addTags("navigation") + .setProperty("navigationType", navigationType) + .setProperty("url", url) + .submit(); + } catch (error) { + console.error("Exceptionless navigation tracking failed", error); + } +} diff --git a/example/nextjs/instrumentation.js b/example/nextjs/instrumentation.js new file mode 100644 index 00000000..a17ffd33 --- /dev/null +++ b/example/nextjs/instrumentation.js @@ -0,0 +1,35 @@ +import { buildRequestContextFromOnRequestError } from "./lib/next-request.js"; + +export async function register() { + if (process.env.NEXT_RUNTIME !== "nodejs") { + return; + } + + const { startup } = await import("./lib/exceptionless-server.js"); + await startup(); +} + +export async function onRequestError(error, request, context) { + if (process.env.NEXT_RUNTIME !== "nodejs") { + return; + } + + const { Exceptionless, KnownEventDataKeys, startup, toError } = await import("./lib/exceptionless-server.js"); + const digest = typeof error === "object" && error !== null && "digest" in error ? error.digest : undefined; + + await startup(); + + const builder = Exceptionless.createUnhandledException(toError(error), `nextjs.${context.routeType}`).addTags("on-request-error"); + + builder.setContextProperty(KnownEventDataKeys.RequestInfo, buildRequestContextFromOnRequestError(request)); + builder.setProperty("digest", digest); + builder.setProperty("routePath", context.routePath); + builder.setProperty("routeType", context.routeType); + builder.setProperty("routerKind", context.routerKind); + builder.setProperty("renderSource", context.renderSource); + builder.setProperty("renderType", context.renderType); + builder.setProperty("revalidateReason", context.revalidateReason); + + await builder.submit(); + await Exceptionless.processQueue(); +} diff --git a/example/nextjs/lib/exceptionless-browser.js b/example/nextjs/lib/exceptionless-browser.js new file mode 100644 index 00000000..6bd292a2 --- /dev/null +++ b/example/nextjs/lib/exceptionless-browser.js @@ -0,0 +1,52 @@ +import { Exceptionless, KnownEventDataKeys } from "../../../packages/browser/dist/index.bundle.js"; + +export { Exceptionless }; + +let startupPromise; + +export function startup() { + startupPromise ??= Exceptionless.startup((config) => { + if (process.env.NEXT_PUBLIC_EXCEPTIONLESS_API_KEY) { + config.apiKey = process.env.NEXT_PUBLIC_EXCEPTIONLESS_API_KEY; + } + + if (process.env.NEXT_PUBLIC_EXCEPTIONLESS_SERVER_URL) { + config.serverUrl = process.env.NEXT_PUBLIC_EXCEPTIONLESS_SERVER_URL; + } + + if (process.env.NODE_ENV !== "production") { + config.useDebugLogger(); + } + + config.addDataExclusions("authorization", "cookie", "password", "set-cookie", "token"); + config.defaultTags.push("Example", "nextjs", "client"); + config.addPlugin({ + priority: 90, + name: "NextEnvironmentInfoPlugin", + run(context) { + const eventData = context.event.data ?? {}; + context.event.data = eventData; + + const environment = + typeof eventData[KnownEventDataKeys.EnvironmentInfo] === "object" && eventData[KnownEventDataKeys.EnvironmentInfo] !== null + ? eventData[KnownEventDataKeys.EnvironmentInfo] + : {}; + const environmentData = typeof environment.data === "object" && environment.data !== null ? environment.data : {}; + + eventData[KnownEventDataKeys.EnvironmentInfo] = { + ...environment, + data: { + ...environmentData, + framework: "Next.js", + router: "App Router", + runtime: "browser" + } + }; + + return Promise.resolve(); + } + }); + }); + + return startupPromise; +} diff --git a/example/nextjs/lib/exceptionless-server.js b/example/nextjs/lib/exceptionless-server.js new file mode 100644 index 00000000..2c519392 --- /dev/null +++ b/example/nextjs/lib/exceptionless-server.js @@ -0,0 +1,56 @@ +import { Exceptionless, KnownEventDataKeys, toError } from "../../../packages/node/dist/index.bundle.js"; + +export { Exceptionless, KnownEventDataKeys, toError }; + +let startupPromise; + +export function startup() { + startupPromise ??= Exceptionless.startup((config) => { + if (process.env.EXCEPTIONLESS_API_KEY ?? process.env.NEXT_PUBLIC_EXCEPTIONLESS_API_KEY) { + config.apiKey = process.env.EXCEPTIONLESS_API_KEY ?? process.env.NEXT_PUBLIC_EXCEPTIONLESS_API_KEY; + } + + if (process.env.EXCEPTIONLESS_SERVER_URL ?? process.env.NEXT_PUBLIC_EXCEPTIONLESS_SERVER_URL) { + config.serverUrl = process.env.EXCEPTIONLESS_SERVER_URL ?? process.env.NEXT_PUBLIC_EXCEPTIONLESS_SERVER_URL; + } + + if (process.env.NODE_ENV !== "production") { + config.useDebugLogger(); + } + + config.addDataExclusions("authorization", "cookie", "password", "set-cookie", "token"); + config.defaultTags.push("Example", "nextjs", "server"); + config.addPlugin({ + priority: 90, + name: "NextEnvironmentInfoPlugin", + run(context) { + const eventData = context.event.data ?? {}; + context.event.data = eventData; + + const environment = + typeof eventData[KnownEventDataKeys.EnvironmentInfo] === "object" && eventData[KnownEventDataKeys.EnvironmentInfo] !== null + ? eventData[KnownEventDataKeys.EnvironmentInfo] + : {}; + const environmentData = typeof environment.data === "object" && environment.data !== null ? environment.data : {}; + + eventData[KnownEventDataKeys.EnvironmentInfo] = { + ...environment, + data: { + ...environmentData, + framework: "Next.js", + router: "App Router", + runtime: "nodejs", + ...(process.env.VERCEL_ENV ?? process.env.NODE_ENV ? { deployment: process.env.VERCEL_ENV ?? process.env.NODE_ENV } : {}), + ...(process.env.VERCEL_REGION ? { region: process.env.VERCEL_REGION } : {}), + ...(process.env.VERCEL_URL ? { url: process.env.VERCEL_URL } : {}), + ...(process.env.VERCEL_GIT_COMMIT_SHA ? { commit: process.env.VERCEL_GIT_COMMIT_SHA } : {}) + } + }; + + return Promise.resolve(); + } + }); + }); + + return startupPromise; +} diff --git a/example/nextjs/lib/next-request.js b/example/nextjs/lib/next-request.js new file mode 100644 index 00000000..129b6c0e --- /dev/null +++ b/example/nextjs/lib/next-request.js @@ -0,0 +1,58 @@ +export function buildRequestContextFromRequest(request, body) { + return buildRequestContext({ + method: request.method, + pathOrUrl: request.url, + headers: request.headers, + body + }); +} + +export function buildRequestContextFromOnRequestError(request) { + return buildRequestContext({ + method: request.method, + pathOrUrl: request.path, + headers: request.headers + }); +} + +export function buildRequestContext({ method, pathOrUrl, headers, body }) { + const normalizedHeaders = normalizeHeaders(headers); + const origin = getOrigin(normalizedHeaders); + const url = new URL(pathOrUrl, origin); + + return { + method, + secure: url.protocol === "https:", + ip: getClientIp(normalizedHeaders), + hostname: url.hostname, + path: url.pathname, + headers: normalizedHeaders, + params: Object.fromEntries(url.searchParams.entries()), + body + }; +} + +function normalizeHeaders(headers) { + if (headers instanceof Headers) { + return Object.fromEntries(Array.from(headers.entries()).map(([key, value]) => [key.toLowerCase(), value])); + } + + return Object.fromEntries( + Object.entries(headers).map(([key, value]) => [key.toLowerCase(), Array.isArray(value) ? value.join(", ") : String(value)]) + ); +} + +function getOrigin(headers) { + const host = headers["x-forwarded-host"] ?? headers.host ?? "localhost"; + const protocol = headers["x-forwarded-proto"] ?? "http"; + return `${protocol}://${host}`; +} + +function getClientIp(headers) { + const forwardedFor = headers["x-forwarded-for"]; + if (forwardedFor) { + return forwardedFor.split(",")[0]?.trim() ?? ""; + } + + return headers["x-real-ip"] ?? ""; +} diff --git a/example/nextjs/next.config.mjs b/example/nextjs/next.config.mjs new file mode 100644 index 00000000..ab2b6cda --- /dev/null +++ b/example/nextjs/next.config.mjs @@ -0,0 +1,12 @@ +const nextConfig = { + webpack(config) { + config.resolve.alias = { + ...(config.resolve.alias ?? {}), + "source-map": false + }; + + return config; + } +}; + +export default nextConfig; diff --git a/example/nextjs/package.json b/example/nextjs/package.json new file mode 100644 index 00000000..f8422393 --- /dev/null +++ b/example/nextjs/package.json @@ -0,0 +1,21 @@ +{ + "name": "nextjs-example", + "private": true, + "version": "3.0.0-dev", + "scripts": { + "dev": "next dev --webpack", + "build": "next build --webpack", + "start": "next start" + }, + "dependencies": { + "@exceptionless/browser": "3.0.0-dev", + "@exceptionless/node": "3.0.0-dev", + "next": "^16.2.1", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "type": "module", + "publishConfig": { + "access": "restricted" + } +} diff --git a/example/nextjs/test/next-request.test.js b/example/nextjs/test/next-request.test.js new file mode 100644 index 00000000..7a5a527c --- /dev/null +++ b/example/nextjs/test/next-request.test.js @@ -0,0 +1,73 @@ +import { describe, expect, test } from "vitest"; + +import { buildRequestContextFromOnRequestError, buildRequestContextFromRequest } from "../lib/next-request.js"; + +describe("next request adapter", () => { + test("builds request info from a web request", () => { + const request = new Request("https://demo.exceptionless.dev/api/demo?mode=log&ref=homepage", { + method: "POST", + headers: { + "content-type": "application/json", + "user-agent": "Vitest Browser", + "x-forwarded-for": "203.0.113.10, 10.0.0.5" + } + }); + + const result = buildRequestContextFromRequest(request, { + mode: "log", + triggeredFrom: "test" + }); + + expect(result).toEqual({ + method: "POST", + secure: true, + ip: "203.0.113.10", + hostname: "demo.exceptionless.dev", + path: "/api/demo", + headers: { + "content-type": "application/json", + "user-agent": "Vitest Browser", + "x-forwarded-for": "203.0.113.10, 10.0.0.5" + }, + params: { + mode: "log", + ref: "homepage" + }, + body: { + mode: "log", + triggeredFrom: "test" + } + }); + }); + + test("builds request info from the onRequestError payload", () => { + const result = buildRequestContextFromOnRequestError({ + path: "/server-component-error?from=test", + method: "GET", + headers: { + host: "localhost:3000", + "user-agent": "Vitest Server", + "x-forwarded-proto": "https", + "x-real-ip": "127.0.0.1" + } + }); + + expect(result).toEqual({ + method: "GET", + secure: true, + ip: "127.0.0.1", + hostname: "localhost", + path: "/server-component-error", + headers: { + host: "localhost:3000", + "user-agent": "Vitest Server", + "x-forwarded-proto": "https", + "x-real-ip": "127.0.0.1" + }, + params: { + from: "test" + }, + body: undefined + }); + }); +}); diff --git a/package-lock.json b/package-lock.json index 955d9984..91fe8b3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,17 @@ "express": "^5.2.1" } }, + "example/nextjs": { + "name": "nextjs-example", + "version": "3.0.0-dev", + "dependencies": { + "@exceptionless/browser": "3.0.0-dev", + "@exceptionless/node": "3.0.0-dev", + "next": "^16.2.1", + "react": "^19.2.4", + "react-dom": "^19.2.4" + } + }, "example/react": { "name": "react-example", "version": "3.0.0-dev", @@ -383,7 +394,6 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1016,35 +1026,549 @@ "@humanwhocodes/retry": "^0.4.0" }, "engines": { - "node": ">=18.18.0" + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.22" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.18" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@jridgewell/gen-mapping": { @@ -1096,6 +1620,152 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@next/env": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.1.tgz", + "integrity": "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.1.tgz", + "integrity": "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.1.tgz", + "integrity": "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.1.tgz", + "integrity": "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.1.tgz", + "integrity": "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.1.tgz", + "integrity": "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.1.tgz", + "integrity": "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.1.tgz", + "integrity": "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.1.tgz", + "integrity": "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@oxc-project/types": { "version": "0.122.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", @@ -1888,6 +2558,15 @@ "vite": "^8.0.0-beta.7 || ^8.0.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@testing-library/react": { "version": "16.3.2", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", @@ -2688,6 +3367,18 @@ "node": "18 || 20 || >=22" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -2777,6 +3468,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2813,6 +3524,12 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2984,7 +3701,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -4416,6 +5133,91 @@ "node": ">= 0.6" } }, + "node_modules/next": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", + "integrity": "sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==", + "license": "MIT", + "dependencies": { + "@next/env": "16.2.1", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.1", + "@next/swc-darwin-x64": "16.2.1", + "@next/swc-linux-arm64-gnu": "16.2.1", + "@next/swc-linux-arm64-musl": "16.2.1", + "@next/swc-linux-x64-gnu": "16.2.1", + "@next/swc-linux-x64-musl": "16.2.1", + "@next/swc-win32-arm64-msvc": "16.2.1", + "@next/swc-win32-x64-msvc": "16.2.1", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/nextjs-example": { + "resolved": "example/nextjs", + "link": true + }, "node_modules/node-localstorage": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-3.0.5.tgz", @@ -5070,7 +5872,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5137,6 +5939,51 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5373,6 +6220,29 @@ "dev": true, "license": "MIT" }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/svelte": { "version": "5.55.0", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.0.tgz", @@ -5592,9 +6462,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", diff --git a/package.json b/package.json index b37f2d6d..0e60eadc 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "3.0.0-dev", "license": "Apache-2.0", "scripts": { - "clean": "rimraf -g packages/*/dist example/*/dist", + "clean": "rimraf -g packages/*/dist example/*/dist example/*/.next", "build": "npm run build --workspaces --if-present", "watch": "npm run watch --workspaces --if-present", "lint": "run-s -c lint:eslint lint:prettier", diff --git a/vitest.config.ts b/vitest.config.ts index 1c4a84a5..b08faf39 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -58,6 +58,13 @@ export default defineConfig({ "@exceptionless/core": path.resolve(__dirname, "packages/core/src") } } + }, + { + test: { + name: "nextjs-example", + root: "example/nextjs", + environment: "node" + } } ] } From 2499e1abe09d0a47a81bbf61ce6d13675d9aca6f Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 25 Mar 2026 08:46:04 -0500 Subject: [PATCH 2/3] Format Next.js example files --- example/nextjs/app/error.jsx | 4 ++-- example/nextjs/app/global-error.jsx | 3 +-- example/nextjs/app/globals.css | 10 ++-------- example/nextjs/app/page.jsx | 9 ++++----- example/nextjs/components/ClientDemoPanel.jsx | 4 ++-- example/nextjs/lib/exceptionless-server.js | 2 +- example/nextjs/lib/next-request.js | 4 +--- 7 files changed, 13 insertions(+), 23 deletions(-) diff --git a/example/nextjs/app/error.jsx b/example/nextjs/app/error.jsx index e5381253..cb546b62 100644 --- a/example/nextjs/app/error.jsx +++ b/example/nextjs/app/error.jsx @@ -30,8 +30,8 @@ export default function ErrorPage({ error, reset }) {

Route Error Boundary

Something inside this route broke.

- Client-only render errors are submitted here. Server-rendered failures already have a digest and are captured by - `instrumentation.js` through `onRequestError`. + Client-only render errors are submitted here. Server-rendered failures already have a digest and are captured by `instrumentation.js` through + `onRequestError`.