Clicks
Typed as button, link, outbound link, download, email, phone, submit, checkbox, tab.
Open source · MIT · 6 KB gzipped
Add one script tag. zerotag records every click, scroll, form and route change, and gives each one a name a human would use, like Clicked "Start free trial" in Pricing. You never write a tracking plan.
<script defer src="https://t.p001.ai/zt.js" data-site="yoursite.com"></script>
Nothing here has an event handler for analytics. Click things, fill in the form, scroll. The feed on the right is what gets sent.
This one does nothing. Click it three times fast.
Before
// checkout.tsx
<Button onClick={() => {
analytics.track('checkout_cta_clicked', {
location: 'pricing_page',
variant: plan.id,
})
startCheckout()
}}>
// ...repeated in 214 other components,
// renamed twice, and half of them broke
// in the last redesign.
After
<script defer
src="https://t.p001.ai/zt.js"
data-site="yoursite.com"></script>
// That's it. Rename a button and the
// label follows. Ship a new page and
// it's already tracked.
When you click the span inside a button, zerotag walks up to the control a person would recognize, reads its name the way a screen reader would, and finds the section around it.
<section id="pricing">
<h2>Pricing</h2>
<button class="btn css-1a2b3c">
<span>Start free trial</span> ← you clicked here
</button>
</section>
{
"name": "Clicked \"Start free trial\" in Pricing",
"kind": "button",
"label": "Start free trial",
"section": "Pricing",
"selector": "section#pricing>button.btn"
}
onclick.data-zt, then aria-label, the field's <label>, visible text, image alt, title.aria-label.Typed as button, link, outbound link, download, email, phone, submit, checkbox, tab.
On load and on every client-side route change. Next.js, React Router, Vue, SvelteKit, Astro.
25, 50, 75, 90 and 100 percent, once each per page.
A section counts as seen after 1 second at 40 percent visible.
Which fields people touched, submits, and abandons. Values are never read.
Three clicks in under a second on the same spot.
A button was clicked and the page didn't change within a second.
Only counts time the tab was visible and someone moved, typed or scrolled in the last 30 seconds.
Play, pause and ended, with the timestamp.
Which section people copy from. Not what they copied.
Uncaught errors with file and line, next to the click that caused them.
UTM parameters, referrer, country, city, device, browser, OS.
Autocapture only works if you can trust what it picks up. These are defaults, not settings you have to find.
[email] and [number].utm_*.data-storage="none". Visitors are hashed with a salt that rotates daily.data-zt-ignore skips an element and everything in it. data-zt-mask keeps the click and hides the text.The browser batches events and sends them with sendBeacon as plain text, so there's no CORS preflight and nothing is lost when a tab closes. One Edge Function receives them, answers in milliseconds, and writes to storage in the background.
Point t.yoursite.com at your own collector. Requests stay first-party, ad blockers leave them alone, and events land in storage you control.
Yes. zerotag.track('Upgraded', {seats: 5}) works, and zerotag.set({plan: 'pro'}) attaches properties to everything after it. Most teams find they need a handful, not hundreds.
Add data-zt="Better name" to the element. That wins over everything else. Usually the real fix is an aria-label, which also helps screen reader users.
The script is about 6 KB gzipped and loads with defer. Listeners are passive and attached once on document, so it doesn't touch your components.
Yes. It hooks history.pushState, replaceState and popstate, so each route change sends a page leave and a new pageview.
The collector is stateless and scales per request on the edge. The limit is whatever storage you point it at. Redis is fine for most sites; switch to Tinybird or a webhook into your warehouse when you outgrow it.
<script defer src="https://t.p001.ai/zt.js" data-site="yoursite.com"></script>