Skip to content

sky-valley/differ-next

Repository files navigation

@differ/next

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.

Cut 1 Scope

This cut captures explicit app failures:

  • browser error events
  • browser unhandledrejection events
  • React error-boundary errors
  • Next.js App Router global errors
  • Next.js instrumentation.ts request errors
  • Next.js proxy.ts errors 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.

Defaults

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.

Runtime Identity

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.

Pre-Publish Consumption

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.

Privacy Boundary

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.

Public Entrypoints

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.

Generated Next.js Shims

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;
  }
}

Transport And Dedupe

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.

Development

npm install
npm run typecheck
npm test
npm run build
npm run check
npm run e2e

Build 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 error and unhandledrejection
  • manual client errors and failures
  • React boundary helpers
  • App Router app/error.tsx and app/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 _app boundary 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.

About

Runtime instrumentation primitives for Differ-managed Next.js apps.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors