Skip to Content
DocsV1 Error Tracking

V1 Error Tracking

This guide is the north-star DX for setting up useanalytics error tracking in a modern Next.js App Router app.

It assumes you already completed V1 getting started.

This guide focuses on:

  1. Instrumentation hook setup (server startup)
  2. Global error capture for App Router
  3. Minimal captureException(...) usage

Source maps are intentionally out of scope for this guide and will be covered separately.

captureException is a dedicated error API and should work without distinctId. Use optional additional properties for context, similar to: captureException(error, additionalProperties).

Prerequisites

Before starting, make sure you already have:

  • lib/analytics.ts (server instance)
  • lib/analytics-client.ts (client instance)
  • /api/analytics/* route handler mounted

If not, complete V1 getting started first.

1) Enable error tracking in lib/analytics.ts

Enable error tracking where you create your analytics server instance:

import { useAnalytics } from "useanalytics"; import { drizzleAdapter } from "useanalytics/adapters/drizzle"; import { errorTracking } from "useanalytics/plugins"; import { db } from "@/lib/db"; export const analytics = useAnalytics({ database: drizzleAdapter(db), plugins: [errorTracking()], });

Then generate/apply migrations as part of your normal setup flow:

pnpm dlx @useanalytics/cli@latest generate

When the plugin is enabled, the CLI should include the error-tracking tables.

2) Add an instrumentation hook (Node + Edge)

Create instrumentation.ts at your project root (or in src/ if your app uses a src layout).

Use runtime-aware imports so setup works in both Node.js and Edge runtimes.

export async function register() { if (process.env.NEXT_RUNTIME === "nodejs") { await import("./instrumentation-node"); } if (process.env.NEXT_RUNTIME === "edge") { await import("./instrumentation-edge"); } }

Why this shape:

  • register() runs once per server instance startup.
  • Dynamic imports keep side effects explicit and runtime-safe.
  • This matches the modern Next.js instrumentation convention.
  • Edge support is essential for complete coverage in modern Next.js apps.

3) Create runtime-specific instrumentation files

Create instrumentation-node.ts for server runtime setup.

import { analytics } from "@/lib/analytics"; /** * Place one-time server startup hooks here: * - initialize runtime integrations if needed * - register process-level handlers (Node runtime) */ void analytics;

Create instrumentation-edge.ts for edge runtime setup.

import { analyticsEdge } from "@/lib/analytics-edge"; /** * Register global handlers in the edge runtime so uncaught errors * are captured with useanalytics. */ addEventListener("unhandledrejection", (event) => { void analyticsEdge.captureException(event.reason, { source: "edge/unhandledrejection", }); }); addEventListener("error", (event) => { void analyticsEdge.captureException(event.error ?? event.message, { source: "edge/error", }); });

Create lib/analytics-edge.ts:

import { createAnalyticsEdge } from "useanalytics/next-js"; export const analyticsEdge = createAnalyticsEdge({ ingestPath: "/api/analytics/ingest", });

Why this shape:

  • Uses the API provided by useanalytics.
  • Keeps edge code edge-safe.
  • Keeps app code minimal and familiar.

4) Add App Router global error capture

Create app/global-error.tsx to capture render/runtime errors that reach the app-level error boundary.

"use client"; import { useEffect } from "react"; import { analyticsClient } from "@/lib/analytics-client"; type GlobalErrorProps = { error: Error & { digest?: string }; reset: () => void; }; export default function GlobalError({ error, reset }: GlobalErrorProps) { useEffect(() => { void analyticsClient.captureException(error, { source: "app/global-error", digest: error.digest, }); }, [error]); return ( <html lang="en"> <body> <h2>Something went wrong</h2> <button type="button" onClick={() => reset()}> Try again </button> </body> </html> ); }

Why this matters:

  • Captures real production crashes users hit.
  • Keeps capture logic centralized and maintainable.
  • Preserves a familiar workflow for teams coming from Sentry-like setups.

5) Optional: capture handled exceptions manually

Not all errors bubble to global-error.tsx. For caught exceptions in Server Actions or business logic, track explicitly.

"use server"; import { analytics } from "@/lib/analytics"; export async function createProjectAction() { try { // your business logic } catch (error) { await analytics.captureException(error, { pathname: "/projects/new", source: "server_action", }); throw error; } }

This pattern ensures handled errors are still observable.

6) Verify your setup

Run the app and trigger both error paths:

  1. Throw from a client component to verify app/global-error.tsx capture.
  2. Throw inside a Server Action try/catch block to verify server-side capture.

You should see both exception events in your useanalytics data.

When calling captureException(error, additionalProperties), pass stable metadata:

  • source (app/global-error, server_action, edge, etc.)
  • pathname (when known)
  • digest (when available in App Router)

The SDK should derive core exception details (message, name, stack) from error.

Notes

  • Prefer a single instrumentation.ts entrypoint with runtime-specific imports.
  • Treat Edge as a first-class runtime, not an afterthought.
  • Use the provided captureException(...) APIs directly in app code.
  • Keep this setup lightweight first, then add richer pipelines (for example source maps) incrementally.
  • captureException(...) should not require distinctId.
Last updated on