Event tracking
Capture custom events with vt.capture() — how delivery, batching, and Google Tag forwarding work end to end.
Track custom events and attach properties for analysis. Each capture is validated and enriched, then sent to vTilt over HTTP and (when you use the Google Tag destination) mirrored to gtag in the browser. Both paths respect the same "remote config committed" signal so dashboard toggles and mappings apply consistently.
#Bots and crawlers
By default, vTilt does not record bot traffic — the same approach as PostHog.
- In the browser — Before an event is queued, the SDK checks
navigator.userAgent, Client Hints brands, andnavigator.webdriveragainst a built-in list (search engines, SEO tools, AI crawlers, headless browsers, and similar). Matching sessions never call/api/e. - On the server — Crawlers that probe your ingest URL directly (without running JavaScript) receive 204 No Content and are not stored.
To disable filtering (for example, to debug a headless test harness):
vt.init('YOUR_PROJECT_TOKEN', {
api_host: 'https://www.vtilt.com',
opt_out_useragent_filter: true,
})To block an extra user agent substring:
vt.init('YOUR_PROJECT_TOKEN', {
api_host: 'https://www.vtilt.com',
custom_blocked_useragents: ['my-internal-crawler'],
})Check the current session in DevTools: vt._is_bot() returns true when the SDK would drop captures.
#How delivery works
Ingest and client-side Google forwarding are parallel; they do not block each other.
- HTTP ingest — After rate limiting, events go through an
EventBufferuntil__remote_config_loaded(a fresh/api/dmerge or fetch failure). Then they batch to/api/e. A safety timeout can flush earlier with$config_pendingso the server can still filter. - Google Tag (browser) — The SDK subscribes to captures and forwards through
gtagwhen the Google Tag destination is enabled. Until__remote_config_loaded, captures are held in a small bounded queue (max 100). After that, the officialdataLayer/gtagshim queues measurement calls untilgtag.jsfinishes loading — no duplicate SDK queue for that phase.
#Capture events
Use vt.capture() to send custom events with optional properties. Names should be snake_case verbs that read like a sentence (button_clicked, purchase_completed).
vt.capture('button_clicked', {
button_name: 'Sign Up',
page: 'homepage',
})
vt.capture('purchase_completed', {
product_id: 'SKU-123',
price: 99.99,
currency: 'USD',
quantity: 1,
})#Standard properties
Every capture is automatically enriched with:
| Property | Meaning |
|---|---|
$current_url | Full URL at the time of capture. |
$pathname | Just the path (/products/123). |
$referrer | document.referrer at capture time. |
$session_id | Sticky session identifier (30-minute idle window). |
$device_id | Stable anonymous device identifier. |
$distinct_id | Identified user id, or the anonymous device id pre-identify(). |
engagement_time_msec | Milliseconds since the previous enqueued event in this tab (clamped 1 ms – 1 hour). Used by GA4 Measurement Protocol. |
$pageleave_* | Set on $pageleave events: $prev_pageview_duration (seconds), $prev_pageview_pathname, $prev_pageview_url, plus navigation_type (pagehide / visibility_hidden / spa_transition). |
#When to use a custom event vs autocapture
- Use autocapture for clicks, form submits, page transitions — anything UI-shaped where the element itself describes the interaction.
- Use
vt.capture()when an event represents a domain action (a purchase, an upgrade, a feature toggle) that doesn't map neatly to a DOM element.
For business-critical events, prefer explicit captures even if autocapture would work — it makes the analysis surface (event names, property schema) deliberate instead of accidental.