The write path: takes a raw hit, enriches it, and stores one
PhoenixKitWebAnalytics.Schemas.Event.
A hit never costs the request anything
track_async/1 hands the work to a Task.Supervisor and returns
immediately, so the enrichment (a settings read, one session-stitching query,
the insert) happens after the response is on its way out. The request process
spends microseconds building a map.
The task supervisor is started with a max_children cap. Under a flood, once
the cap is reached, further hits are dropped rather than queued — an
analytics backlog must not become the reason a host runs out of database
connections. Drops are logged at debug level.
Raw hit shape
Every key is optional except :path:
%{
event_type: "pageview" | "event", # default "pageview"
event_name: "signup", # required for "event"
path: "/pricing",
page_title: "Pricing",
site: "myapp.com",
referrer: "https://news.ycombinator.com/",
query_params: %{"utm_source" => "hn"},
ip: {127, 0, 0, 1},
user_agent: "Mozilla/5.0 …",
language: "en-US",
user_uuid: "018e…",
status: 200,
duration_ms: 12,
location: %{country_code: "EE"}, # pre-resolved (edge headers)
metadata: %{"plan" => "pro"}
}:ip and :user_agent are used for the daily visitor hash and the client
classification, then discarded — see PhoenixKitWebAnalytics.Visitor.
Summary
Functions
Resolves which session a visitor's hit belongs to.
Child spec for the task supervisor that runs the async writes.
Stores a hit synchronously.
Stores a hit off the request path. Always returns :ok.
Functions
@spec resolve_session(String.t(), pos_integer(), DateTime.t()) :: Ecto.UUID.t()
Resolves which session a visitor's hit belongs to.
Reuses the visitor's previous session when their last hit is within
timeout_minutes, otherwise mints a new one. This is the whole reason no
session cookie is needed: the stitch is an indexed lookup on
(visitor_id, inserted_at), server-side.
@spec task_supervisor_spec() :: Supervisor.child_spec()
Child spec for the task supervisor that runs the async writes.
Returned from PhoenixKitWebAnalytics.children/0, so a host running
PhoenixKit's module supervision gets it with no configuration.
@spec track(map()) :: {:ok, PhoenixKitWebAnalytics.Schemas.Event.t()} | {:error, :disabled | :bot | :invalid | Ecto.Changeset.t() | term()}
Stores a hit synchronously.
Used by tests (an async task can't see the Ecto sandbox connection) and by
callers that want the result. Returns {:error, :disabled} when tracking is
off and {:error, :bot} when the hit was filtered as automated traffic —
both are ordinary outcomes, not failures.
@spec track_async(map()) :: :ok
Stores a hit off the request path. Always returns :ok.
Falls back to an unsupervised process when the task supervisor isn't running (a host that hasn't wired PhoenixKit's module children), so tracking still works — just without the backpressure cap.