# 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](https://juuna.app/docs) 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](https://juuna.app/docs/vue/llms.txt) for AI agents.

## Install

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

## Initialise in main.ts

```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:

```ts
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:

```html
<!-- 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:

```ts
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:

```ts
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.
