@vskstudio/takt-core

June 19, 2026 · View on GitHub

@vskstudio/takt-core

Tiny, privacy-friendly analytics SDK for Takt.

npm version snippet size zero deps license


  • 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.

Snippet (no build step)

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

data-* options

AttributeEffectDefault
data-domainSite identifier sent with every eventlocation.hostname
data-script-originFirst-party origin to derive the endpoint from ({origin}/api/event) — your Takt domain or a custom domain to dodge ad-blockersnone
data-endpointIngestion endpoint (wins over data-script-origin)/api/event
data-exclude-localhost="false"Track localhost / private IPsexcluded
data-enabled="false"Kill-switch — the tracker does nothingenabled
data-respect-dnt="false"Opt out of the Do Not Track short-circuitrespected
data-sample-rate="0.5"Send only this fraction (0–1) of events1 (all)
data-track-queryKeep the full query string and hash (default strips both)stripped
data-query-params="utm_source,utm_medium"Keep only these query paramsnone

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).

Auto extensions — takt.auto.js

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 valueEvent sentProperty
outboundOutbound Link: Clickurl
downloadsFile Downloadurl
404404path
taggedcustom (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 with data-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 name pageview is refused. Identical to init({ tagged: true }) on the SDK.
  • 404: detected at load via the Navigation Timing API, or by adding data-takt-404 to <body> / a <meta name="takt:404"> tag on server-rendered error pages.

npm

pnpm add @vskstudio/takt-core

Quick start — default instance

import { 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.

Instance API — createTakt

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.

Configuration

init() and createTakt() accept the same options:

OptionTypeDefaultEffect
domainstringlocation.hostnameSite identifier sent with every event
scriptOriginstringnoneFirst-party origin to derive the endpoint from ({origin}/api/event) — your Takt domain or a custom domain to dodge ad-blockers
endpointstring/api/eventIngestion endpoint (wins over scriptOrigin)
enabledbooleantrueMaster switch — when false, nothing is sent
debugbooleanfalseLog each payload to the console before sending
sampleRatenumber1Keep this fraction of events (e.g. 0.25 ≈ 25%)
respectDntbooleantrueSuppress events when Do Not Track is on
excludeLocalhostbooleantrueSuppress events on localhost / private IPs
trackQuerybooleanfalseKeep the full query string and hash on URLs
queryParamsstring[]Allowlist: keep only these query params, drop the rest
scrubUrl(url: string) => stringCustom scrubber; overrides trackQuery / queryParams

Privacy

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 tracking

Events 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.

Widgets & public stats

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=1https://takt.example.com). Errors surface as PublicApiError (carrying the HTTP status).

Wire payload contract

Every event is posted to the endpoint as a compact JSON object. The keys are frozen — the Takt backend ingestion depends on them:

KeyMeaning
nevent name (pageview for pageviews)
ddomain
uURL (query + hash stripped by default)
rreferrer (query + hash stripped by default)
wviewport width
pprops (object, omitted if empty)
$revenue { a: amount, c: currency } (currency uppercased)

Architecture

@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.

License

MIT