How tracking works
Data model
Section titled “Data model”Visitor one row per browser/device, ever — durable identity └─ Session one browsing session, a new one after session_timeout_minutes of inactivity └─ Event one page view (type page_view) or custom action (type action)| Model | Table | What it holds |
|---|---|---|
Visitor | visit_visitors | Visitor id (token), first-touch attribution, last-known geo/device/locale, linked user |
Session | visit_sessions | Landing/exit URL, referrer, last-touch attribution, IP, a snapshot of geo/device at session start, page view count, duration |
Event | visit_events | type, name, URL/path/route name, bot name, optional eventable model, meta JSON |
StatDaily | visit_stats_daily | Daily counts per metric and dimension, built by visits:aggregate for the dashboard |
Two rules repeat across the columns:
- Visitor = first touch or last known, Session = snapshot. UTM/
ref,search_term,extra_paramsand the landing/referrer URL on the visitor are written once, when the visitor row is created. Geo, device and locale on the visitor are overwritten with the latest values. A session stores what was true when it started. - Real columns for what you filter by, JSON for the rest.
utm_source,country_code,device_type,browserand similar are indexed columns; driver-specific geo fields, browser versions and ad click IDs go togeo_meta,device_metaandextra_params.
Full column list: Database tables.
Request flow
Section titled “Request flow”Three entry points build the same payload and dispatch the same job:
flowchart LR
T["TrackVisit middleware<br/>GET through 'web'"] --> P
C["POST /visits/collect<br/>JS beacon, mobile app"] --> P
V["Visits::track()<br/>PHP, any context"] --> P
P["resolve visitor id<br/>queue cookie<br/>build payload"] --> Q(("queue"))
Q --> J["RecordVisitJob"]
J --> D[("visit_visitors<br/>visit_sessions<br/>visit_events")]
During the request only cheap work happens: the visitor id is resolved (or generated), the cookie is queued on the response, the query string, referrer, locale and authenticated user are captured into a VisitPayload, and RecordVisitJob is dispatched to visits.queue.
Everything else runs in the job, in this order:
- IP in
exclude_ips— stop, nothing is written - Device and bot detection from the User-Agent
- Per-visitor budget (
rate_limit.visitor_budget) exceeded — stop, nothing is written - Geo lookup for the IP, skipped for bots, cached per IP
- Find or create the
Visitorby id — firesVisitorCreatedfor a new one - Reuse the visitor’s open session if it was active within
session_timeout_minutes, otherwise open a new one — firesSessionStarted - Write the
Event(unlesspage_viewsisfirst_onlyand this is a returning visitor’s page view), incrementpage_views_countfor page views, fireVisitRecorded, andConversionRecordedfor an action with aneventablemodel - Update
last_activity_aton the session andlast_seen_aton the visitor
A step-by-step diagram of the job is in Architecture.
What this means in practice
Section titled “What this means in practice”- Nothing appears without a queue worker. With a lagging queue the dashboard lags by the same amount.
- Timestamps are processing times.
created_at,started_atandlast_activity_atare set when the job runs, not when the request arrived. A queue backlog shifts them, and the dashboard shows events at the time they were processed. - Sessions are opened by events, closed by a command. A new event after the timeout opens a new session right away; the old one keeps
ended_at = nulluntilvisits:close-stale-sessionssetsended_at,duration_secondsandexit_url. - Attribution is decided when a session opens. UTM parameters on a later page of an already open session are not written anywhere — neither the session (already open) nor the visitor (first touch only).
- Bots are recorded, then hidden.
is_botis stored on every row; the models exclude bots from queries by default (withBots()/onlyBots()to include them). Once a session has seen a bot request it staysis_bot = true. - The dashboard totals come from rollups. Overview and Campaigns read
visit_stats_daily, refreshed byvisits:aggregateevery five minutes by default — see Rollups, sessions & retention.