Docs

04 · Docs

Vue analyticswith juuna.

Init in main.ts, one afterEach hook on the router, and every view is counted. Everything on this page is copy-paste correct for this instance.

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

llms.txt

Vue analytics with juuna

Last updated · 2026-08-31

TL;DR: to add analytics to a Vue 3 app, install @muziris/juuna, call juuna.init once in main.ts, and record pageviews from Vue Router's afterEach hook, which also fires for the initial navigation.

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

Install

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

Initialise in main.ts

// src/main.ts
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { juuna } from '@muziris/juuna'
import App from './App.vue'
import { routes } from './routes'

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

const router = createRouter({ history: createWebHistory(), routes })

router.afterEach(() => {
  juuna.page()
})

createApp(App).use(router).mount('#app')

That is the whole integration. Vue Router runs the initial navigation through the same pipeline as every later one, so afterEach fires for the first view too, and juuna.init records nothing on its own, so each view is counted exactly once. The SDK has no automatic SPA route detection and never patches the History API; the one afterEach hook 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 }).

Without Vue Router

No router means no client-side navigation to track. Initialise and record the one pageview:

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

The script tag alternative

The hosted tag in index.html initialises itself and records the initial pageview on its own:

<!-- 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>

Because the tag has already counted the first view, an afterEach hook used with it must skip its first run:

let first = true
router.afterEach(() => {
  if (first) {
    first = false // the tag already recorded the initial pageview
    return
  }
  ;(window as unknown as { juuna?: { page: () => void } }).juuna?.page()
})

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

Do I need to call juuna.page() on the initial load?

Not when the afterEach hook is in place. Vue Router runs the initial navigation through the same hooks as every later one, so the first view is recorded by the same line. Only a routerless app calls juuna.page() once by hand.

Does this work with Nuxt?

The same two calls do. Run juuna.init in a client-side plugin and call juuna.page() from the router afterEach hook there; the SDK is framework-agnostic and has no Nuxt module. Keep init on the client: the package imports safely on the server, but events belong in the browser.

Can a Vue 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.