Pixelex.Attribution (Pixelex v0.1.0)

Copy Markdown View Source

Where a visitor came from, worked out rather than declared.

The premise: a marketer should not have to tag anything. Ad platforms already stamp a click id on every click they sell, browsers already send a referrer, and between the two almost every visit can be attributed without a single utm_source. UTM parameters are still read when present — they are an explicit statement of intent and beat inference — but they are an override, not the mechanism.

Resolution order

  1. A paid click id (fbclid, gclid, ttclid, …). Highest confidence: the platform stamped it, so somebody was charged for this visit. See Pixelex.Attribution.ClickIds.
  2. Explicit campaign parametersutm_*, or Plausible's short ref. Deliberately below click ids: a stale utm_source copied into a link is common, a forged gclid is not.
  3. The referrer, classified by ref_inspector into search / social / email / paid / referral, with the search term when the engine leaks it.
  4. Direct — nothing to go on.

A referrer from the site's own domain is not a referral; it is someone clicking around. Pass :hostname so those resolve to :internal and leave the visitor's real first touch intact.

First touch and last touch

Both are kept. Last touch answers "what closed this", first touch answers "what found them", and a library that stores only one has picked a side of an argument that is not its to settle. merge/2 is the rule: first touch is written once and never overwritten, last touch is overwritten by any new non-direct touch — a direct visit does not erase the campaign that earned it.

Degrading without ref_inspector

ref_inspector is an optional dependency, so referrer classification falls back to the referring hostname as the source with medium "referral". Click ids, UTM parameters, and self-referral detection all still work; only search vs. social vs. email gets coarser. Pixelex.Attribution.classifier/0 reports which mode is active.

Summary

Functions

:ref_inspector when the classifier is available, :hostname when degraded.

Is this touch attributable to nothing at all?

Fold a new touch into a stored %{"first" => …, "last" => …} map.

Struct to a JSON-safe map, dropping empty fields.

Resolve one touch from a landing URL and a referrer.

Types

t()

@type t() :: %Pixelex.Attribution{
  campaign: String.t() | nil,
  click_id: String.t() | nil,
  click_id_param: String.t() | nil,
  content: String.t() | nil,
  medium: String.t() | nil,
  network: String.t() | nil,
  referrer: String.t() | nil,
  source: String.t() | nil,
  term: String.t() | nil
}

Functions

classifier()

@spec classifier() :: :ref_inspector | :hostname

:ref_inspector when the classifier is available, :hostname when degraded.

direct?(attribution)

@spec direct?(t()) :: boolean()

Is this touch attributable to nothing at all?

merge(existing, touch)

@spec merge(map() | nil, t()) :: map()

Fold a new touch into a stored %{"first" => …, "last" => …} map.

First touch is immutable. Last touch is replaced by any touch that is not direct — a visitor returning by typing the URL should not wipe out the ad that brought them yesterday.

to_map(t)

@spec to_map(t()) :: map()

Struct to a JSON-safe map, dropping empty fields.

touch(url, referrer, opts \\ [])

@spec touch(String.t() | nil, String.t() | nil, keyword()) :: t()

Resolve one touch from a landing URL and a referrer.

Options

  • :hostname — the site's own host, so self-referrals are recognised.

Examples

iex> t = Pixelex.Attribution.touch("https://shop.test/x?fbclid=IwAR9", nil)
iex> {t.network, t.medium, t.click_id}
{"facebook", "paid_social", "IwAR9"}

iex> t = Pixelex.Attribution.touch("https://shop.test/x", nil)
iex> {t.source, t.medium}
{"direct", "none"}