Docs

04 · Docs

SvelteKit analyticswith juuna.

Init behind a browser guard, one afterNavigate call, and every view is counted. Everything on this page is copy-paste correct for this instance.

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

llms.txt

SvelteKit analytics with juuna

Last updated · 2026-08-31

TL;DR: to add analytics to a SvelteKit app, install @muziris/juuna, initialise it in the root +layout.svelte behind a browser guard, and record a pageview from afterNavigate, which covers the first load and every client-side navigation with one call.

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/sveltekit/llms.txt for AI agents.

Install

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

Initialise in the root layout

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { browser } from '$app/environment'
  import { afterNavigate } from '$app/navigation'
  import { juuna } from '@muziris/juuna'

  let { children } = $props()

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

  afterNavigate(() => {
    juuna.page()
  })
</script>

{@render children()}

On Svelte 4, keep your existing <slot /> in place of the children render; the script block is the integration. Two details make it exact:

  • afterNavigate runs after the layout first mounts and again after every client-side navigation, and juuna.init records nothing on its own, so each view is counted exactly once, the first included.
  • The browser guard keeps init out of server-side rendering. Importing the package on the server is safe (it touches no browser global at import time), but the client should only ever start in the browser.

The root layout persists across navigations, so init runs once per visit. The SDK has no automatic SPA route detection and never patches the History API; the one afterNavigate call is the whole of route tracking. Optional autocapture goes in the same call: juuna.init('wk_…', { apiHost: '…', webVitals: true, clickTracking: true, scrollTracking: true, sessionReplay: true }).

The script tag alternative

Add the hosted tag to src/app.html and skip the first afterNavigate, because the tag records the initial pageview itself:

<!-- src/app.html, inside <head> -->
<script defer src="https://juuna.app/sdk/juuna.js"
        data-write-key="wk_YOUR_WRITE_KEY"
        data-api-host="https://juuna.app"></script>
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { afterNavigate } from '$app/navigation'

  afterNavigate((nav) => {
    if (nav.type === 'enter') return // initial load: the tag already recorded it
    ;(window as unknown as { juuna?: { page: () => void } }).juuna?.page()
  })
</script>

One limitation: session replay is not available from the script build. It needs the npm package (sessionReplay: true).

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

Is the package safe with server-side rendering?

Yes. Importing @muziris/juuna touches no browser global at import time, so SSR and prerendering never break. Keep init behind the browser guard so the client only ever starts in the browser.

Do prerendered pages get counted?

Yes. A prerendered SvelteKit page still hydrates in the visitor browser, the layout script runs there, and afterNavigate fires. The exception is a route that switches client-side JavaScript off entirely with csr = false: no app code runs there, so use the script tag in app.html for those pages, which still ships in the HTML.

Can a SvelteKit app use session replay?

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