takt

Solid

SolidJS has a dedicated wrapper: @vskstudio/takt-solid (component, accessor, declarative click, web component), built on the @vskstudio/takt-core core.

pnpm add @vskstudio/takt-solid @vskstudio/takt-core

solid-js (^1.8) and @vskstudio/takt-core (>=0.10.0 since 0.8.0) are peer dependencies. It is a thin SSR-safe layer (init only happens on mount, in the browser), fully typed, that never changes the wire payload or the core’s privacy guarantees. It exposes two entry points:

  • @vskstudio/takt-solid: the <Takt> component, the useTakt() accessor, createTaktEvent(), the <TaktEvent> component, the <TaktBadge> / <TaktEmbed> widgets, plus the core helpers it re-exports (badgeUrl(), embedUrl(), createStats(), PublicApiError, optOut(), optIn(), isOptedOut()).
  • @vskstudio/takt-solid/element: the Solid-free <takt-analytics> web component for plain HTML.

<Takt> component + useTakt()

Place <Takt> once near the root: it boots analytics in onMount, emits the initial pageview, wires up SPA navigation and publishes the instance into a module store. useTakt() then retrieves it from anywhere in the app, as long as you call the accessor after that mount.

import type { JSX } from 'solid-js'
import { Takt } from '@vskstudio/takt-solid'

export function App(props: { children: JSX.Element }) {
  return (
    <Takt domain="example.com" outbound files={['pdf', 'zip']}>
      {props.children}
    </Takt>
  )
}

Call useTakt() inside the handler, not in the component body: a descendant’s body runs before <Takt>’s onMount, so an instance captured there would stay the no-op forever and every event would go nowhere. A click handler, a createEffect or createTaktEvent() all resolve the instance after mount.

import { useTakt } from '@vskstudio/takt-solid'

export function SignupButton() {
  return (
    <button
      onClick={() =>
        useTakt().track('Signup', {
          props: { plan: 'pro' },
          revenue: { amount: '29.00', currency: 'EUR' }
        })
      }
    >
      Sign up
    </button>
  )
}

<Takt> component props:

PropTypeDefaultRole
domainstringlocation.hostnameSite identifier
endpointstringhttps://taktlytics.com/api/eventIngestion URL. Pass /api/event for a same-origin first-party proxy.
scriptOriginstringnoneFirst-party origin to derive the endpoint from (that origin followed by /api/event). endpoint wins over it.
outboundbooleanfalseTracks outbound links
filesboolean \| string[]falseTracks downloads (optional extension list)
spabooleantrueTracks client navigation (auto pageviews)
track404booleanfalseReports a 404 event on error pages ([data-takt-404] / <meta name="takt:404"> marker, or a 404 HTTP status).
taggedbooleanfalseAuto-tracks clicks on [data-takt-event] elements
respectDntbooleantrueRespects Do Not Track
excludeLocalhostbooleantrueIgnores localhost / private IPs
enabledbooleantrueMaster switch: false disables all tracking
sampleRatenumber1Fraction of sessions tracked (0 to 1)
trackQuerybooleanfalseKeeps the query string in URLs
queryParamsstring[]noneParameters kept when trackQuery is false (allowlist)
excludestring[]nonePath prefixes never tracked. Segment-bounded: /app covers /app and /app/… but not /application.
scrubUrl(url: string) => stringnoneTransforms every URL before it is sent (page, referrer, url of outbound links and downloads, path of 404s)
debugbooleanfalseLogs each payload to the console before sending
redactRoutesstring[]noneRoute patterns sent instead of the real path, e.g. ['/verify/:token'] (see Route redaction)
redactUndecodablePathsbooleantrueNot exposed by this wrapper: set it on the core SDK (init() / createTakt()), see Unreadable paths
routeTemplatesbooleanfalseSends every page as its route template (/blog/:slug); needs routeTemplate
routeTemplate() => string \| null \| undefinednoneReturns the current route template: solidRouterTemplate(useCurrentMatches()) with @solidjs/router

Config props are read once when <Takt> mounts. Changing them afterwards has no effect, so remount the component to reconfigure. routeTemplate is the exception: it is read on every pageview.

The component forwards all of this configuration to the core: the advanced settings (enabled, sampleRate, trackQuery, queryParams, exclude, scrubUrl) are available as props, with no separate core instance, and so is debug since 0.7.0.

useTakt() never throws: called outside a <Takt>, during SSR, or before <Takt> has mounted, it returns a no-op instance that sends nothing over the network and warns once in the console (useTakt() called before <Takt> mounted). That message is the symptom of the pitfall above.

Consent, however, works before mounting: useTakt().optOut(), useTakt().optIn() and useTakt().isOptedOut() act directly on the stored choice, as do the optOut, optIn and isOptedOut functions exported by the package. A consent banner can therefore render first, and the instance created later honours the choice.

import { createSignal } from 'solid-js'
import { isOptedOut, optIn, optOut } from '@vskstudio/takt-solid'

function AnalyticsToggle() {
  const [blocked, setBlocked] = createSignal(isOptedOut())
  const toggle = () => {
    if (blocked()) optIn()
    else optOut()
    setBlocked(isOptedOut())
  }
  return <button onClick={toggle}>{blocked() ? 'Enable analytics' : 'Disable analytics'}</button>
}

Route redaction

The query string is stripped by default, but the path is sent as is: /verify/abc123 leaks the token. redactRoutes lists the sensitive routes; a matching path is sent as the pattern and every other path keeps its real value.

<Takt domain="example.com" redactRoutes={['/verify/:token', '/reset/:code', '/invoices/:id']}>
  <App />
</Takt>

Patterns accept Solid Router syntax (:param, :param?, *rest) as well as [param], [[optional]], [...rest], (group), * and **. The rule covers the page URL, same-site referrers, outbound and download link destinations, and 404 paths.

For a fully private app, routeTemplates sends every page as its route template: /blog/hello becomes /blog/:slug. With @solidjs/router, call useCurrentMatches() in a component rendered under the router and hand it to solidRouterTemplate(), which returns the pattern of the deepest matched route:

import { Router, Route, useCurrentMatches, type RouteSectionProps } from '@solidjs/router'
import { Takt, solidRouterTemplate } from '@vskstudio/takt-solid'

function Layout(props: RouteSectionProps) {
  const matches = useCurrentMatches()
  return (
    <Takt domain="example.com" routeTemplates routeTemplate={solidRouterTemplate(matches)}>
      {props.children}
    </Takt>
  )
}

export function App() {
  return (
    <Router root={Layout}>
      <Route path="/" component={Home} />
      <Route path="/blog/:slug" component={Post} />
    </Router>
  )
}

solidRouterTemplate only needs an accessor returning { route: { pattern } }[], so the package does not depend on @solidjs/router. When no route matches, redactRoutes still applies and the real path is sent otherwise. On a public site this mode merges every article into one row, so prefer redactRoutes. Details in Privacy.

createTaktEvent() + <TaktEvent>

For declarative click tracking, two ways that resolve the active instance at click time (no stale closure), with a fallback to the core’s default instance.

createTaktEvent() returns an { onClick } you spread onto any element:

import { createTaktEvent } from '@vskstudio/takt-solid'

export function BuyButton() {
  const onBuy = createTaktEvent({
    name: 'Purchase',
    revenue: { amount: '29.00', currency: 'EUR' }
  })
  return <button {...onBuy}>Buy</button>
}

<TaktEvent> wraps a single child and composes its existing onClick, so you can annotate an element without touching its handler:

import { TaktEvent } from '@vskstudio/takt-solid'

export function SignupCta(props: { onClick: () => void }) {
  return (
    <TaktEvent name="Signup" props={{ plan: 'pro' }}>
      <button onClick={props.onClick}>Sign up</button>
    </TaktEvent>
  )
}

The child’s original onClick still fires; tracking runs alongside it. The child must render an actual DOM element: otherwise tracking is disabled, with a console warning.

<TaktBadge> and <TaktEmbed> widgets

To display a site’s public stats (see Widgets), the package ships two drop-in components:

import { TaktBadge, TaktEmbed } from '@vskstudio/takt-solid'

export function Footer() {
  return (
    <>
      <TaktBadge domain="example.com" variant="b" glyph="dash" lang="en" />
      <TaktEmbed domain="example.com" theme="dark" lang="en" />
    </>
  )
}

<TaktBadge> renders an <img> (loading="lazy", decoding="async"): variant is a, b or d, glyph is unplug, dash, off or eyeoff. <TaktEmbed> renders a 404×264 <iframe> by default, loading="lazy" and sandboxed: theme is light, dark or auto. Both accept lang (fr / en), a host prop to point at another Takt origin, and forward their remaining attributes to the rendered element. The re-exported createStats() client serves the same public stats as JSON.

<takt-analytics> web component

For plain HTML or a non-Solid framework, import the ./element subpath (auto-registered, bundles the core, no Solid runtime):

import '@vskstudio/takt-solid/element'
<takt-analytics domain="example.com" outbound files></takt-analytics>

Attributes mirror the props, in kebab-case:

  • Flags (presence = on, value ignored): outbound, files, track-404, tagged.
  • On by default, set ="false" (or ="0") to turn them off: spa, respect-dnt, exclude-localhost.
  • Values: domain, endpoint, script-origin, sample-rate, plus query-params, exclude and redact-routes which take a comma-separated list. enabled, track-query and debug are only read when the attribute is present, and are turned off with ="false" / ="0".
<takt-analytics domain="example.com" spa="false" respect-dnt="false"></takt-analytics>

files takes no extension list here, and scrubUrl has no equivalent: those are values only the <Takt> component receives as props. routeTemplates has none either, since the element has no router.

<Takt> boots inside onMount and is guarded by Solid's isServer: nothing touches window/document on the server, and useTakt() returns the no-op during the SSR pass. Importing ./element on the server is a no-op while customElements does not exist.