Vanilla JS
@vskstudio/takt-core is the framework-agnostic core every wrapper builds on. Fully typed, zero dependencies, it works in any bundler, and ships the CDN snippet too.
The import is server-safe: the module touches no browser API at load time, and track() / pageview() are no-ops until init() has run. optOut(), optIn() and isOptedOut() need no instance at all (they read and write the choice stored in the browser, and do nothing on the server). init() itself belongs in the browser though: it wires history and reads location.
pnpm add @vskstudio/takt-core
# or: npm install @vskstudio/takt-core
# or: yarn add @vskstudio/takt-core
# or: bun add @vskstudio/takt-core init() + track()
Call init() once at startup: it creates the shared instance, emits the initial pageview and wires up SPA navigation (pushState/replaceState/popstate/hashchange). Those last two are driven by the auto option: with auto: false, neither the history patch nor the boot pageview is installed. Then emit events from anywhere with track().
import { init, track } from '@vskstudio/takt-core'
// Once, at app startup.
init({ domain: 'example.com', outbound: true })
// Everywhere else.
track('Signup', { props: { plan: 'pro' } }) init() is idempotent: a second call disposes the previous instance’s listeners before creating a new one. The full option list lives in the configuration and the API reference.
Revenue
track() accepts a revenue object alongside props: the amount is a string matching \d+(\.\d{1,2})?, the currency a 3-letter code (sent upper-cased):
import { track } from '@vskstudio/takt-core'
track('Purchase', {
props: { plan: 'pro' },
revenue: { amount: '29.00', currency: 'EUR' }
}) If the amount or the currency fails validation, the revenue is dropped (with a console warning) but the event is still sent.
The instance returned by init(), or by createTakt(), which builds a standalone instance with no singleton, exposes the exact same signature, handy when you’d rather not rely on the singleton:
const takt = init({ domain: 'example.com' })
takt.track('Purchase', { revenue: { amount: '29.00', currency: 'EUR' } }) pageview() + consent
pageview() emits a pageview manually. It is rarely needed while auto is on, since navigation is then tracked for you; it is the right tool once you turn auto off (which is what the Astro integration does). optOut() / optIn() drive per-visitor consent, persisted in localStorage under the takt_ignore key, and isOptedOut() reads it back:
import { pageview, optOut, optIn, isOptedOut } from '@vskstudio/takt-core'
optOut() // no event sent
optIn() // resumes tracking
isOptedOut() // true after optOut(), to render the state of a toggle
pageview() Since 0.9.0, these three functions work without an instance: a consent banner can call them before init(), and every instance created later honours the choice. An instance from createTakt() also exposes optOut(), optIn() and isOptedOut(), backed by the same stored state.
import { init, isOptedOut, optOut } from '@vskstudio/takt-core'
function onConsentRefused() {
optOut()
}
function startAnalytics() {
if (!isOptedOut()) init({ domain: 'example.com' })
} See Privacy for the exact order of the guardrails.
Route redaction
Since 0.10.0, redactRoutes replaces the path of sensitive routes with their pattern, and routeTemplates sends every page as a template:
import { init } from '@vskstudio/takt-core'
init({ domain: 'example.com', redactRoutes: ['/verify/[token]', '/reset/:code'] }) The redactUndecodablePaths option (since 0.12.0, default true) is set in the same place: init({ domain: 'example.com', redactRoutes: ['/verify/[token]'], redactUndecodablePaths: false }) matches an unreadable path against the routes instead of masking it. See Unreadable paths.
Without a framework, the core does not know your router: for routeTemplates, provide a routeTemplate resolver returning the current route template, or null when none matches.
init({
domain: 'app.example.com',
routeTemplates: true,
routeTemplate: () => router.currentRoute?.pattern ?? null
}) The framework wrappers plug this resolver into their router. See Route redaction for the pattern syntax and the per-page stats trade-off.
CDN snippet
No bundler? The same package ships a ready-to-use script: a single tag in the <head>, init done from data-* attributes:
<script defer src="https://cdn.jsdelivr.net/npm/@vskstudio/takt-core/dist/takt.js" data-domain="example.com"></script> With neither data-endpoint nor data-script-origin, the snippet sends to https://taktlytics.com/api/event, same as the npm SDK. Pass data-endpoint="/api/event" for a same-origin first-party proxy.
On the snippet side, only window.takt(...) (the track function) is exposed: see Installation for the queue shim and API reference for the global. The snippet supports neither redactRoutes nor routeTemplates: the path is sent as is.
Server side
The same package exposes a Node entry, @vskstudio/takt-core/server, to send pageviews and events server to server with createServerTakt(). See Node.js (server).
@vskstudio/takt-core.