Install
Add analytics and revenue attribution to a React app
A single-page app breaks most analytics tools' assumptions: there's one HTML document and every 'page' after the first is a history push. The snippet handles that on its own, so there's no router hook to write and nothing to remember when you add a route.
Add the snippet to index.html
Vite and Create React App both serve a single index.html. Put the tag just before </body>; it's the only file you'll touch. Your site key is in Dashboard → Sites → the </> icon.
index.html<body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> <script defer src="https://revtrail.pyln.dev/track.js" data-site="SITE_KEY"></script> </body>Do nothing about routing
React Router, TanStack Router, Wouter, or your own switch statement — they all navigate through the History API, and the script listens for that. Each client-side navigation fires a pageview with the new path. There is no useEffect to write, and no list of routes to keep in sync.
Confirm it's receiving
Open your site in a normal browser tab, then check Realtime in the Revtrail dashboard — your own visit should appear within a few seconds. If nothing arrives, the usual causes are a mistyped site key, an ad blocker on your own browser, or a Content-Security-Policy that blocks the script origin.
Instrument the conversion that matters
Pageviews tell you traffic; a goal tells you whether the traffic worked. Call revtrail() at the moment of signup, trial start, or booking, then register it as a goal in the dashboard. This is the one step people skip, and it's the step that makes every other number worth reading.
anywhere in your client coderevtrail('signup')Wire revenue in from Stripe
Point a Stripe webhook at Revtrail for checkout.session.completed and invoice.payment_succeeded, save the signing secret in your site settings, and pass the visitor id as the Checkout session's client_reference_id. Payments then attach to the visitor's first-touch channel, including renewals months later. Revenue is never accepted from the browser, so this webhook is the only way money reaches your dashboard.
when you create the Checkout sessionconst visitorId = await revtrail.visitorIdAsync(); // best-effort: omit when null, never block checkout on analytics stripe.checkout.sessions.create({ client_reference_id: visitorId ?? undefined, // …line items, success_url, etc. });
Calling revtrail() from a component
The global is installed by the script tag, so it exists on window before your app mounts in practice — but a defer'd script and a fast bundle can race in development. Guard the call rather than asserting the global: window.revtrail?.('signup') never throws and never needs a type declaration beyond a one-line global augmentation.
Hash routing
If you're on a hash router (#/pricing), paths are reported with the hash included. It works, but the fragment isn't sent to servers in normal web requests, so hash paths behave differently from real paths in some tooling. Prefer a history router if you have the choice.
Development noise
Local development fires real events at your production site key if you leave the snippet in your dev index.html. Either use a separate site for development, or open the opt-out URL from your dashboard once in your dev browser — it stores the opt-out choice locally and no identifier.
Questions people actually ask
- Do I need a React SDK or npm package?
- No. There's no package to install for the web — the script tag is the whole integration. React Native is different and has a drop-in client.
- Are client-side route changes really tracked automatically?
- Yes. The script patches pushState and replaceState and listens for popstate, so any router built on the History API produces pageviews without integration code.
- Do I need a cookie banner for this?
- Not for Revtrail. The default identity is a daily rotating hash — no cookies, no persistent identifier, nothing stored on the device. If you run ad pixels or other analytics that do use cookies, their obligations are unchanged.
- Will this slow my site down?
- The script is deferred, so it never blocks rendering, and it's about 3KB over the wire gzipped. It fires one beacon per pageview.
Install guides