Migrate from Sentry to @nais/apm ยถ
This guide takes a frontend app off Sentry (@sentry/browser, @sentry/react,
@sentry/nextjs, โฆ) and onto @nais/apm,
the Nais browser telemetry SDK. Your errors stop going to sentry.gc.nav.no and
start showing up as issues in Nais APM, on your team's own
Grafana stack.
Who this is for ยถ
You're on this page if your app calls Sentry.init and friends today. Many of
these apps also already run Grafana Faro alongside
Sentry โ @nais/apm is a thin, opinionated wrapper over Faro with a Sentry-like
developer experience, so if that's you, this migration also collapses two SDKs
into one.
The end state is idiomatic @nais/apm with Sentry fully removed: no
@sentry/* dependency, no DSN, no withSentryConfig, no SENTRY_AUTH_TOKEN.
Not a compatibility shim
There is deliberately no Sentry-named drop-in. @nais/apm gives you the
same shapes (captureException, setUser, an error boundary) under its own
names, so you migrate call sites once and delete Sentry for good. A handful of
Sentry features have no equivalent yet โ see
Differences from Sentry before you start.
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.
Concept map ยถ
| Sentry | @nais/apm | Notes |
|---|---|---|
Sentry.init({ dsn, ... }) | init() / initNaisAPMClient() | Zero-config on Nais โ no DSN. App name, version, environment, collector URL all resolve automatically. |
Sentry.captureException(e) | captureException(e, { context, fingerprint }) | Returns void โ no event ID (Limitations). |
Sentry.captureMessage(m, level) | captureMessage(m, level) | Same severity levels. |
Sentry.setUser({ id }) | setUser({ id }) | PII is dropped. Opaque/hashed ids only โ see Privacy. |
Sentry.setUser(null) | clearUser() | On logout. |
Sentry.setTag(k, v) | setTag(k, v) | Approximation โ rides as context, not an indexed label. |
Sentry.setContext(name, obj) | setContext(name, obj) | Flattened as name.key; setContext(name, null) removes it. |
Sentry.setExtra(k, v) | setContext(name, obj) | No setExtra โ fold extras into a named context. |
Sentry.ErrorBoundary | ApmErrorBoundary | From @nais/apm/react. |
Sentry.withErrorBoundary(C) | withApmErrorBoundary(C) | HOC form. |
| React Router integration | enableApmReactRouterV6() + <ApmRoutes> | React Router v6. |
Sentry.captureRouterTransitionStart (Next.js) | useApmRouteTracking() hook | Next.js App Router. |
browserTracingIntegration / tracesSampleRate | init({ tracing: true }) | On/off; auto-instruments fetch/XHR. See trace propagation. |
replayIntegration() | init({ sessionReplay: { enabled: true } }) | Defaults to the events tier (no DOM) โ Limitations. |
ignoreErrors: [...] | init({ ignoreErrors: [...] }) | Appended to DEFAULT_IGNORE_ERRORS (extensions, ResizeObserver, Script error.). |
beforeSend(event) | init({ beforeSend }) | Runs before the mandatory PII scrubber. |
@sentry/webpack-plugin / withSentryConfig sourcemap upload | (nothing to configure) | Stack traces resolve server-side from the CDN โ sourcemaps. |
Sentry.showReportDialog / User Feedback | captureFeedback() | Programmatic, preview/internal-pilot only, no built-in widget. |
| Sentry Issues UI | Issues tab in Nais APM | Different triage model โ triage an issue. |
| Sentry Alerts | Grafana alerts | Built from templates, delivered via Grafana contact points. |
release: process.env.โฆ | version (auto) | Resolved from GITHUB_SHA / image tag; joins deploy markers. |
Step-by-step recipe ยถ
1. Remove Sentry ยถ
Drop every @sentry/* package and its config from package.json:
npm uninstall @sentry/browser @sentry/react @sentry/nextjs @sentry/webpack-pluginDelete the DSN and auth-token wiring while you're here โ you won't need them:
-
NEXT_PUBLIC_SENTRY_DSN/SENTRY_DSNenv and secrets -
SENTRY_AUTH_TOKEN(CI secret used for sourcemap upload) - any
sentry.client.config.*/sentry.server.config.*/sentry.edge.config.*
2. Add @nais/apm ยถ
@nais/apm is published to the GitHub Package Registry. Configure the registry
(one-time, in .npmrc) and install โ the full steps are in
Track frontend errors:
npm install @nais/apm@0.4.0The React helpers (error boundary, route tracking, Next.js client init) live in
the @nais/apm/react entry point.
3. Swap Sentry.init โ init() ยถ
There is no DSN and, on Nais, nothing to configure โ init() reads the app name,
version, environment, team, and collector URL from the Nais
meta tags / NAIS_* env.
Before (initSentry.ts, real navikt shape):
import * as Sentry from '@sentry/browser';
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
release: import.meta.env.VITE_SENTRY_RELEASE,
environment: globalThis.location.hostname,
integrations: [Sentry.breadcrumbsIntegration({ console: false })],
ignoreErrors: ['TypeError: Failed to fetch'],
beforeSend: (event) => (isNoise(event) ? null : event),
});After (main.tsx):
import { init } from '@nais/apm';
init({
// app / version / environment / namespace all resolve from Nais โ omit them.
ignoreErrors: [/Failed to fetch/],
beforeSend: (item) => (isNoise(item) ? null : item),
});Before (instrumentation-client.ts, real navikt shape):
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
release: process.env.NEXT_PUBLIC_APP_VERSION,
allowUrls: ['arbeidsplassen.nav.no'],
ignoreErrors: ['TypeError: Failed to fetch'],
beforeSend(event, hint) { return isNoise(hint) ? null : event; },
});
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;After (instrumentation-client.ts):
import { initNaisAPMClient } from '@nais/apm/react';
initNaisAPMClient({
namespace: 'my-team', // or resolved from a <meta name="nais-team"> tag / NAIS_TEAM
ignoreErrors: [/Failed to fetch/],
tracing: true, // optional โ see step 5
});initNaisAPMClient no-ops on the server and is idempotent under React Strict
Mode, so it's safe to import from instrumentation-client.ts (Next 15+) or a
Pages Router _app.tsx. Route tracking replaces onRouterTransitionStart โ
see step 4.
`beforeSend` semantics changed
In Sentry your beforeSend was the last word. In @nais/apm it runs first;
the mandatory PII scrubber always runs last and cannot be removed (only
dangerouslyDisablePiiScrubbing: true disables it, and then your team owns the
GDPR consequences). You usually no longer need allowUrls / manual noise
filters for extensions and ResizeObserver โ those are in DEFAULT_IGNORE_ERRORS.
4. Replace the error boundary and route tracking ยถ
Error boundary โ swap Sentry.ErrorBoundary for ApmErrorBoundary:
// Before
import * as Sentry from '@sentry/react';
<Sentry.ErrorBoundary fallback={<TekniskFeilSide />}>
<App />
</Sentry.ErrorBoundary>
// After
import { ApmErrorBoundary } from '@nais/apm/react';
<ApmErrorBoundary fallback={<TekniskFeilSide />}>
<App />
</ApmErrorBoundary>ApmErrorBoundary reports through captureException exactly once, so caught
render errors get the SDK's fingerprint/context pipeline. The withApmErrorBoundary(Component, props?)
HOC mirrors Sentry.withErrorBoundary. The fallback can also be a render prop
(error, resetError) => node.
Route tracking:
Call once after init(), passing your own react-router-dom exports, then
render <ApmRoutes> where you rendered <Routes>:
import {
createRoutesFromChildren, matchRoutes, Routes, useLocation, useNavigationType,
} from 'react-router-dom';
import { enableApmReactRouterV6, ApmRoutes } from '@nais/apm/react';
enableApmReactRouterV6({
createRoutesFromChildren, matchRoutes, Routes, useLocation, useNavigationType,
});
// <ApmRoutes> instead of <Routes>Use the hook in a client component and mount it once in your layout:
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useApmRouteTracking } from '@nais/apm/react';
export function ApmRouteTracker() {
useApmRouteTracking(usePathname(), useSearchParams());
return null;
}React Router v5/v7 and data routers are not wired yet
Route tracking currently covers React Router v6 and the Next.js App Router only. See Limitations.
5. Tracing (optional) ยถ
If you used browserTracingIntegration / tracesSampleRate, enable tracing with
one flag. @grafana/faro-web-tracing is lazily loaded so it stays out of your
bundle unless you turn it on:
init({ tracing: true });Trace headers are propagated to your own origin and *.nav.no backends by
default (a non-overridable security floor). Add more with
init({ tracing: { propagateExtraOrigins: ['https://api.partner.example'] } }).
Your backend still needs to allow the traceparent CORS header โ see
frontend-to-backend trace propagation.
6. Delete sourcemap upload ยถ
Remove the entire @sentry/webpack-plugin / withSentryConfig sourcemap-upload
step and the SENTRY_AUTH_TOKEN it needed:
// next.config.ts โ DELETE the withSentryConfig wrapper
const nextConfig = withSentryConfig(baseConfig, {
org: 'nav',
project: 'my-app',
sentryUrl: 'https://sentry.gc.nav.no/',
authToken: process.env.SENTRY_AUTH_TOKEN,
});
export default nextConfig; // -> export default baseConfig;There is nothing to replace it with. The Nais telemetry collector resolves
minified stack traces server-side by fetching your .map files from the CDN
(cdn.nav.no) at read time. You only need to emit sourcemaps and deploy them
alongside your bundle โ no upload, no auth token. Follow
Sourcemap deobfuscation for the build settings and the CDN
requirement.
7. Replace Sentry Replay ยถ
If you ran replayIntegration(), use sessionReplay:
init({ sessionReplay: { enabled: true, mode: 'on-error' } });This defaults to the events tier โ a DOM-free interaction timeline, safe by construction. It is not a like-for-like replacement for Sentry's full DOM recording; opting into DOM capture is a deliberate, personvernombud-gated decision. Read the session replay limitations and Enable session replay before you turn anything on.
8. Move alerting to Grafana ยถ
Sentry alert rules don't come across. Recreate them as Grafana alerts from Nais APM templates (error rate, exception spike, new exceptions, web vitals) โ delivered through your team's Grafana contact points.
Differences from Sentry ยถ
@nais/apm is deliberately not a drop-in for the whole Sentry API. Before you
delete @sentry/*, read the canonical
Limitations & differences from Sentry
โ it covers the unsupported APIs (addBreadcrumb, scopes, manual spans,
setExtra/setExtras, lastEventId, showReportDialog, withProfiler,
React Router v5/v7), the source-map and replay model, and more.
The two gotchas that bite migrations hardest:
Two headline gotchas
- No event ID from
captureException. It returnsvoidโ nolastEventId(), soshowReportDialog/eventId patterns don't carry over. Details โ -
setUserdrops PII. Only opaque/hashed ids survive; email/idents/fnr are silently dropped. Details โ
A few migration-specific pointers, each covered in full on its own page:
- Issues show up in the Grafana Issues tab, not Sentry's stream โ the triage model (Resolve / Ignore / Assign, Regressed, personal mute) is explained in Triage an issue. Grouping is driven by fingerprinting.
- Alerts are Grafana alerts seeded from Nais APM templates โ Sentry's alert rules and routing don't migrate.
- Source maps resolve server-side; there's nothing to upload. See Sourcemap deobfuscation.
Migration checklist ยถ
- Removed every
@sentry/*dependency frompackage.json. - Deleted DSN env/secrets (
*_SENTRY_DSN) andSENTRY_AUTH_TOKEN. - Deleted
sentry.*.config.*files and thewithSentryConfigwrapper. - Added
@nais/apm(registry configured in.npmrc, exact version pinned). - Swapped
Sentry.initโinit()/initNaisAPMClient(). - Replaced
Sentry.ErrorBoundaryโApmErrorBoundary(andwithErrorBoundaryโwithApmErrorBoundary). - Wired route tracking (
enableApmReactRouterV6+<ApmRoutes>, oruseApmRouteTracking). - Migrated
captureException/captureMessage/setTag/setContextcall sites. - Confirmed
setUseronly ever receives an opaque/hashed id โ no fnr, email, or ident. - Enabled
tracing: trueif you usedbrowserTracingIntegration(and CORS allowstraceparent). - Removed sourcemap upload; verified
.mapfiles ship to the CDN (sourcemaps). - Recreated alerts as Grafana alerts.
- Audited Limitations & differences from Sentry and logged any gaps.
- Deployed, triggered a test error, and confirmed it appears in the Issues tab.
Related ยถ
- Track frontend errors with
@nais/apmโ the from-scratch tutorial. -
@nais/apmAPI reference โ every export and option. - Sourcemap deobfuscation โ how stack traces resolve.