Vite + React
A single-page app with React Router (or any router) and no server.
npm install @saaspro/react
1. Environment
# .env
VITE_SPM_KEY=spm_pub_prod_xxxxxxxx
Vite only exposes VITE_-prefixed variables to the bundle. A public key is
designed to live there — never put a secret key in a browser build.
2. Provider
// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import { SaaSProMaxProvider } from "@saaspro/react";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<SaaSProMaxProvider
config={{
key: import.meta.env.VITE_SPM_KEY,
release: __APP_VERSION__,
env: import.meta.env.MODE,
}}
>
<BrowserRouter>
<App />
</BrowserRouter>
</SaaSProMaxProvider>
</StrictMode>,
);
The provider initialises once even under StrictMode's double-effects.
Expose the version from vite.config.ts if you want it on every event:
import { defineConfig } from "vite";
import pkg from "./package.json";
export default defineConfig({
define: { __APP_VERSION__: JSON.stringify(pkg.version) },
});
3. Pageviews
The browser SDK already patches history.pushState / replaceState, so most
routers produce pageviews with no extra code. To drive them explicitly from
React Router:
// src/App.tsx
import { useLocation } from "react-router";
import { SPMPageview, SPMErrorBoundary } from "@saaspro/react";
export default function App() {
const location = useLocation();
return (
<SPMErrorBoundary fallback={(error, reset) => <Crashed error={error} onRetry={reset} />}>
<SPMPageview pathname={location.pathname} search={location.search} />
<Routes>…</Routes>
</SPMErrorBoundary>
);
}
Set autoPageviews: false in the provider config if you drive them yourself and
want to be sure nothing fires twice.
4. Events
import { TrackedButton, TrackOnView, useTrack, useIdentify } from "@saaspro/react";
function Pricing() {
return (
<TrackOnView event="pricing seen" as="section">
<TrackedButton event="cta clicked" properties={{ plan: "pro" }}>
Start free
</TrackedButton>
</TrackOnView>
);
}
function useAuth() {
const identify = useIdentify();
const track = useTrack();
async function signIn(email: string, password: string) {
const user = await api.signIn(email, password);
identify(user.id, { email: user.email, plan: user.plan });
track("signed in", { method: "password" });
return user;
}
…
}
On sign-out:
import { useSaaSProMax } from "@saaspro/react";
const spm = useSaaSProMax();
spm.reset();
5. Feature flags
import { FeatureFlag, useLoadFlags } from "@saaspro/react";
function FlagLoader({ user }: { user: User | null }) {
useLoadFlags(user ? { plan: user.plan, country: user.country } : undefined);
return null;
}
<FeatureFlag flag="new-onboarding" fallback={<OldFlow />}>
<NewFlow />
</FeatureFlag>
Values are cached in localStorage, so a returning visitor renders the last
known variant immediately with no flash.
6. Errors
window errors and unhandled rejections are captured automatically. Add
SPMErrorBoundary for render errors, and report caught ones by hand:
import { useCaptureException } from "@saaspro/react";
const report = useCaptureException();
try {
await risky();
} catch (error) {
report(error, { area: "upload" });
}
To make stack traces readable, build with source maps:
export default defineConfig({ build: { sourcemap: true } });
7. Cookie banner
<SaaSProMaxProvider config={{ key: import.meta.env.VITE_SPM_KEY, consent: "pending" }}>
import { useSaaSProMax } from "@saaspro/react";
function CookieBanner() {
const spm = useSaaSProMax();
return (
<div role="dialog">
<button onClick={() => spm.consent("granted")}>Accept</button>
<button onClick={() => spm.consent("revoked")}>Reject</button>
</div>
);
}
Nothing is sent or stored while consent is pending; granting flushes the buffer.
Checklist
-
VITE_SPM_KEYis a public key (spm_pub_…) - Provider wraps the router, not the other way round
-
spm.reset()on sign-out -
build.sourcemap: truefor readable stack traces - CSP (if any) allows
connect-src https://saaspro.dev