Next.js quickstart with @nais/apm ΒΆ

Instrument a Next.js frontend with @nais/apm, the Nais browser telemetry SDK, and see your errors as issues in Nais APM. Next.js is the most common frontend framework on Nais, so this is the fast path: client init, a React error boundary, route tracking, and β€” if you want it β€” distributed tracing.

This guide covers both routers:

  • App Router (app/) β€” the default since Next.js 13 and what most Next apps here use. Start here.
  • Pages Router (pages/) β€” the _app.tsx variant, including the Nais golden-path template. See the Pages Router section for the differences.

Status: pre-release

@nais/apm is pre-1.0. Pin an exact version and read the CHANGELOG before upgrading. This guide targets 0.4.0.

Migrating off Sentry?

If your app calls Sentry.init today, follow Migrate from Sentry to @nais/apm instead β€” it maps every @sentry/nextjs call site to its @nais/apm equivalent and lists what's different.

Prerequisites ΒΆ

  • A Next.js application (15 or newer for instrumentation-client.ts; the Pages Router pattern works on older versions too) deployed on Nais.
  • The @nais package registry configured in your .npmrc. This is a one-time setup β€” follow step 1 of Track frontend errors.

Install ΒΆ

sh

The React helpers used below β€” initNaisAPMClient, ApmErrorBoundary, useApmRouteTracking β€” live in the @nais/apm/react entry point.

App Router ΒΆ

1. Initialize the client ΒΆ

On Next.js 15+, the client instrumentation hook instrumentation-client.ts runs once, early, in the browser β€” the ideal place to initialize telemetry. Create it at your project root (next to next.config.js):

ts

initNaisAPMClient no-ops on the server (typeof window === 'undefined') and is idempotent, so it's safe under React Strict Mode's double-invoke and any repeated imports. Everything else β€” app name, version, environment, and the collector URL β€” resolves automatically from Nais meta tags or NAIS_* build env. On localhost, where no collector resolves, it sends nothing and echoes every signal to the console.

`namespace` is your team

namespace is the Nais team that owns the app (its Kubernetes namespace). Nais APM attributes all telemetry by team, so it's effectively required. Pass it explicitly as shown, or let it resolve from a <meta name="nais-team" content="my-team"> tag or the NAIS_TEAM env β€” then you can drop the option entirely.

2. Add an error boundary ΒΆ

Wrap your app in ApmErrorBoundary in the root layout. It reports each caught render error exactly once through captureException β€” so it picks up the SDK's fingerprint and context β€” and renders a fallback:

tsx

ApmErrorBoundary is a client component, but it accepts your Server Component children as-is β€” no 'use client' needed in the layout. The fallback can also be a render prop (error, resetError) => node, and withApmErrorBoundary(Component, props?) gives you the HOC form.

Next.js also has its own route-segment error boundaries (error.tsx, global-error.tsx). These are separate from ApmErrorBoundary β€” to report errors they catch, call captureException from them:

tsx

3. Track route changes ΒΆ

The App Router does client-side navigation without full page loads, so pathname changes need to be reported explicitly. There's no faro-react App Router integration; @nais/apm ships a small hook, useApmRouteTracking, that you feed Next's navigation values. Put it in a client component and mount it once in the layout:

tsx
tsx

Wrap the tracker in `<Suspense>`

useSearchParams() opts a component into client-side rendering. Wrapping the tracker in a <Suspense> boundary keeps that opt-in from bubbling up and forcing the rest of the page to render client-side during static generation.

4. Enable tracing (optional) ΒΆ

To connect browser spans to your backend traces in Tempo, turn on tracing in the init call. @grafana/faro-web-tracing is lazily loaded, so it stays out of your bundle unless you enable it:

ts

Trace headers are propagated to your own origin and *.nav.no backends by default (a non-overridable security floor). Add more origins with tracing: { propagateExtraOrigins: ['https://api.partner.example'] }. Your backend still has to allow the traceparent CORS header and export spans to Tempo β€” see frontend-to-backend trace propagation.

Pages Router ΒΆ

The Pages Router (pages/) β€” including navikt's golden-path Next.js template β€” works the same way, with two differences: where you initialize and how you track routes.

Initialize in _app.tsx ΒΆ

There's no instrumentation-client.ts, so call initNaisAPMClient at module scope in your custom App. It no-ops on the server and is idempotent, so top-level is fine:

tsx

Track routes with the router events ΒΆ

The Pages Router exposes navigation through next/router events instead of usePathname(). Feed the new path into the same hook:

tsx

Tracing (tracing: true) and the error boundary work identically to the App Router.

Ship readable stack traces ΒΆ

Stack traces resolve server-side from sourcemaps on the CDN β€” nothing to upload (how it works). You just have to emit them. For Next.js:

js

Server-rendered assets don't get deobfuscated

Resolution only works for bundles served from the CDN. A purely server-rendered Next app that serves its own JS from the pod won't get resolved stack traces unless it also ships static assets to the CDN. See Sourcemaps β†’ Requirements.

Don't send personal data ΒΆ

Telemetry lands in a shared Loki instance every team can read, so identities must never reach it. setUser takes an opaque/hashed id only β€” emails, idents, and fΓΈdselsnummer are dropped in code β€” and a mandatory PII scrubber runs over every outgoing signal. See Privacy: PII scrubbing for the full rules.

See your errors as issues ΒΆ

Deploy, then trigger an error. Open your service in Nais APM and go to the Issues tab. Your exception shows up as an issue, grouped with other occurrences, with its (deobfuscated) stack trace and impact. Filter the source to Browser to isolate frontend issues.

From there you can triage it or create an alert.