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.tsxvariant, 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
@naispackage registry configured in your.npmrc. This is a one-time setup β follow step 1 of Track frontend errors.
Install ΒΆ
pnpm add @nais/apm@0.4.0
# or: npm install @nais/apm@0.4.0 / yarn add @nais/apm@0.4.0The 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):
// instrumentation-client.ts
import { initNaisAPMClient } from '@nais/apm/react';
initNaisAPMClient({
namespace: 'my-team', // your Nais team β see the note below
});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:
// app/layout.tsx
import { ApmErrorBoundary } from '@nais/apm/react';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="nb">
<body>
<ApmErrorBoundary fallback={<p>Noe gikk galt.</p>}>
{children}
</ApmErrorBoundary>
</body>
</html>
);
}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:
// app/global-error.tsx
'use client';
import { captureException } from '@nais/apm';
import { useEffect } from 'react';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
captureException(error);
}, [error]);
return (
<html lang="nb">
<body>
<h2>Noe gikk galt.</h2>
<button onClick={reset}>PrΓΈv igjen</button>
</body>
</html>
);
}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:
// app/ApmRouteTracker.tsx
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useApmRouteTracking } from '@nais/apm/react';
export function ApmRouteTracker() {
useApmRouteTracking(usePathname(), useSearchParams());
return null;
}// app/layout.tsx (add to the body)
import { Suspense } from 'react';
import { ApmRouteTracker } from './ApmRouteTracker';
// ...inside <body>, alongside {children}:
<Suspense fallback={null}>
<ApmRouteTracker />
</Suspense>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:
// instrumentation-client.ts
initNaisAPMClient({
namespace: 'my-team',
tracing: true,
});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:
// pages/_app.tsx
import type { AppProps } from 'next/app';
import { initNaisAPMClient, ApmErrorBoundary } from '@nais/apm/react';
initNaisAPMClient({ namespace: 'my-team' });
export default function App({ Component, pageProps }: AppProps) {
return (
<ApmErrorBoundary fallback={<p>Noe gikk galt.</p>}>
<Component {...pageProps} />
</ApmErrorBoundary>
);
}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:
// pages/_app.tsx
import { useRouter } from 'next/router';
import { useApmRouteTracking } from '@nais/apm/react';
function App({ Component, pageProps }: AppProps) {
const router = useRouter();
useApmRouteTracking(router.asPath);
// ...render as above
}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:
// next.config.js
module.exports = {
productionBrowserSourceMaps: true,
};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.
Related ΒΆ
- Track frontend errors with
@nais/apmβ the framework-agnostic tutorial and registry setup. - React + Vite quickstart β for React SPAs.
- Migrate from Sentry to
@nais/apmβ call-site-by-call-site. -
@nais/apmAPI reference β every export and option.