useTracking

A declarative event map next to a component instead of track() calls sprinkled through handlers.

Imperative track() calls rot: they live inside handlers, get forgotten on the new code path, and vanish in a refactor. useTracking moves the instrumentation into one declarative map beside the component, so what a component reports is readable in a single place.

import { defineTracking, useTracking } from '@voltro/client'

const spec = defineTracking('CheckoutButton', {
  onMount: 'checkout.viewed',
  onClick: (props) => ({ event: 'checkout.started', plan: props.plan }),
})

const CheckoutButton = (props) => {
  const tracked = useTracking(spec, props, sink)
  return <button {...tracked}>Checkout</button>
}

useTracking(spec, props, sink) returns a copy of your props with every callback the map names wrapped, so invoking onClick fires its event and then calls your original handler. Spread the returned props; nothing is mutated.

In the map, onMount and onUnmount are lifecycle — they fire from an effect, once per mount, and are never wrapped as props. Every other key is the name of a callback prop to wrap. An entry is either a bare event name or a function of the props returning { event, ...payload }.

The sink is yours to provide: (event: TrackingEvent) => void. The kernel is transport-agnostic — it does not know where events go, which is also what makes it testable with a recorder.

resolveTrackingEvent(entry, props) and wrapTrackedCallbacks(spec, props, sink) are exported as pure functions if you need the behaviour outside a component.

For a typed, validated event taxonomy on top of the same sink, see the Analytics catalog.