Pixelex.Destinations (Pixelex v0.1.0)

Copy Markdown View Source

One call, every ad platform a site has configured.

Pixelex.Destinations.fire("shop", :purchase,
  event_id: "order:" <> order.id,
  event_source_url: url,
  user_data: %{email: patient.email, phone: patient.phone, ip: ip, fbclid: fbclid},
  custom_data: %{currency: "EGP", value: 1499.0}
)

Each platform no-ops without its own credentials, so a site that set up only Meta behaves exactly as though the others did not exist.

event_id is the whole design

The same id goes to every platform and to the browser pixel. Each platform deduplicates on it, so firing both legs counts one conversion — and, less obviously, it is what makes retrying safe. Derive it from the row it describes ("order:#{order.id}", never UUID.generate()) and a replay can never double-count. Everything else here depends on that.

Delivery is durable, or it says so

With oban installed, fire/3 enqueues and Pixelex.Destinations.Worker makes the calls with five attempts. Without it, delivery falls back to an unsupervised task and logs a warning once — a deploy mid-flight then drops the conversion with no record and no retry, which is silent under-reporting of exactly the events ad spend optimises against.

Secrets are not put in the queue

The job carries a site id and an event name. Credentials are re-read when it runs. Writing decrypted access tokens into a database-backed job table would be strictly worse than the extra read, and rotating a token would leave stale secrets sitting in the queue.

Configuring a site

%{
  "meta"      => %{"pixel_id" => "…", "access_token" => "…"},
  "tiktok"    => %{"pixel_code" => "…", "access_token" => "…"},
  "snapchat"  => %{"pixel_id" => "…", "access_token" => "…"},
  "ga4"       => %{"measurement_id" => "G-…", "api_secret" => "…"}
}

in pixelex_sites.destinations, or under config :pixelex, sites:. Encrypting them at rest is the host's job — pixelex never logs them, but it cannot encrypt a column it does not own.

Summary

Functions

The canonical events every destination maps from.

A site's per-platform credentials, keys converted to atoms safely.

The whole canonical-event → platform-dialect table.

Deliver synchronously to every configured platform. Called by the worker.

Queue event for every platform site_id has configured. Always :ok.

Destination modules in play — the built-ins, plus anything configured.

Functions

canonical_events()

@spec canonical_events() :: [atom()]

The canonical events every destination maps from.

credentials(site_id)

@spec credentials(String.t()) :: %{required(atom()) => map()}

A site's per-platform credentials, keys converted to atoms safely.

dialects()

@spec dialects() :: %{required(atom()) => %{required(atom()) => String.t() | nil}}

The whole canonical-event → platform-dialect table.

Pixelex.Destinations.dialects()[:purchase]
#=> %{meta: "Purchase", tiktok: "CompletePayment", snapchat: "PURCHASE", ga4: "purchase"}

A nil means the platform genuinely has no equivalent.

dispatch(site_id, event, opts)

@spec dispatch(String.t(), atom(), keyword()) :: [{atom(), :ok | {:error, term()}}]

Deliver synchronously to every configured platform. Called by the worker.

Deliberately not rescued. A raise here fails the Oban job so it retries, which is the entire point of not using a fire-and-forget task.

fire(site_id, event, opts \\ [])

@spec fire(String.t(), atom(), keyword()) :: :ok

Queue event for every platform site_id has configured. Always :ok.

Best-effort at the call site by design: a tracking problem is never the reason a booking or a payment fails.

Options

  • :event_idrequired in practice. Derive it from the row.
  • :event_source_url, :action_source
  • :user_data — raw, unhashed. Hashing happens per platform, at the edge.
  • :custom_datacurrency, value, content_ids, …
  • :consent — signals from Pixelex.Plug.Context; omitted means no visitor is involved (a cron, an admin action) and no banner applies.

modules()

@spec modules() :: [module()]

Destination modules in play — the built-ins, plus anything configured.