PhoenixKitWebAnalytics.Web.BeaconPayload (PhoenixKitWebAnalytics v0.5.0)

Copy Markdown View Source

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_id is client-supplied, like any landing URL)
  • user_uuid is never read from the body (the collection endpoints run without a session, so beacon hits are never attributed to a user)
  • visitor_id is 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; t its title; r the referrer
  • props — custom event properties
  • k — 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

client_script_hit?(params)

@spec client_script_hit?(map()) :: boolean()

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.

event_type(params)

@spec event_type(map()) :: String.t()

The event type a payload is stored as — "pageview" or "event", kept for callers of the 0.2 API.

kind(params)

@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).

path(url)

@spec path(term()) :: String.t()

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")
"/"

props(props)

@spec props(term()) :: map()

Caps custom event properties by count, key length, and value size.

to_hit(conn, params)

@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.

utm_params(url)

@spec utm_params(term()) :: map()

Extracts campaign parameters and ad-click identifiers from a client-sent URL's query string (Tracking.campaign_params/1; the name predates the identifiers).