- Zero dependencies, tree-shakeable ESM module.
- ≤ 1 kB gzip drop-in snippet — no build step required.
- Privacy first: honours opt-out and Do Not Track, strips query strings, and excludes localhost / private IPs by default.
- Hexagonal core: a pure domain wrapped in ports & adapters — easy to test, easy to extend.
<script defer src="https://cdn.jsdelivr.net/npm/@vskstudio/takt-core/dist/takt.js" data-domain="example.com"></script>Pin a version in production, e.g.
@vskstudio/takt-core@0.5.0. jsDelivr and unpkg both serve the snippet straight from npm — no extra hosting required.
Then, anywhere on the page:
window.takt('Signup', { props: { plan: 'pro' } })Calls made before the script finishes loading are queued and replayed — install a tiny stub first if you need that:
<script>
window.takt = window.takt || function () { (window.takt.q = window.takt.q || []).push(arguments) }
</script>| Attribute | Effect | Default |
|---|---|---|
data-domain |
Site identifier sent with every event | location.hostname |
data-script-origin |
First-party origin to derive the endpoint from ({origin}/api/event) — your Takt domain or a custom domain to dodge ad-blockers |
none |
data-endpoint |
Ingestion endpoint (wins over data-script-origin) |
/api/event |
data-exclude-localhost="false" |
Track localhost / private IPs | excluded |
data-enabled="false" |
Kill-switch — the tracker does nothing | enabled |
data-respect-dnt="false" |
Opt out of the Do Not Track short-circuit | respected |
data-sample-rate="0.5" |
Send only this fraction (0–1) of events | 1 (all) |
data-track-query |
Keep the full query string and hash (default strips both) | stripped |
data-query-params="utm_source,utm_medium" |
Keep only these query params | none |
The base snippet stays under 1 kB gzip: pageviews, SPA navigation, window.takt(), and the privacy guards — nothing more. It respects Do Not Track and strips the query string and hash from URLs by default; the attributes above tune both. For a custom scrubber function, use the npm build (scrubUrl below).
Need outbound clicks, file downloads, HTML-declared events, or 404 detection without writing code? Swap takt.js for the opt-in takt.auto.js bundle and list what you want in data-auto:
<script defer
src="https://cdn.jsdelivr.net/npm/@vskstudio/takt-core/dist/takt.auto.js"
data-domain="example.com"
data-auto="outbound,downloads,tagged,404"></script>Without data-auto, takt.auto.js behaves exactly like takt.js. Each extension is opt-in.
data-auto value |
Event sent | Property |
|---|---|---|
outbound |
Outbound Link: Click |
url |
downloads |
File Download |
url |
404 |
404 |
path |
tagged |
custom (data-takt-event) |
from data-takt-prop-* |
- downloads default extensions:
pdf, xlsx, docx, pptx, csv, zip, gz, rar, 7z, dmg, exe, apk, mp3, mp4, wav, mov, avi, mkv, txt— override withdata-downloads-ext="pdf,csv,epub". - tagged: add
data-takt-event="Cta"to any clickable element;data-takt-prop-<key>attributes become props (empty keys/values are ignored). The reserved namepageviewis refused. Identical toinit({ tagged: true })on the SDK. - 404: detected at load via the Navigation Timing API, or by adding
data-takt-404to<body>/ a<meta name="takt:404">tag on server-rendered error pages.
pnpm add @vskstudio/takt-coreimport { init, track, pageview } from '@vskstudio/takt-core'
init({ domain: 'example.com', outbound: true, files: true, notFound: true })
track('Signup', {
props: { plan: 'pro' },
revenue: { amount: '29.00', currency: 'EUR' },
})init() creates a single shared instance, fires an automatic pageview, and wires SPA navigation. track, pageview, optOut, and optIn delegate to it.
Autocapture toggles — outbound, files, notFound, and tagged — opt into the same extensions as the snippet's data-auto: outbound-link clicks, file downloads, 404 detection, and data-takt-event custom events. tagged: true tracks clicks on elements carrying data-takt-event (with data-takt-prop-* becoming props), matching data-auto=tagged.
For full control (multiple instances, no globals, explicit teardown), construct an instance directly:
import { createTakt } from '@vskstudio/takt-core'
const takt = createTakt({ domain: 'example.com', endpoint: '/api/event' })
takt.pageview()
takt.track('Signup', { props: { plan: 'pro' } })
// Each enableX returns a disposer for teardown.
const stopSpa = takt.enableSpa()
const stopOutbound = takt.enableOutbound()
const stopFiles = takt.enableFiles(['pdf', 'zip', 'csv'])
const stop404 = takt.enable404() // detects a 404 page once and reports it
const stopTagged = takt.enableTagged() // custom events from data-takt-event
// later…
stopSpa()
stopOutbound()
stopFiles()
stop404()
stopTagged()createTakt() is a pure factory (no side effects until you call a method), so it tree-shakes cleanly.
init() and createTakt() accept the same options:
| Option | Type | Default | Effect |
|---|---|---|---|
domain |
string |
location.hostname |
Site identifier sent with every event |
scriptOrigin |
string |
none | First-party origin to derive the endpoint from ({origin}/api/event) — your Takt domain or a custom domain to dodge ad-blockers |
endpoint |
string |
/api/event |
Ingestion endpoint (wins over scriptOrigin) |
enabled |
boolean |
true |
Master switch — when false, nothing is sent |
debug |
boolean |
false |
Log each payload to the console before sending |
sampleRate |
number |
1 |
Keep this fraction of events (e.g. 0.25 ≈ 25%) |
respectDnt |
boolean |
true |
Suppress events when Do Not Track is on |
excludeLocalhost |
boolean |
true |
Suppress events on localhost / private IPs |
trackQuery |
boolean |
false |
Keep the full query string and hash on URLs |
queryParams |
string[] |
— | Allowlist: keep only these query params, drop the rest |
scrubUrl |
(url: string) => string |
— | Custom scrubber; overrides trackQuery / queryParams |
By default the query string and hash are stripped from every URL (page, referrer, and autocaptured link destinations) before sending — secrets in ?token=… or #access_token=… never leave the browser. Opt back in with trackQuery: true, narrow it with a queryParams allowlist, or take full control with scrubUrl. Props and revenue are sanitized too: props are coerced to strings, capped (30 keys, 64-char keys, 1024-char values), and revenue is dropped unless the amount and 3-letter currency are well-formed.
import { optOut, optIn } from '@vskstudio/takt-core'
optOut() // sets localStorage `takt_ignore` = '1'; no events are sent
optIn() // resumes trackingEvents are suppressed, in order, when: the visitor has opted out, or Do Not Track is enabled (respectDnt), or the host is localhost / a private IP (excludeLocalhost), or the event is dropped by sampleRate.
Besides tracking, the package ships framework-agnostic helpers for Takt's
server-rendered widgets and its public stats API. These are tree-shakeable and
re-exported by the framework wrappers (@vskstudio/takt-react, -vue, etc.).
import { badgeUrl, embedUrl, createStats } from '@vskstudio/takt-core'
// URL builders for the server-rendered badge SVG and embed iframe.
badgeUrl('example.com', { variant: 'd', glyph: 'off', lang: 'en' })
// → /public/example.com/badge.svg?variant=d&glyph=off&lang=en
embedUrl('example.com', { theme: 'dark' })
// → /embed/example.com?theme=dark
// Anonymous client for the public stats API. Pass `host` for a remote Takt.
const stats = createStats({ host: 'https://takt.example.com', domain: 'example.com' })
await stats.summary(undefined, { period: '30d', compare: 'previous' })
await stats.timeseries()
await stats.realtime()
await stats.breakdown('page')host defaults to '' (same-origin), matching the SDK's relative endpoint.
When set, it must be an absolute http(s):// origin — anything else
(javascript:, data:, protocol-relative //…) is rejected, so a host
value can never smuggle a non-http scheme into a widget src or a fetch.
The value is reduced to its origin: any path, query, or fragment is dropped
(https://takt.example.com/x?a=1 → https://takt.example.com).
Errors surface as PublicApiError (carrying the HTTP status).
Every event is posted to the endpoint as a compact JSON object. The keys are frozen — the Takt backend ingestion depends on them:
| Key | Meaning |
|---|---|
n |
event name (pageview for pageviews) |
d |
domain |
u |
URL (query + hash stripped by default) |
r |
referrer (query + hash stripped by default) |
w |
viewport width |
p |
props (object, omitted if empty) |
$ |
revenue { a: amount, c: currency } (currency uppercased) |
@vskstudio/takt-core follows a hexagonal (ports & adapters) layout:
domain/ Pure business core, zero I/O. Value objects (EventName, Props,
Revenue, AnalyticsEvent), payload mapping, and the URL scrubber.
application/ Use cases: the Analytics service, the TrackingPolicy (consent +
sampling), and autocapture trackers — depending only on small
single-method port interfaces.
infrastructure/ Driven adapters: a resilient fetch/beacon transport, localStorage
consent, and browser providers (DNT, environment, history, clicks).
composition/ createTakt() factory, the ESM entry, and the snippet adapter.
The domain never reaches outward; adapters are injected at the composition root (createTakt). This keeps the core testable with fakes and lets you swap transports or storage without touching business logic.
MIT