Agent-readable docs index: /docs/llms.txt. Full docs in one file: /docs/llms-full.txt. Download /docs/docs.zip to grep all markdown files locally.

Vite (React)

@interfere/vite wires Interfere into your Vite build and your running app together. You set it up once: drop in the plugin and start the SDK before render. After that, Interfere records everything the app produces (errors, sessions, traces, and logs) with no sampling and no knobs to turn. It then collapses related symptoms into one problem, weighs how much it matters, and works out the cause for you.
It works for a plain single-page app and for server-rendered setups like TanStack Start. This page covers the install plus the handful of settings most teams actually touch.
You'll need a surface public key (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.

Prerequisites

  • A Vite app using React >= 19
  • Node.js >= 20

Quick start

These four steps cover a single-page app. If you server-render (TanStack Start, for example), do these first, then Server-side rendering.
  1. Install the packages
    npm install @interfere/vite @interfere/react
  2. Add the plugin to your Vite config
    The plugin stamps your public key into the build and, with build.sourcemap on, uploads source maps so a production stack trace points back to your original code instead of minified output.
    vite.config.ts
    import { interfere } from "@interfere/vite/plugin"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; export default defineConfig({ build: { sourcemap: true }, plugins: [react(), interfere()], });
  3. Start the SDK before render
    Call init() before you mount React, so the SDK is capturing the moment your app loads.
    src/main.tsx
    import { init } from "@interfere/vite/init"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { App } from "./app"; init(); createRoot(document.getElementById("root")!).render( <StrictMode> <App /> </StrictMode> );
  4. Add the provider
    Wrap your app once so components can reach the SDK through hooks.
    src/app.tsx
    import { InterfereProvider } from "@interfere/vite/provider"; export function App() { return <InterfereProvider>{/* your routes */}</InterfereProvider>; }
That's the whole install for a single-page app. Run it, trigger an error, and it shows up in your dashboard within seconds, already grouped and triaged. Open your workspace to confirm it. (To control whether the SDK runs in development, see the FAQ.)

Environment variables

VariableRequiredDescription
VITE_INTERFERE_PUBLIC_KEYYesYour surface public key (interfere_pub_<region>_…, where <region> is us or eu). Safe to expose; the plugin stamps it into your build.
INTERFERE_API_KEYFor releasesInterfere API key (interfere_secret_<region>_…). A build-time credential for source-map upload and release metadata.
INTERFERE_ENVIRONMENTNoLabels this build's telemetry and releases (production, staging, preview, or a label you choose). The plugin stamps it into the build, so it needs no VITE_ prefix. Defaults to VERCEL_ENV, then NODE_ENV, then development. See Environments.
Only VITE_-prefixed variables reach the browser. Keep INTERFERE_API_KEY out of any VITE_ name so your secret never ships in the client bundle.

Configuration

The defaults are meant to be left alone, so plenty of apps never open this section. When you do need it, serviceName lives on the init() call from step 3, and what's captured is set on the provider.

Name your app

Running more than one app on Interfere, like a storefront and an admin panel? Give each one a serviceName so its problems, sessions, and metrics stay attributed to it.
src/main.tsx
init({ serviceName: "@acme/storefront" });
path serviceNamestringdefault: interfere-sdk
A stable name for this app. Interfere uses it to keep each surface's data separate, and to correlate the same issue across surfaces into one problem.

Environments

Interfere tags everything it captures with the environment it ran in, and labels each release the same way. In the dashboard, releases carry an environment badge and you can filter by environment, so production data and preview data stay apart.
Set it with INTERFERE_ENVIRONMENT:
INTERFERE_ENVIRONMENT=production
The label is free-form: 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. The plugin stamps the value into your build, so it needs no VITE_ prefix.
Interfere drops local-like environments (development, local, test) before ingestion, so everyday local traffic never reaches your dashboard. A build with no environment set defaults to development and is filtered out. Set a real environment, or turn off environment filtering for the surface, to capture from one.

Custom ingest domain

Point the browser SDK at a subdomain of your own site to survive ad-blocker deny-lists (no server component involved), or at a regional endpoint. Set apiHost on the plugin:
vite.config.ts
import { interfere } from "@interfere/vite/plugin"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; export default defineConfig({ build: { sourcemap: true }, plugins: [react(), interfere({ apiHost: "https://in.yourapp.com" })], });
Add a CNAME pointing your subdomain at the target shown in your dashboard; Interfere provisions TLS automatically. To target a regional endpoint instead, set apiHost (or VITE_INTERFERE_API_URL) to that URL. Ingest requests never carry cookies.

Choose what's captured

Interfere captures every signal below unless you say otherwise. Switch any of them off with the provider's plugins prop, for instance to drop session replay when recordings aren't welcome:
src/app.tsx
<InterfereProvider plugins={{ replay: false }}>{/* your routes */}</InterfereProvider>
path pluginsobject
Each signal can be toggled on or off. All default to on.
  • errors: uncaught exceptions
  • logs: console output
  • device: device and browser info
  • pageEvents: pageviews and clicks
  • rageClick: rage-click detection
  • replay: session replay

Identity

Sessions start out anonymous. Attach the signed-in user and a problem will show you exactly who ran into it in Users, by name and email instead of a random id. Call identity.set() from the useInterfere hook once you have a user:
import { useInterfere } from "@interfere/vite/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 identifierstringrequired
Your internal, stable user ID. Use this rather than the email.
path sourceobjectrequired
Where the identity came from.
  • type: one of clerk, auth0, or custom
  • name: the provider's display name, for example "Clerk"
path namestring
Display name.
path emailstring
Email address.
path avatarstring
Avatar URL.
path traitsobject
Any extra metadata you want attached to the user (Record<string, unknown>).
identity.set() is deduplicated per session, so calling it on every render is fine. Identity clears automatically when the session rotates.

Consent

Everything captures out of the box. To tie that to a cookie banner or a privacy choice, hand the provider a consent object. From then on only essential capture (error tracking and logs) runs, plus whichever categories you switch on:
<InterfereProvider consent={{ analytics: true, replay: false }}> {/* your routes */} </InterfereProvider>
CategoryWhat it coversGateable
necessaryError tracking, logsAlways on
analyticsPage events, rage clicks, device infoYes
replaySession replayYes
Update consent at runtime through the same hook:
const { consent } = useInterfere(); consent.set({ analytics: true, replay: true }); // selective consent.set(); // grant everything
Interfere works with any consent platform (c15t, CookieYes, OneTrust). Map its booleans to the categories above.
Set the 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.

Mask replay data

Session replay records the DOM, so anything on screen can land in a recording. Password inputs are masked for you, and three privacy classes are always active — they're hard-wired, so no config can turn them off. Add a class to any element to redact more; nothing else to set up.
ClassEffect
interfere-blockThe element isn't recorded. It replays as a same-size placeholder. Drops a whole input, widget, or region.
interfere-ignoreThe element still renders, but its input events aren't recorded — keystrokes and changes are never captured.
interfere-maskThe element's text and its children's text is masked. Applies to rendered text, not input values.
<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 */}
To mask input values by type, pass a replay config to the provider's plugins. Only passwords are masked by default; set maskAllInputs: true for a stricter baseline that redacts every input.
src/app.tsx
<InterfereProvider plugins={{ replay: { maskInputOptions: { email: true }, // mask specific input types // maskAllInputs: true, // or mask every input value }, }} > {/* your routes */} </InterfereProvider>
For finer control, 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.

Report a handled error

Uncaught errors are already covered. For one you catch and handle yourself but still want on the record, call capture:
import { capture } from "@interfere/vite/init"; try { await saveDraft(); } catch (error) { capture(error); }

Server-side rendering

If you server-render with TanStack Start (or another Nitro-based setup), do the Quick start steps, then boot server instrumentation and split the browser init() behind a runtime guard. Telemetry posts directly from the browser — set apiHost on the plugin for an ad-blocker-resistant subdomain.
  1. Boot server instrumentation and browser init from the router
    Call register() on the server (OTel bootstrap) and init() in the browser. Guard on typeof window so each half only runs where it should.
    src/router.tsx
    import { init } from "@interfere/vite/init"; import { register } from "@interfere/vite/server"; import { createRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen"; if (typeof window === "undefined") { register().catch(() => {}); } else { init(); } export const getRouter = () => createRouter({ routeTree, scrollRestoration: true });
    You can drop step 3 (init() in main.tsx) from the Quick start — the router handles it now.
  2. Wrap the root route with the provider
    src/routes/__root.tsx
    import { InterfereProvider } from "@interfere/vite/provider"; import { Outlet } from "@tanstack/react-router"; function RootComponent() { return ( <InterfereProvider> <Outlet /> </InterfereProvider> ); }
Report handled errors from the server (route handlers, server functions) with captureError:
import { captureError } from "@interfere/vite/server"; try { await chargeCard(); } catch (error) { captureError(error); throw error; }

FAQ

Should I keep development data out of my dashboard?
The Vite SDK runs whenever it's loaded, including locally. To capture only in production builds, gate it: init({ enabled: import.meta.env.PROD }).
Some telemetry is blocked by ad-blockers on my SPA.
Set apiHost on the plugin 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 needed.
Do you handle the same issue across multiple apps?
Yes. Give each app its own serviceName. When the same issue hits more than one surface, Interfere correlates it into a single problem instead of a separate alert per app.
Can I ship errors only, without analytics or replay?
Yes. Disable the signals you don't want with the provider's plugins prop, for example <InterfereProvider plugins={{ replay: false, pageEvents: false }}>. To drop browser tracing from the bundle entirely, pass init({ tracing: false }).