Product analytics for React
Pug’s Web SDK is plain framework-agnostic JavaScript, so it works in any React app: Vite, Create React App, or anything else. Call init() once where your app boots, before the router mounts, and autocapture takes over.
Add Pug to React
Four steps, start to first insight. Everything below uses the Web SDK exactly as it ships. There is no React wrapper in between.
-
Install the Web SDK
No provider package, no hook library, no peer dependency on React. The bundler build ships compiled JavaScript and type definitions from the package root.
npm install @pug-sh/browser # or: pnpm add @pug-sh/browser · yarn add @pug-sh/browser · bun add @pug-sh/browser -
Initialize at your entry point
Call init() once, at the top of the module that boots your app: before createRoot() and before your router mounts, so the first page view is recorded and the History wrappers are in place for the first navigation.
src/main.tsximport { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { init } from '@pug-sh/browser' import App from './App' init(import.meta.env.VITE_PUG_PROJECT_ID, { apiKey: import.meta.env.VITE_PUG_PUBLIC_KEY, }) createRoot(document.getElementById('root')!).render( <StrictMode> <App /> </StrictMode>, ) -
Identify on sign-in, reset on sign-out
identify() attaches everything the anonymous visitor already did to their profile, and keeps it joined on their next device. reset() is its counterpart: it clears the identity for a logout or account switch so the next person on a shared machine does not inherit it.
src/auth.tsimport { identify, reset } from '@pug-sh/browser' export async function onSignIn(user: { id: string; email: string; plan: string }) { await identify(user.id, { email: user.email, plan: user.plan }) } export function onSignOut() { reset() } -
Track the events autocapture can’t name
Autocapture knows a click happened; it does not know it was a trial upgrade. Name the moments your funnels are built from: the well-known kinds are generated from the event registry, so a spelling that matches gets first-class treatment in the dashboard.
src/components/UpgradeButton.tsximport { track } from '@pug-sh/browser' export function UpgradeButton({ plan }: { plan: string }) { return ( <button onClick={() => track('subscription_started', { plan })}> Upgrade </button> ) }
What to watch for in React
The specifics that decide whether the numbers you see are the numbers that happened.
Client-side routing is already covered
React Router, TanStack Router, and anything else built on the History API navigate through pushState/replaceState. The SDK wraps both, plus popstate, so a route change in a React SPA sends a page_view with no extra wiring.
StrictMode double-invocation is safe
A second init() no-ops with a console warning rather than installing a second set of listeners, so the development-only double render does not double-count. Calling it at module scope rather than in an effect avoids the warning entirely.
Autocapture is an allowlist, not switches
Passing autoCapture: { pageView: true, click: true } enables exactly those two: every omitted tracker stays off. There is no "everything except one" form, which is why the type rejects an explicit false.
Cookieless until someone answers
The default consent state writes no identifier to the device: events still flow, attributed to a server-derived identity that rotates daily. Wire a banner to optInTracking() / optOutTracking() when you want the full-identity version.
Product analytics in your React app
-
Autocapture, six trackers
Page views, clicks, scrolls, form starts and submits, rage clicks, and dead clicks: all on after one init(), or narrowed with a per-listener allowlist.
-
One profile per person
identify() merges the anonymous session into the signed-in profile and keeps it joined across devices and later visits.
-
Your own events
track() takes a name and typed properties, with a generated list of well-known event kinds so your names match what the insights expect.
-
Every insight type
Trends, funnels, retention cohorts, segmentation, user flows, and top-K run over the same events, no separate instrumentation per report.
Read it in the docs
The Web SDK reference goes deeper than a setup page can: every option, every event, and the edge cases.
Pug + React: common questions
Do I need a React-specific package?
No. There is one Pug Web SDK and it is framework-agnostic. In React you import @pug-sh/browser and call init() once at startup, no provider or hook library required.
Where should I call init()?
At your app’s entry point (e.g. main.tsx), at module scope so it runs once before the tree renders and before your router mounts. Autocapture then records page views, clicks, scrolls, and form interactions; call identify() on sign-in and track() for custom events.
Does it support single-page-app route changes?
Yes. The SDK wraps history.pushState and history.replaceState and listens for popstate, so any router built on the History API (React Router, TanStack Router, and the rest) produces a page_view on every client-side navigation without extra wiring.
Will React StrictMode double-count events?
No. init() no-ops with a warning if it is called a second time, so StrictMode’s development double-invocation does not install duplicate listeners. Track calls inside effects follow ordinary React rules: the usual guards apply there as with any side effect.
Can I keep form fields and URLs out of the payload?
Yes. Form autocapture records metadata only (form id, name, and the action on submit), never field values. Known-sensitive query and fragment params are redacted from URLs by default, and beforeSend gives you a synchronous hook to mask, rewrite, or drop any event before it leaves the browser.
Add Pug to your React app.
Open-source product analytics with unified profiles. Self-host under AGPL-3.0, or use the free cloud during open beta.
Questions? Email hello@pug.sh