Runtime instrumentation primitives for Differ-managed Next.js apps.
This package emits structured explicit-failure envelopes from inside the app to Differ-reserved same-origin endpoints. It does not contain secrets, private API keys, app-specific logic, or server-side Differ implementation.
@differ/next is ESM-only and supports Node.js 20 or newer.
This cut captures explicit app failures:
- browser
errorevents - browser
unhandledrejectionevents - React error-boundary errors
- Next.js App Router global errors
- Next.js
instrumentation.tsrequest errors - Next.js
proxy.tserrors when generated proxy shims report explicitly - manual caught errors
- manual user-visible failures
The boundary is explicit versus implicit. This package does not infer silent failures, stuck loading states, retry loops, data reconciliation issues, or root cause. It reports observations.
Browser reports go to:
/__differ/faults/client
Server reports go to:
/__differ/faults/server
No browser secret, API key, or Sentry-style DSN is required. Generated integration code is expected to install and wire this package for Differ-managed apps; application authors should not need to configure it manually.
Next.js onRequestError reports resolve the server endpoint from the request host. Server-side manual reports should pass an absolute request url in the report context, or generated setup may configure serverOrigin, so a relative reserved endpoint can be resolved safely.
Fault envelopes may include a bounded Differ identity:
identity?: {
differUserId?: string;
clientType?: "human" | "agent";
}identity.differUserId is only the Differ runtime user id in the form
dusr_<uuid>. Invalid values are omitted. Browser reports resolve it in this
order: explicit config or report context, the differ_user_id cookie, then
localStorage["differ_user_id:<appId>"] when appId is configured. The
configured appId is only used to choose that storage key and is not sent in
the envelope.
Server onRequestError reports extract only differ_user_id from the incoming
request's cookie header. Manual server reports may pass differUserId in the
report context. Receiving Differ systems still resolve app attribution from the
runtime host alias and should compare envelope identity with the request cookie
when a cookie is available.
Before the first npm release, generated integration tooling may install from an approved exact git commit:
{
"@differ/next": "github:sky-valley/differ-next#<commit-sha>"
}Use an immutable commit SHA, not a moving branch such as main. Git installs run
the package prepare script so dist/ is built for the consuming app. Published
npm versions remain the preferred release path.
Browser envelopes do not include raw cookies, auth headers, localStorage/sessionStorage dumps, request bodies, DOM snapshots, or arbitrary app state. They may include only the validated bare dusr_* identity id described above.
Server envelopes do not include raw cookies, auth headers, request bodies, or private request headers. Request headers are omitted by default and only explicitly safe headers may be included.
Messages, stacks, URLs, headers, identity fields, and serialized payloads are bounded. Sensitive URL query parameters and sensitive header names are redacted. The package never emits PostHog cookies, PostHog localStorage blobs, or PostHog distinct-id objects.
import { initDifferClient } from "@differ/next/client";
import { reportDifferError, reportDifferFailure } from "@differ/next/report";
import { reportDifferGlobalError } from "@differ/next/global-error";
import { reportDifferSegmentError } from "@differ/next/error-boundary";
import { register, onRequestError } from "@differ/next/instrumentation";
import {
reportDifferProxyError,
withDifferProxyExportReporting,
withDifferProxyReporting,
} from "@differ/next/proxy";Exported TypeScript types are public API.
For a runner-oriented recipe, see Generated Integration Manual.
Generated integration code can initialize browser capture from instrumentation-client.ts:
import { initDifferClient } from "@differ/next/client";
initDifferClient();Generated integration code can delegate server capture from instrumentation.ts:
export { onRequestError, register } from "@differ/next/instrumentation";Generated proxy.ts code should report explicit proxy failures before
rethrowing them. Prefer the wrapper when generated code owns the proxy function:
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { withDifferProxyReporting } from "@differ/next/proxy";
function appProxy(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/internal")) {
return NextResponse.next();
}
return NextResponse.next();
}
export const proxy = withDifferProxyReporting(appProxy);
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};When the app already exports a framework-owned proxy callable, wrap it with the
proxy-export helper. For Auth.js / NextAuth proxy files, do not pass auth to
withDifferProxyReporting; auth is an overloaded framework helper, not a
plain proxy handler:
import { auth } from "@/auth";
import { withDifferProxyExportReporting } from "@differ/next/proxy";
export const proxy = withDifferProxyExportReporting(auth);Keep any existing config export unchanged. If the original proxy file had no
config, do not add one only for Differ. If the original proxy was a default
export, preserve the default-export shape.
Use the lower-level proxy reporter when generated code needs to preserve an
existing try / catch shape:
import type { NextRequest } from "next/server";
import { reportDifferProxyError } from "@differ/next/proxy";
export async function proxy(request: NextRequest) {
try {
return await appProxy(request);
} catch (error) {
await reportDifferProxyError(error, request);
throw error;
}
}Next.js defines onRequestError with a routeType: "proxy" context, and
generated apps should still export instrumentation.ts for the rest of the
server request surface. In the production Next.js proxy path tested here,
however, a bare throw from proxy.ts logs through Next but does not reliably
arrive through onRequestError. Proxy coverage therefore uses explicit
generated proxy reporting. The helper reports an observation and rethrows; it
does not classify root cause or change proxy control flow. It emits
routeType: "proxy", routePath: "/proxy", and
routerKind: "Pages Router" to match the current Next.js native proxy
instrumentation context.
Generated app/global-error.tsx code can report the received error while preserving app-specific UI:
"use client";
import { useEffect } from "react";
import { reportDifferGlobalError } from "@differ/next/global-error";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
reportDifferGlobalError(error, { surface: "app/global-error" });
}, [error]);
return (
<html>
<body>
<button onClick={reset}>Try again</button>
</body>
</html>
);
}Generated app/error.tsx or segment error.tsx code can report boundary errors:
"use client";
import { useEffect } from "react";
import { reportDifferSegmentError } from "@differ/next/error-boundary";
export default function ErrorBoundary({
error,
unstable_retry,
}: {
error: Error & { digest?: string };
unstable_retry: () => void;
}) {
useEffect(() => {
reportDifferSegmentError(error, { surface: "app/error" });
}, [error]);
return <button onClick={unstable_retry}>Try again</button>;
}Later generated code may report explicit caught failures:
import { reportDifferError, reportDifferFailure } from "@differ/next/report";
try {
await submitOrder();
} catch (error) {
reportDifferError(error, {
surface: "checkout",
operation: "submitOrder",
});
}
reportDifferFailure(
{
code: "checkout_unavailable",
message: "Checkout is unavailable",
},
{
surface: "checkout",
operation: "render",
},
);Server-side generated code should pass an absolute request URL when one is available:
import { reportDifferError } from "@differ/next/report";
export async function POST(request: Request) {
try {
return await submitOrder(request);
} catch (error) {
reportDifferError(error, {
surface: "app/api/orders/route",
operation: "POST",
url: request.url,
});
throw error;
}
}Browser transport uses sendBeacon for safe JSON payloads when available and falls back to fetch with keepalive.
Server transport uses fetch with content-type: application/json and a short timeout. Relative server endpoints are resolved against the request host when Next.js provides it.
For server-side manual reports outside onRequestError, relative endpoints are resolved from context.url when it is absolute, or from serverOrigin configured through configureDifferInstrumentation / register. If no safe origin is available, the report is skipped rather than sending an invalid relative URL.
All transport failures are swallowed. In-memory dedupe and rate limiting are best-effort per browser page load and per server module instance. Authoritative dedupe belongs at the receiving Differ endpoint.
npm install
npm run typecheck
npm test
npm run build
npm run check
npm run e2eBuild output is emitted to dist/.
npm run e2e builds a separate Next.js fixture from the packed
@differ/next tarball and drives each explicit capture vector in a browser. The
fixture lives under e2e/, is not part of the TypeScript package build, and is
excluded from the published package by files plus npm run pack:guard.
The e2e fixture intentionally covers both App Router and Pages Router surfaces in one production Next.js app:
- browser
errorandunhandledrejection - manual client errors and failures
- React boundary helpers
- App Router
app/error.tsxandapp/global-error.tsx - App Router server render, route handler, server action, Edge render, and Edge route handler errors
- generated proxy shim reporting through
@differ/next/proxy - Pages Router
getServerSideProps, API route, and_appboundary errors - client and server transport failure swallowing
- browser bundle guardrails so client chunks do not include the server endpoint or server transport markers
The proxy fixture uses the public proxy wrapper and rethrows the original error.
This test proves the generated integration contract, not an assumption that a
bare proxy.ts throw will always pass through Next.js instrumentation in every
production build.