Part 1 · Light DOM

Regular markup with data-vane-* attributes — the baseline every analytics tool should handle.

content tracking · exposure 2000ms

Events, emitted. Nothing sent.

This hero is marked with data-vane-content="hero-banner" and data-vane-context-* attributes. Check the event payload for context. A content_serve event fired the moment it entered the DOM, and after it has been visible for 2 cumulative seconds, a content_view appears.

emitscontent_servecontent_viewcontent_click
content tracking · default exposure 1000ms

Feature showcase

Each card below has its own data-vane-content-click identifier.

👁️ Viewport detection

IntersectionObserver-based, with large-content handling.

🪪 Cookie-free IDs

localStorage client & session IDs. Zero cookies set.

📊 Web vitals

FCP, LCP, CLS and FID as standalone web_vitals event at page end.

🔌 Zero backend

You decide where events go. GA4, PostHog, anywhere.

emitscontent_servecontent_viewcontent_click
large content · 65% viewport-fill rule · exposure 3000ms

A deliberately tall article

This article is taller than your viewport, so it can never be 100% visible at once. Weathervane switches to percentage-based detection: the article counts as "visible" while it fills at least 65% of your viewport. Scroll through it slowly and after 3 cumulative seconds of qualifying visibility, a content_view fires.

Section one. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec ullamcorper nulla non metus auctor fringilla. Maecenas faucibus mollis interdum.

Section two. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Aenean lacinia bibendum nulla sed consectetur. Nullam id dolor id nibh ultricies vehicula ut id elit.

Section three. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras mattis consectetur purus sit amet fermentum. Sed posuere consectetur est at lobortis. Etiam porta sem malesuada magna mollis euismod.

Section four. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.

Section five. Donec id elit non mi porta gravida at eget metus. Nulla vitae elit libero, a pharetra augue. Maecenas sed diam eget risus varius blandit sit amet non magna. Curabitur blandit tempus porttitor.

Section six. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Sed posuere consectetur est at lobortis. Vestibulum id ligula porta felis euismod semper. Integer posuere erat a ante venenatis dapibus posuere velit aliquet.

emitscontent_servecontent_viewcontent_click
form tracking · engage, submit + abandonment

Sign up form

Focus a field to see form_engage, then submit to see form_submit (with completion_time). Or focus a field, then click outside the form — after 3 seconds of inactivity, form_abandon fires with the engagement time. Re-focusing resets the timer.


emitsform_engageform_submitform_abandon
automatic link tracking

Links

Every anchor click emits link_click with the URL, text, target, and whether it's external. No attributes needed. (These are safe to click — they won't leave the page.)

emitslink_click
dynamic content · mutationobserver

Injected after load

Content added via JavaScript is discovered automatically — no re-init needed.

emitscontent_servecontent_viewcontent_click

Part 2 · Shadow DOM

The same data-vane-* attributes, inside web components — where most analytics tools go blind. Every condition below is handled automatically.

imperative shadow root · created before init · content + link + form

Component rendered at load

<vane-hero-widget> below calls attachShadow() while the page is still parsing — before Weathervane initializes. The initial scan finds the root anyway. Its shadow tree contains tracked content, a CTA, a link, and a form: all four event flows work through the boundary.

emitscontent_servecontent_viewcontent_clicklink_clickform_engageform_submit
declarative shadow dom · parsed at page load · zero javascript

Declarative shadow DOM (static)

This root was created by the HTML parser from a <template shadowrootmode="open"> in the page source. No JavaScript created it — there was never an attachShadow() call to intercept.

emitscontent_servecontent_viewcontent_click
nested shadow roots · two levels deep

Component inside a component

<vane-nested-outer> renders <vane-nested-inner> inside its own shadow root. Both levels carry tracked content — discovery recurses through every open root it finds.

emitscontent_serve ×2content_view ×2content_click
cross-boundary attribution · composedPath()

Click inside, container outside

The card below is a light-DOM content container (data-vane-content="cross-boundary-card"), but its button lives inside a web component's shadow root. The click walks event.composedPath() and attributes to the outer container — check the content_name on the event.

Light-DOM card

My button is rendered by <vane-cta-button>, a shadow boundary away from where my container is declared.

emitscontent_servecontent_viewcontent_click → cross-boundary-card
closed shadow root · intentionally not tracked

The closed root (nothing should happen)

<vane-closed-widget> uses attachShadow({ mode: 'closed' }). Its shadow tree contains the exact same tracked attributes as the components above — but closed roots are private by design: events are retargeted beyond recovery and the tree is unreachable. Click its button and watch the console stay silent.

emitsnothing — closed roots are private, by design
post-load injection · both root-creation paths

Components that arrive later

Two different ways a shadow root can appear after load: attachShadow() (intercepted by instrumentation) and parser-created declarative roots via setHTMLUnsafe() (discovered on insertion). Both are tracked seamlessly.

emitscontent_servecontent_viewcontent_click

Part 3 · Page-level behaviors

Tracking that isn't tied to any DOM tree.

spa navigation · pageview_dynamic

Single-page app navigation

Weathervane patches the History API and listens for hash changes. Each button below changes the URL without reloading — watch for pageview_dynamic events with a fresh page_view_id.

emitspageview_dynamic
custom events & api

Manual API

Anything you can't express with attributes, you can track directly: vane.track('video_played', {...}).

emitsvideo_playeduser_identifysession_startcontext_demo

Part 4 · Other useful things

Error tracking, rage clicks, consent API, sampling, privacy controls, and more.

v0.8 features

Interactive demos

Error Tracking

Uncaught errors and promise rejections are automatically captured.

emitserror

Rage Click Detection

Click this button rapidly 3+ times within 1 second:

emitsrage_click

Consent API

Consent controls what data is collected (PII stripping), not which events fire.

Current consent level: all

Essential mode: no device info, session-only client ID, sanitized URLs (path only), no UTM params

emitsconsent_change

Payload Comparison

Click buttons above, then trigger an event to see the difference in payloads:

Sampling

Check if this session is sampled: ...

Flush to endpoint

vane.flush(url) sends event history via sendBeacon. Try it (uses a fake endpoint):

Event History

Get all tracked events: vane.getHistory()

Payload Modes

Current mode: full (default). Modes affect payload size:

APIsisSampled()flush(url)setConsent()getHistory()