@interfere/next connects your Next.js app to Interfere. It's a one-time install: add a few lines, ship as usual, and Interfere captures everything your app emits (errors, sessions, traces, and logs) with no sampling and nothing to tune. From there, Interfere groups related symptoms into a single problem, decides how much it matters, and investigates the cause for you.interfere_pub_<region>_…) from Surfaces. For source-map upload and release tracking you'll also want an Interfere API key (interfere_secret_<region>_…). See Environment variables.>= 16 (App Router)>= 19>= 201npm install @interfere/next
123456import { withInterfere } from "@interfere/next/config"; import type { NextConfig } from "next"; const config: NextConfig = {}; export default withInterfere(config);
register() on server start.1export { onRequestError, register } from "@interfere/next/instrumentation";
123456789101112131415import { InterfereProvider } from "@interfere/next/provider"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body> <InterfereProvider>{children}</InterfereProvider> </body> </html> ); }
| Variable | Required | Description |
INTERFERE_PUBLIC_KEY | Yes | Your surface public key (interfere_pub_<region>_…, where <region> is us or eu). Safe to expose; it only authorizes ingestion. |
INTERFERE_API_KEY | For releases | Interfere API key (interfere_secret_<region>_…). A server-side build credential for source-map upload and release metadata. Never expose it in browser code. |
INTERFERE_ENVIRONMENT | No | Labels this deployment's telemetry and releases (production, staging, preview, or a label you choose). Read at build time and stamped into the bundle, so it needs no NEXT_PUBLIC_ prefix. Defaults to VERCEL_ENV, then NODE_ENV, then development. See Environments. |
INTERFERE_API_URL | No | Override the ingest endpoint (for example, to target a regional endpoint). Defaults to Interfere's endpoint. See Custom ingest domain. |
INTERFERE_API_KEY is a secret. Keep it in server and CI environments only. The public key
(INTERFERE_PUBLIC_KEY) is the only key that reaches the browser.serviceName so problems, sessions, and metrics are attributed to the right one. Set it in an optional instrumentation-client.ts:123import { init } from "@interfere/next/instrument-client"; init({ serviceName: "@acme/storefront" });
path serviceNamestringdefault: interfere-sdkINTERFERE_ENVIRONMENT:1INTERFERE_ENVIRONMENT=production
production, staging, preview, canary, or whatever you name it. If you don't set it, Interfere falls back to VERCEL_ENV, then NODE_ENV, then development. On Vercel you usually get the right value without doing anything.NEXT_PUBLIC_ prefix.development, local, test) before ingestion, so everyday local traffic never reaches your dashboard. An app with no environment set defaults to development and is filtered out, which is one reason nothing shows up until you deploy. Set a real environment, or turn off environment filtering for the surface, to capture from one.123456789import { withInterfere } from "@interfere/next/config"; import type { NextConfig } from "next"; const config: NextConfig = {}; export default withInterfere({ interfere: { apiHost: "https://in.yourapp.com" }, ...config, });
apiHost (or the INTERFERE_API_URL env var) to that URL. Ingest requests never carry cookies.plugins prop. For example, disable session replay if you don't want recordings:1<InterfereProvider plugins={{ replay: false }}>{children}</InterfereProvider>
path pluginsobjecterrors: uncaught exceptionslogs: console outputdevice: device and browser infopageEvents: pageviews and clicksrageClick: rage-click detectionreplay: session replayidentity.set() from the useInterfere hook once your user loads:123456789101112131415161718import { useInterfere } from "@interfere/next/provider"; function SyncIdentity() { const { identity } = useInterfere(); const { user } = useAuthProvider(); // Clerk, Auth0, etc. useEffect(() => { if (!user) return; identity.set({ identifier: user.id, name: user.name, email: user.email, source: { type: "clerk", name: "Clerk" }, }); }, [user]); return null; }
path identifierstringrequiredpath sourceobjectrequiredtype: one of clerk, auth0, or customname: the provider's display name, for example "Clerk"path namestringpath emailstringpath avatarstringpath traitsobjectRecord<string, unknown>).identity.set() is deduplicated per session, so calling it on every render is fine. Identity
clears automatically when the session rotates.consent to the provider. Once you do, only essential capture (error tracking and logs) plus the categories you opt into will run:1<InterfereProvider consent={{ analytics: true, replay: false }}>{children}</InterfereProvider>
| Category | What it covers | Gateable |
necessary | Error tracking, logs | Always on |
analytics | Page events, rage clicks, device info | Yes |
replay | Session replay | Yes |
1234const { consent } = useInterfere(); consent.set({ analytics: true, replay: true }); // selective consent.set(); // grant everything
consent prop from the provider's first render so a non-consented feature never gets a
chance to load. Update it through the hook as the user changes their mind.| Class | Effect |
interfere-block | The element isn't recorded. It replays as a same-size placeholder. Drops a whole input, widget, or region. |
interfere-ignore | The element still renders, but its input events aren't recorded — keystrokes and changes are never captured. |
interfere-mask | The element's text and its children's text is masked. Applies to rendered text, not input values. |
123<input className="interfere-block" /> {/* whole element dropped */} <input className="interfere-ignore" /> {/* input events not recorded */} <span className="interfere-mask">$128,400.00</span> {/* text + children masked */}
replay config to the provider's plugins. Only passwords are masked by default; set maskAllInputs: true for a stricter baseline that redacts every input.12345678910<InterfereProvider plugins={{ replay: { maskInputOptions: { email: true }, // mask specific input types // maskAllInputs: true, // or mask every input value }, }} > {children} </InterfereProvider>
plugins.replay also forwards raw rrweb options — maskTextSelector / blockSelector / ignoreSelector to target elements by CSS selector, maskInputFn / maskTextFn to transform masked values, and recordCanvas. These are additive; the interfere-* classes still apply.capture on the client or captureError on the server.1234567import { capture } from "@interfere/next/instrument-client"; try { await saveDraft(); } catch (error) { capture(error); }
12345678910import { captureError } from "@interfere/next/server"; export async function placeOrder() { try { await chargeCard(); } catch (error) { captureError(error); throw error; } }
NODE_ENV !== "production", so local noise doesn't reach your dashboard. To capture while testing, call init({ enabled: true }) in instrumentation-client.ts. Interfere also drops the development, local, and test environments before ingestion, so set a real environment too if you want that data to land.serviceName. When the same issue hits more than one surface,
Interfere correlates it into a single problem instead of a separate alert per app.apiHost to a subdomain of your own site — a CNAME to the target shown in your dashboard. Telemetry then posts to your own origin, no server route or middleware needed.plugins prop, for example <InterfereProvider plugins={{ replay: false, pageEvents: false }}>. To drop browser tracing from the bundle entirely, pass init({ tracing: false }).