@karibsen/krak-lite-nuxt
v0.1.2
Published
Lightweight privacy-first analytics module for Nuxt
Readme
Krak-Lite

Lightweight, privacy-first analytics module for Nuxt.
Why krak-lite?
Most analytics tools drop cookies, fingerprint visitors, and ship their data to a third party - which means consent banners, GDPR paperwork, and giving away your traffic. krak-lite flips that: it stores nothing on the device, keeps no persistent identifier, and sends batched events to your own endpoint. You get the product insights without the privacy baggage.
krak-lite batches actions client-side and ships them to your own endpoint - no third-party servers, no cookies, no persistent identifiers. The session id lives in memory only and is gone on reload, so a visitor can never be tied back across page lifetimes.
Features
- 🪶 Tiny, zero-dependency client (queue + batched flush)
- 🔒 Privacy-first: in-memory anonymous session, no cookies / storage
- 🚫 Respects the browser's Do Not Track signal
- 🧹 Strips query strings from tracked paths automatically
- 🖱 Declarative click tracking via
data-krak-*attributes - 📄 Optional automatic
page_viewtracking - 🔁 Built-in retry with exponential backoff and a capped queue
- 📡 Reliable last-batch delivery via
navigator.sendBeaconon page hide
Quick Setup
Install the module:
npx nuxt module add @karibsen/krak-lite-nuxtOr manually:
npm install @karibsen/krak-lite-nuxt
# pnpm add @karibsen/krak-lite-nuxt
# yarn add @karibsen/krak-lite-nuxtThen add it to your nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@karibsen/krak-lite-nuxt'],
krakLite: {
endpoint: '/api/analytics/actions',
source: 'my-app',
autoPageView: true,
},
})That's it ✨
Configuration
All options live under the krakLite key. Defaults shown below.
krakLite: {
enabled: true, // master switch
endpoint: '/api/analytics/actions', // where batches are POSTed
source: 'unknown', // tag every action with its origin
maxQueueSize: 100, // oldest action dropped past this
flushInterval: 30_000, // ms between automatic flushes
debug: false, // log queue/flush activity
autoPageView: false, // track `page_view` on route change
autoCapture: true, // honor data-krak-* click attributes
respectDoNotTrack: true, // skip tracking when DNT is on
referrer: false, // add query-stripped referrer to page_view
retry: 3, // extra send attempts after a failure
retryAfter: 5_000, // ms before retrying a failed batch
}Usage
Programmatic tracking
Use the auto-imported useKrakLite composable:
<script setup lang="ts">
const { track, flush } = useKrakLite()
function onSubscribe() {
track('cc', { plan: 'pro', price: 29 })
}
function onCheckout() {
// send right away instead of waiting for the next flush
track('form_submit', { form: 'checkout' }, { immediate: true })
}
</script>track(actionName, data?, options?):
| Argument | Type | Description |
|--------------| ------------------------------------- |-------------------------------------------------|
| actionName | string | Free-form, or one of the built-in action names. |
| meta | Record<string, unknown> | Optional structured payload. |
| options | { immediate?, retry?, retryAfter? } | Flush now and/or override retry settings. |
flush(options?) forces the queue to be sent; pass { preferBeacon: true } to use navigator.sendBeacon.
Built-in action names are exported for convenience:
import { KRAK_LITE_ACTIONS } from '@karibsen/krak-lite-nuxt'
// pv, ccTyped custom events
track() accepts any string, but you can register your own event names to get
autocomplete on the action name and a typed meta payload. krak-lite
exposes a global KrakLiteEventRegistry interface - augment it from any .d.ts
in your project (e.g. types/krak-lite.d.ts):
// types/krak-lite.d.ts
export {}
declare global {
interface KrakLiteEventRegistry {
purchase: { amount: number, currency: string }
newsletter_signup: { plan: string }
}
}Each key becomes a suggested action name, and its value types the meta argument:
const { track } = useKrakLite()
track('purchase', { amount: 29, currency: 'EUR' }) // autocompleted + checked
track('purchase', { amount: 29 }) // `currency` is missing
track('anything-else', { foo: 1 }) // unregistered names still allowedThe registry is global on purpose:
track's action-name type is computed from it internally, and a global interface merges reliably regardless of how the package is imported. Augmentingdeclare module '@karibsen/krak-lite-nuxt'instead would not work, because the package only re-exports the interface.
Declarative click tracking
With autoCapture enabled (default), any click on an element carrying
data-krak-action is tracked - no JavaScript required:
<button
data-krak-action="cc"
data-krak-data-plan="pro"
data-krak-data-price="29"
data-krak-immediate
>
Subscribe
</button>
<!--
data-krak-data-plan -> data: { plan: "pro" }
data-krak-data-price -> data: { price: 29 } (coerced to a number)
data-krak-immediate -> flush right away
-->data-krak-data-* values are coerced to boolean / number when they look
like one.
Sensitive data is stripped
Sensitive keys (email, phone, name, address, userId, password,
token, cardNumber, …) are removed from every meta payload before it is
queued - whether the action was tracked programmatically via track() or
declaratively via data-krak-*. Matching is case-insensitive and applies at any
nesting depth:
track('cc', { plan: 'pro', email: '[email protected]', user: { id: 1, name: 'Ada' } })
// queued meta -> { plan: 'pro', user: { id: 1 } }This is a safety net, not a license to send PII - krak-lite is designed to be fed non-personal data in the first place.
Per-page options
With autoPageView enabled, every route change emits a pv. You can
opt a specific page out via definePageMeta:
<script setup lang="ts">
definePageMeta({
krakLite: { pageView: false },
})
</script>This only disables the automatic pv for that route - you can still
call useKrakLite().pageView() manually if needed.
Referrer
Set referrer: true to attach where the visitor came from to each page_view,
under meta.ref:
krakLite: { autoPageView: true, referrer: true }
// page_view meta -> { title: "…", ref: "https://google.com/search" }The referrer is reduced to origin + path only - the query string and hash are stripped so no sensitive parameters leak. It is off by default: it is not a user identifier and, since krak-lite keeps no persistent storage, it cannot be used to link visits - but it does add to the data you collect, so it stays opt-in.
Receiving actions
Actions are POSTed to your endpoint as a single JSON batch:
{
"v": 1,
"s": "my-app",
"d": [
{
"a": "pv", // action name
"t": 1718136000000, // timestamp (ms)
"p": "/pricing", // path (query string stripped)
"s": "my-app", // source
"sid": "…", // in-memory anonymous session id
"meta": { } // optional payload
}
]
}A minimal Nitro handler:
// server/api/analytics/actions.post.ts
export default defineEventHandler(async (event) => {
const batch = await readBody(event)
// persist batch.ev …
return null
})Roadmap
- 🛠 Nuxt DevTools tab - inspect the live action queue, recent flushes and dropped batches without leaving the browser.
- 🔗 Automatic outbound-link tracking (
ocaction) via adata-krak-*-free opt-in. - 🚚 Pluggable
transport(custom sender function) for non-HTTP sinks. - 🧰 Server-side
defineKrakLiteHandlerhelper to validate incoming batches.
Ideas and PRs welcome.
Contribution
# Install dependencies
pnpm install
# Generate type stubs
pnpm dev:prepare
# Develop with the playground
pnpm dev
# Build the playground
pnpm dev:build
# Run ESLint
pnpm lint
# Run Vitest
pnpm test
pnpm test:watch
# Release a new version
pnpm releaseLicense
MIT © D3ller
