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:
- Instrumentation hook setup (server startup)
- Global error capture for App Router
- 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 generateWhen 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:
- Throw from a client component to verify
app/global-error.tsxcapture. - Throw inside a Server Action
try/catchblock to verify server-side capture.
You should see both exception events in your useanalytics data.
Recommended exception metadata
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.tsentrypoint 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 requiredistinctId.