vTilt
Why vTiltHow It WorksFeaturesFAQDocs
Docs / Event tracking
Quick startEvent forwarding
MCP server
Guides
OverviewAuthenticationOAuthAgent skills (prompts)AI intelligenceGoogle Ads
Client setup
CursorClaude DesktopVS CodeCodex
Realtime
Debug ViewRealtime Dashboard
Integration guides
Frontend frameworks
Next.jsNuxt.jsVue.jsReactReact RouterRemixGatsbySvelte / SvelteKitAstroAngularTanStack StartDocusaurus
Backend frameworks
NestJSHonoCloudflare WorkersDjangoFlaskLaravelPhoenixRuby on Rails
Backend languages
PythonPHPRubyElixirGoJava.NET / C#Rust
Stack guides
Vue + PHP
SDK
Browser SDK
InstallScript bundlesEvent trackingAutocaptureIdentify & aliasWeb VitalsSession recordingChat widgetFeature readinessRemote configurationReverse proxyDebug logging
Node SDK
Install & setupCapture, identify & aliasContext & shutdown

Documentation

vTilt
Quick startEvent forwarding

MCP server

Realtime

Debug ViewRealtime Dashboard

Integration guides

SDK

InstallScript bundlesEvent trackingAutocaptureIdentify & aliasWeb VitalsSession recordingChat widgetFeature readinessRemote configurationReverse proxyDebug logging
DocsBrowser SDKEvent tracking

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, and navigator.webdriver against 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,
})
typescript

To block an extra user agent substring:

vt.init('YOUR_PROJECT_TOKEN', {
  api_host: 'https://www.vtilt.com',
  custom_blocked_useragents: ['my-internal-crawler'],
})
typescript

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 EventBuffer until __remote_config_loaded (a fresh /api/d merge or fetch failure). Then they batch to /api/e. A safety timeout can flush earlier with $config_pending so the server can still filter.
  • Google Tag (browser) — The SDK subscribes to captures and forwards through gtag when the Google Tag destination is enabled. Until __remote_config_loaded, captures are held in a small bounded queue (max 100). After that, the official dataLayer / gtag shim queues measurement calls until gtag.js finishes 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,
})
typescript

#Standard properties

Every capture is automatically enriched with:

PropertyMeaning
$current_urlFull URL at the time of capture.
$pathnameJust the path (/products/123).
$referrerdocument.referrer at capture time.
$session_idSticky session identifier (30-minute idle window).
$device_idStable anonymous device identifier.
$distinct_idIdentified user id, or the anonymous device id pre-identify().
engagement_time_msecMilliseconds 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.

PreviousScript bundlesBrowser SDKNextAutocaptureBrowser SDK

On this page

  • Bots and crawlers
  • How delivery works
  • Capture events
  • Standard properties
  • When to use a custom event vs autocapture