Turns an untrusted beacon payload into a hit map for
PhoenixKitWebAnalytics.Collector.
This is the trust boundary for the two public collection endpoints, kept out of the controller so it can be tested directly for exactly the things that matter: what a client can and cannot influence.
A client controls only the content of a hit — which path, which event name, which properties. It cannot influence identity:
- only the path is read from the client's URL; scheme and host are dropped
(campaign parameters and an ad-click identifier are read from its query
string — so a beacon-recorded
click_idis client-supplied, like any landing URL) user_uuidis never read from the body (the collection endpoints run without a session, so beacon hits are never attributed to a user)visitor_idis derived server-side downstream and isn't representable here at all- the event type is one of a fixed set, and a click's kind one of a fixed set; anything else is dropped
site is the request's Host header — the same thing the plug records.
Like every client-side analytics endpoint, a caller can send any Host; a
host that serves many domains should put a canonical-host redirect in front.
Payload
Compact keys, as sent by <.beacon /> and the optional client script:
e—"pageview","event","click","scroll"or"leave"n— custom event name ("event")p— the page URL;tits title;rthe referrerprops— custom event propertiesk— a click's kind:"click","outbound","download","contact";x— what was clicked (a host + path, a file name, a label)ms— visible time on the page ("leave");sd— scroll depth 0–100
Free-form content is capped so an unbounded jsonb blob per event can't turn the analytics table into the largest one in the database: 20 properties, 200 bytes per value, 120 bytes of event name.
Summary
Functions
Whether the payload is one of the client script's hits (clicks, scroll, leave), which are accepted only with the client script switched on — as opposed to the beacon's page views and custom events.
The event type a payload is stored as — "pageview" or "event", kept for
callers of the 0.2 API.
What the payload reports: :pageview, :event, :click, :scroll or
:leave — or :unknown for anything else, which is not stored. An
"event" needs a non-empty name n; a payload with only a name is an
event too (the 0.2 snippet's shape).
Extracts just the path from a client-sent URL.
Caps custom event properties by count, key length, and value size.
Builds the hit map for params received on conn.
Extracts campaign parameters and ad-click identifiers from a client-sent
URL's query string (Tracking.campaign_params/1; the name predates the
identifiers).
Functions
Whether the payload is one of the client script's hits (clicks, scroll, leave), which are accepted only with the client script switched on — as opposed to the beacon's page views and custom events.
The event type a payload is stored as — "pageview" or "event", kept for
callers of the 0.2 API.
@spec kind(map()) :: :pageview | :event | :click | :scroll | :leave | :automation | :unknown
What the payload reports: :pageview, :event, :click, :scroll or
:leave — or :unknown for anything else, which is not stored. An
"event" needs a non-empty name n; a payload with only a name is an
event too (the 0.2 snippet's shape).
Extracts just the path from a client-sent URL.
Anything that isn't an absolute path becomes "/" — URI.parse/1 happily
reports a bare word as a relative path, and a "path" that doesn't start with
a slash would corrupt every pages report it appeared in.
iex> PhoenixKitWebAnalytics.Web.BeaconPayload.path("https://evil.example/steal?x=1")
"/steal"
iex> PhoenixKitWebAnalytics.Web.BeaconPayload.path("garbage")
"/"
Caps custom event properties by count, key length, and value size.
@spec to_hit(Plug.Conn.t(), map()) :: map()
Builds the hit map for params received on conn.
Never raises: every field degrades to nil or a default, because the caller
is a public endpoint that must answer 204 no matter what it was sent.
Extracts campaign parameters and ad-click identifiers from a client-sent
URL's query string (Tracking.campaign_params/1; the name predates the
identifiers).