Instrument once. Route anywhere.

Weathervane observes browser behavior and emits structured events you can send to any destination. No backend. No dashboards. No lock-in.

Browser
Weathervane
Your Stack
~8 KB gzipped 0 cookies 0 network requests 0 dependencies MIT licensed
$ npm i weathervane
View on GitHub → Live demo
What makes it different

Most analytics count visits.
Weathervane measures attention.

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.

This page is emitting right now
The problem

Stop rebuilding analytics every time you switch vendors.

Tracking logic duplicated across projects. Vendor SDKs leaking into application code. Behavioral events inconsistent across tools. Weathervane fixes this.

Tag managers route events
vs
Weathervane generates them
Destinations

One event listener. Infinite destinations.

Weathervane emits CustomEvents. You decide where they go.

Analytics

  • PostHog
  • GA4
  • Mixpanel
  • Amplitude

CDPs

  • Segment
  • RudderStack
  • mParticle

Warehouses

  • BigQuery
  • Snowflake
  • ClickHouse
  • Redshift

Your own

  • REST API
  • Webhooks
  • Slack
  • Anything
send anywhere
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) });
});
Event structure

Real event payloads

Every interaction emits a structured event you can inspect, filter, and forward.

content_view
{
  "event_name": "content_view",
  "properties": {
    "content_name": "pricing-section",
    "exposure_time": 18420,
    "visibility_ratio": 0.95
  },
  "session_id": "01HX..."
}
form_abandon
{
  "event_name": "form_abandon",
  "properties": {
    "form_name": "checkout",
    "form_step": 2,
    "engagement_time": 14320
  },
  "session_id": "01HX..."
}
Quick start

Three steps. Five minutes.

1

Install

Script tag or npm. No build step, no config required — it auto-initializes and starts observing.

<script src="weathervane.min.js"></script>
2

Annotate

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

Forward

Events surface as CustomEvents. One listener, any destination — or five.

window.addEventListener('vane:event',
  (e) => anywhere.send(e.detail));
What it observes

Behavioral events,
not just pageviews.

🎯

Content exposure

Cumulative, pause-aware exposure tracking. Know exactly how long users engaged with each section — across tab switches and scroll-aways.

📝

Form tracking

See when users start forms, how long they engage, and when they abandon. Funnel, step, and goal metadata included. Never reads field values.

😤

Rage clicks

Detects rapid repeated clicks indicating user frustration. High-signal behavioral data for UX issues.

📊

Web vitals

FCP, LCP, CLS, and FID emitted automatically, plus scroll depth and time-on-page.

🔗

Navigation

Every link click captured — URL, text, target, internal vs external. SPA navigation tracked as dynamic pageviews.

🚨

Errors

Automatic capture of uncaught errors and unhandled promise rejections with stack traces.

🌒

Web components

Works inside Shadow DOM. Tracks through open shadow roots, nested components, and declarative shadow DOM.

✅

Privacy & consent

setConsent() controls PII collection. Essential mode strips device info and sanitizes URLs.

🔍

Inspectable

Every event is visible in the browser before it leaves. The perfect chokepoint for consent and scrubbing.

Bring your own destination

Forward to anything.
Or everything at once.

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.

GA4 PostHog Mixpanel GTM Your API Slack
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` }),
  });
});
Define the boundary

What Weathervane is not.

Weathervane doesn't decide what your data means. It observes behavior, structures it, and hands it to you.

Questions you're already asking

FAQ

Why not just use Google Tag Manager?

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.

Is it actually private?

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.

Do ad blockers kill it?

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

Does it work with React / Vue / my framework?

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.

What does it cost?

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.

Start collecting behavior.
Route it anywhere.

One file. Five minutes. Every event in your console before anything goes anywhere.

$ npm i weathervane
View on GitHub → Live demo