@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.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.>= 19>= 201npm install @interfere/vite @interfere/react
build.sourcemap on, uploads source maps so a production stack trace points back to your original code instead of minified output.12345678import { interfere } from "@interfere/vite/plugin"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; export default defineConfig({ build: { sourcemap: true }, plugins: [react(), interfere()], });
init() before you mount React, so the SDK is capturing the moment your app loads.12345678910111213import { 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> );
12345import { InterfereProvider } from "@interfere/vite/provider"; export function App() { return <InterfereProvider>{/* your routes */}</InterfereProvider>; }
| Variable | Required | Description |
VITE_INTERFERE_PUBLIC_KEY | Yes | Your surface public key (interfere_pub_<region>_…, where <region> is us or eu). Safe to expose; the plugin stamps it into your build. |
INTERFERE_API_KEY | For releases | Interfere API key (interfere_secret_<region>_…). A build-time credential for source-map upload and release metadata. |
INTERFERE_ENVIRONMENT | No | Labels 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. |
VITE_-prefixed variables reach the browser. Keep INTERFERE_API_KEY out of any VITE_
name so your secret never ships in the client bundle.serviceName lives on the init() call from step 3, and what's captured is set on the provider.serviceName so its problems, sessions, and metrics stay attributed to it.1init({ 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. The plugin stamps the value into your build, so it needs no VITE_ prefix.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.apiHost on the plugin:12345678import { 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" })], });
apiHost (or VITE_INTERFERE_API_URL) to that URL. Ingest requests never carry cookies.plugins prop, for instance to drop session replay when recordings aren't welcome:1<InterfereProvider plugins={{ replay: false }}>{/* your routes */}</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 you have a user:123456789101112131415161718import { 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 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 object. From then on only essential capture (error tracking and logs) runs, plus whichever categories you switch on:123<InterfereProvider consent={{ analytics: true, replay: false }}> {/* your routes */} </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 }, }} > {/* your routes */} </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:1234567import { capture } from "@interfere/vite/init"; try { await saveDraft(); } catch (error) { capture(error); }
init() behind a runtime guard. Telemetry posts directly from the browser — set apiHost on the plugin for an ad-blocker-resistant subdomain.register() on the server (OTel bootstrap) and init() in the browser. Guard on typeof window so each half only runs where it should.12345678910111213import { 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 });
init() in main.tsx) from the Quick start — the router handles it now.12345678910import { InterfereProvider } from "@interfere/vite/provider"; import { Outlet } from "@tanstack/react-router"; function RootComponent() { return ( <InterfereProvider> <Outlet /> </InterfereProvider> ); }
captureError:12345678import { captureError } from "@interfere/vite/server"; try { await chargeCard(); } catch (error) { captureError(error); throw error; }
init({ enabled: import.meta.env.PROD }).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.serviceName. When the same issue hits more than one surface,
Interfere correlates it into a single problem instead of a separate alert per app.plugins prop, for example <InterfereProvider plugins={{ replay: false, pageEvents: false }}>. To drop browser tracing from the bundle entirely, pass init({ tracing: false }).