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/apmNotes
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.ErrorBoundaryApmErrorBoundaryFrom @nais/apm/react.
Sentry.withErrorBoundary(C)withApmErrorBoundary(C)HOC form.
React Router integrationenableApmReactRouterV6() + <ApmRoutes>React Router v6.
Sentry.captureRouterTransitionStart (Next.js)useApmRouteTracking() hookNext.js App Router.
browserTracingIntegration / tracesSampleRateinit({ 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 FeedbackcaptureFeedback()Programmatic, preview/internal-pilot only, no built-in widget.
Sentry Issues UIIssues tab in Nais APMDifferent triage model โ€” triage an issue.
Sentry AlertsGrafana alertsBuilt 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:

sh

Delete the DSN and auth-token wiring while you're here โ€” you won't need them:

  • NEXT_PUBLIC_SENTRY_DSN / SENTRY_DSN env 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:

sh

The 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):

ts

After (main.tsx):

ts

Before (instrumentation-client.ts, real navikt shape):

ts

After (instrumentation-client.ts):

ts

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:

tsx

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>:

tsx

Use the hook in a client component and mount it once in your layout:

tsx

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:

ts

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:

ts

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:

ts

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 returns void โ€” no lastEventId(), so showReportDialog/eventId patterns don't carry over. Details โ†’
  • setUser drops 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 from package.json.
  • Deleted DSN env/secrets (*_SENTRY_DSN) and SENTRY_AUTH_TOKEN.
  • Deleted sentry.*.config.* files and the withSentryConfig wrapper.
  • Added @nais/apm (registry configured in .npmrc, exact version pinned).
  • Swapped Sentry.init โ†’ init() / initNaisAPMClient().
  • Replaced Sentry.ErrorBoundary โ†’ ApmErrorBoundary (and withErrorBoundary โ†’ withApmErrorBoundary).
  • Wired route tracking (enableApmReactRouterV6 + <ApmRoutes>, or useApmRouteTracking).
  • Migrated captureException / captureMessage / setTag / setContext call sites.
  • Confirmed setUser only ever receives an opaque/hashed id โ€” no fnr, email, or ident.
  • Enabled tracing: true if you used browserTracingIntegration (and CORS allows traceparent).
  • Removed sourcemap upload; verified .map files 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.