Weathervane observes browser behavior and emits structured events you can send to any destination. No backend. No dashboards. No lock-in.
Understand what users actually consumed — not just that they showed up. Cumulative, pause-aware exposure tracking that handles tab switches, scroll-aways, and re-engagement.
Tracking logic duplicated across projects. Vendor SDKs leaking into application code. Behavioral events inconsistent across tools. Weathervane fixes this.
Weathervane emits CustomEvents. You decide where they go.
window.addEventListener('vane:event', (e) => { posthog.capture(e.detail.event_name, e.detail); gtag('event', e.detail.event_name, e.detail); segment.track(e.detail.event_name, e.detail); fetch('/api/events', { body: JSON.stringify(e.detail) }); });
Every interaction emits a structured event you can inspect, filter, and forward.
{
"event_name": "content_view",
"properties": {
"content_name": "pricing-section",
"exposure_time": 18420,
"visibility_ratio": 0.95
},
"session_id": "01HX..."
}
{
"event_name": "form_abandon",
"properties": {
"form_name": "checkout",
"form_step": 2,
"engagement_time": 14320
},
"session_id": "01HX..."
}
Script tag or npm. No build step, no config required — it auto-initializes and starts observing.
<script src="weathervane.min.js"></script>
Optional data-vane-* attributes add exposure timing, click identity, and form metadata.
<section data-vane-content="hero"
data-vane-content-exposure="2000">
<button data-vane-content-click="cta">
Events surface as CustomEvents. One listener, any destination — or five.
window.addEventListener('vane:event',
(e) => anywhere.send(e.detail));
Cumulative, pause-aware exposure tracking. Know exactly how long users engaged with each section — across tab switches and scroll-aways.
See when users start forms, how long they engage, and when they abandon. Funnel, step, and goal metadata included. Never reads field values.
Detects rapid repeated clicks indicating user frustration. High-signal behavioral data for UX issues.
FCP, LCP, CLS, and FID emitted automatically, plus scroll depth and time-on-page.
Every link click captured — URL, text, target, internal vs external. SPA navigation tracked as dynamic pageviews.
Automatic capture of uncaught errors and unhandled promise rejections with stack traces.
Works inside Shadow DOM. Tracks through open shadow roots, nested components, and declarative shadow DOM.
setConsent() controls PII collection. Essential mode strips device info and sanitizes URLs.
Every event is visible in the browser before it leaves. The perfect chokepoint for consent and scrubbing.
No vendor SDK conflicts, no migration projects. Adding a destination is adding a listener — and swapping vendors never touches your markup. Own your analytics model.
window.addEventListener('vane:event', (e) => { const { event_name, properties, page } = e.detail; gtag('event', event_name, { ...properties, page_path: page.path }); });
window.addEventListener('vane:event', (e) => { const d = e.detail; posthog.capture(d.event_name, { ...d.properties, $current_url: d.page.url, vane_session_id: d.session_id, }); });
window.addEventListener('vane:event', (e) => { mixpanel.track(e.detail.event_name, e.detail.properties); }); window.addEventListener('vane:user_identify', (e) => mixpanel.identify(e.detail.user_id));
window.addEventListener('vane:event', (e) => { dataLayer.push({ event: 'vane.' + e.detail.event_name, vane: e.detail }); });
// Simplest: use vane.flush() which handles sendBeacon + batching addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') vane.flush('/api/events'); }); // With periodic flushing: setInterval(() => vane.flush('/api/events', { clear: true }), 30000); // Or manual batching if you prefer: const queue = []; window.addEventListener('vane:event', (e) => queue.push(e.detail)); function flush() { if (queue.length) navigator.sendBeacon('/api/events', JSON.stringify(queue.splice(0))); }
window.addEventListener('vane:form_submit', (e) => { if (e.detail.properties.form_goal !== 'conversion') return; fetch(SLACK_WEBHOOK_URL, { method: 'POST', body: JSON.stringify({ text: `🎉 ${e.detail.properties.form_name} submitted` }), }); });
Weathervane doesn't decide what your data means. It observes behavior, structures it, and hands it to you.
GTM is a tag loader — it injects vendor scripts and routes dataLayer pushes, but it doesn't generate rich behavioral events. Out of the box it can't tell you a hero was visible for 2.3 cumulative seconds, or that a checkout form was abandoned after 14 seconds of engagement. Weathervane is the tracking engine that produces those events; GTM can be the router. They compose nicely — see the GTM recipe above.
Weathervane itself makes zero network requests and sets zero cookies — verifiably, since it's
one readable file. Identity lives in localStorage. Use vane.setConsent('essential')
to strip PII: no device info, session-only client ID, sanitized URLs (no query params or referrer).
Events still fire — consent controls what data is collected, not which events fire.
A persistent client ID is still pseudonymous personal data under GDPR, but the clean part is
that you control whether it ever leaves the browser.
There's nothing to block — no tracking endpoint, no third-party domain. You self-host one file under any name on your own origin. Whether your destinations get blocked is between you and your forwarder (server-side forwarding sidesteps that entirely).
Yes — it's framework-agnostic by construction. SPA route changes are detected automatically
via the History API, dynamically rendered content is discovered by MutationObserver, and web
components are first-class. Or disable auto-detection and call
vane.trackPageView() from your router.
Nothing. MIT licensed, no SaaS, no tiers, no telemetry phoning home — there isn't even a server to pay for. It's a utility, not a platform.
One file. Five minutes. Every event in your console before anything goes anywhere.