Docs

04 · Docs

React analyticswith juuna.

Init once in main.tsx, one effect for React Router, and every view is counted. Everything on this page is copy-paste correct for this instance.

Plain markdown for AI agents · /docs/react/llms.txt

llms.txt

React analytics with juuna

Last updated · 2026-08-31

TL;DR: to add analytics to a React single-page app built with Vite, install @muziris/juuna, call juuna.init once in main.tsx, and record a pageview with juuna.page() on every route change. Events appear in your juuna dashboard within seconds.

juuna is product analytics on a dedicated instance of your own: traffic, funnels, heatmaps, session replay and Web Vitals from one small, dependency-free SDK. Every event belongs to a source identified by a public write key (wk_…), created in the dashboard under Settings → Sources; use yours in place of wk_YOUR_WRITE_KEY below. The full integration guide covers the SDK reference, autocapture and the HTTP API, and this page is served as plain markdown at https://juuna.app/docs/react/llms.txt for AI agents.

Install

npm install @muziris/juuna     # or pnpm add / yarn add

Initialise once in main.tsx

Initialise before the app renders. There is no provider or context to set up; the exported juuna client is a module-level singleton you can import anywhere.

// src/main.tsx
import { createRoot } from 'react-dom/client'
import { juuna } from '@muziris/juuna'
import App from './App'

juuna.init('wk_YOUR_WRITE_KEY', { apiHost: 'https://juuna.app' })

createRoot(document.getElementById('root')!).render(<App />)

If the app has no client-side router, record the one pageview right after init with juuna.page() and you are done. Optional autocapture goes in the same call: juuna.init('wk_…', { apiHost: '…', webVitals: true, clickTracking: true, scrollTracking: true, sessionReplay: true }).

Track route changes with React Router

The SDK has no automatic SPA route detection and never patches the History API: each pageview is an explicit juuna.page() call. With React Router, one null component inside the router is enough:

// src/pageviews.tsx
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { juuna } from '@muziris/juuna'

export function Pageviews() {
  const { pathname } = useLocation()
  useEffect(() => {
    juuna.page()
  }, [pathname])
  return null
}
// src/App.tsx
<BrowserRouter>
  <Pageviews />
  {/* your routes */}
</BrowserRouter>

The effect runs on the first mount and again on every navigation, and init records nothing on its own, so each view is counted exactly once, the first included.

The script tag alternative

For a React app whose bundle you would rather not touch, the hosted tag in index.html works too:

<!-- index.html, before </head> -->
<script defer src="https://juuna.app/sdk/juuna.js"
        data-write-key="wk_YOUR_WRITE_KEY"
        data-api-host="https://juuna.app"></script>

The tag initialises itself and records the initial pageview on its own, so a route-change tracker used with it must skip its first run. With a client-side router, prefer the npm package above; it keeps one code path and it is the only build that supports session replay.

Custom events and identity

The same client that records pageviews takes named events and identity. Call it from the package import, or from window.juuna when using the script tag:

juuna.track('Signup Completed', { plan: 'pro' })
juuna.identify('user_42', { email: 'ada@example.com' }) // after sign-in
juuna.reset()                                           // on logout

Pageviews, sessions, visitors, referrers, devices, browsers and geography are all recorded without any of this. Add a custom event only where a real product question needs one; identify stitches a visitor's anonymous history to their user id, including activity from before they signed in.

Verify it works

Click through a few pages in a normal browser window. Automated browsers are deliberately ignored (the SDK records nothing when navigator.webdriver is set), so a Playwright or Selenium session will never show up. Then check either end of the wire:

  • Network tab: filter for /api/v1/batch. Events batch in memory and flush every 5 seconds by default, at 20 queued events, or when the tab is hidden, so allow a few seconds. A delivered batch answers {"accepted":1,"rejected":0} with a 200.
  • Dashboard: the source's overview shows live visitors and new events within seconds of a flush.

A 401 means the write key is wrong or unknown. A 403 means the source has allowed domains configured and the origin you are testing from is not among them. If nothing is sent at all, check that localStorage.juuna_ignore is not 'true' in that browser: it is the per-browser opt-out.

FAQ

Why do I see two pageviews per route in development?

React Strict Mode mounts effects twice in development, so the pageview effect fires twice there. Production builds mount effects once and record one pageview per route, so the numbers on a deployed site are correct.

Do I need a provider or React context?

No. The exported juuna client is a module-level singleton; import it in any file once init has run. Calls made before init are dropped silently rather than thrown, so ordering mistakes degrade to missing events, never to a crash.

Can a React SPA use session replay?

Yes. Pass sessionReplay: true to juuna.init when using the npm package. The recorder, rrweb, lazy-loads as its own chunk so the core bundle stays tiny, and recordings are masked by default. The script tag build excludes replay.