@muziris/juuna
v0.4.1
Published
Privacy-first product analytics SDK for juuna instances: identify / track / page, autocapture, session replay, Web Vitals.
Maintainers
Readme
@muziris/juuna
The browser SDK for juuna: private product analytics on
a dedicated server of your own. Traffic, funnels, session replay, heatmaps
and Core Web Vitals in one dashboard, with no third parties in the tracking
path. The SDK is tiny and cookieless (a first-party anonymous id in
localStorage), with a Segment-shaped API: identify /
track / page, plus opt-in autocapture, Web Vitals and session replay.
The core bundle and the script tag are dependency-free; session replay
lazy-loads rrweb, the package's one dependency, only when you turn it on.
Why juuna
Most teams run three tools: one for traffic, one for replay and heatmaps, one for performance. juuna is the three in one dashboard, without the ads ecosystem that comes with Google Analytics: no third-party cookies, no advertising identifiers, no data shared with anyone. Every paid plan is a managed dedicated instance, its own server and its own Postgres, on a flat monthly price with every feature included. See pricing, the honest comparisons (Google Analytics, Plausible, PostHog, Matomo, Fathom, Umami, Mixpanel, Hotjar), or poke the live demo.
Install
npm install @muziris/juuna # or pnpm add / yarn addNo account or token is needed for the package; you do need a juuna instance and its write key to send events anywhere. Prefer zero build steps? Every instance also serves a one-tag script build (see below).
Usage (bundled apps)
import { juuna } from '@muziris/juuna'
juuna.init('wk_your_write_key', { apiHost: 'https://your-team.juuna.app' })
juuna.page() // pageview
juuna.track('Signup Completed', { plan: 'pro' })
juuna.identify('user_123', { email: '[email protected]' }) // stitch anon to known user
juuna.reset() // on logoutThe SDK never patches the History API; single-page apps call juuna.page()
on route changes. Framework-correct patterns, copy-paste ready:
Next.js, React,
Astro, SvelteKit,
Vue, and the full
integration guide.
Session replay and Web Vitals (bundled apps)
juuna.init('wk_your_write_key', {
apiHost: 'https://your-team.juuna.app',
webVitals: true, // LCP, CLS, INP, FCP, TTFB from real visits
sessionReplay: true, // npm build only; inputs masked by default
})Replay masks every input by default and passwords always; recordings carry
no IP address and live on your instance, nowhere else. Sampling, masking and
session caps are tunable (sessionReplaySampleRate,
sessionReplayMaskAllText and friends; see the typed options).
Usage (plain <script>)
<script defer src="https://your-team.juuna.app/sdk/juuna.js"
data-write-key="wk_your_write_key"
data-api-host="https://your-team.juuna.app"
data-web-vitals="true"
data-click-tracking="true"
data-scroll-tracking="true"></script>The script auto-initialises from the data-* attributes, records the
initial pageview itself (since 0.2.1; remove any manual juuna.page() you
added on load to compensate), and exposes window.juuna with the same API.
The three optional attributes switch on Web Vitals, click-heatmap and
scroll-map capture; omit them for pageviews only. Session replay is not in
the tag build; it ships only in the npm build. Single-page apps call
juuna.page() on client-side route changes.
The tag itself is 1.0 KB gzipped (dist/juuna.global.js). The three
autocapture features live in a second file (dist/juuna-x.global.js, 1.9 KB
gzipped) that the tag loads from its own directory, juuna.js loading
juuna-x.js and juuna.global.js loading juuna-x.global.js, and only
when one of them is switched on. Serving the files yourself means copying
both, side by side, and sending CORS headers if they are on another origin
than the page. If the extras file cannot be loaded, everything above still
works; only the autocapture is missing.
Behaviour
- Cookieless. The anonymous id is a random first-party value in
localStorage(in-memory when storage is unavailable); it never crosses sites and no cookie is ever set.identifylinks it to a user id;resetrotates it. - Batched delivery via
navigator.sendBeacon(falls back tokeepalivefetch), flushed on a size threshold, an interval, and on page hide. - Auto context: page url/path/referrer/title, screen size, locale.
- Never throws in the host app; delivery is best-effort.
- Respects opt-outs.
localStorage.juuna_ignore = 'true'silences the SDK for that browser, and automated browsers (navigator.webdriver) are never tracked.
Options
| Option | Default | Meaning |
|---|---|---|
| apiHost | https://juuna.app | Base URL of the ingestion service |
| flushAt | 20 | Flush once this many events are queued |
| flushInterval | 5000 | Max ms between flushes while events trickle in |
| webVitals | off | Report Core Web Vitals from real visits |
| clickTracking | off | Capture clicks (normalised coordinates) for heatmaps |
| scrollTracking | off | Capture scroll depth for scroll maps |
| sessionReplay | off | Record sessions, masked by default (npm build only) |
Testing / advanced
createClient(deps) exposes the environment-agnostic core (inject your own
transport / storage / context), which is how the SDK is unit-tested
without a DOM. A plain-markdown description of the whole product lives at
juuna.app/llms.txt for AI agents.
MIT licensed. Built by Muziris LLC.
