Pixelex.Destinations (Pixelex v0.2.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

Two ways, and the first is the one most people want:

  • the dashboard. pixelex_settings "/analytics/settings" mounts Pixelex.Dashboard.Settings, where a tenant pastes the snippet their ad platform gave them and clicks Test. See that module.
  • config :pixelex, sites:, for a single-tenant app that keeps its credentials with the rest of its secrets. Config wins over the database, so a site defined there cannot be edited from the dashboard — the settings screen says so rather than saving into a void.

Either way the shape is the same:

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

Set config :pixelex, secret_key: and everything marked secret in a destination's Pixelex.Destination.fields/0 is encrypted at rest by Pixelex.Secrets. Without a key it is stored as given, which is the right default only while the credentials come from config in the first place.

Summary

Functions

The canonical events every destination maps from.

Every configurable platform, as {module, fields}, in modules/0 order.

A site's per-platform credentials, atomised and decrypted.

Forget one platform's credentials entirely.

The whole canonical-event → platform-dialect table.

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

The form a platform needs, from its Pixelex.Destination.fields/0.

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

The destination module answering to platform, or nil.

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

Save one platform's credentials for a site.

Send one real page_view to one platform and report what it said.

Functions

canonical_events()

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

The canonical events every destination maps from.

configurable()

@spec configurable() :: [{module(), [Pixelex.Destination.field()]}]

Every configurable platform, as {module, fields}, in modules/0 order.

credentials(site_id)

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

A site's per-platform credentials, atomised and decrypted.

Secret fields stored by Pixelex.Dashboard.Settings come back out of Pixelex.Secrets; a value that cannot be decrypted is dropped, so the platform reads as unconfigured rather than authenticating with ciphertext.

delete_credentials(site_id, platform)

@spec delete_credentials(String.t(), atom()) ::
  {:ok, Pixelex.Sites.t()} | {:error, term()}

Forget one platform's credentials entirely.

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.

fields(platform)

@spec fields(module() | atom()) :: [Pixelex.Destination.field()]

The form a platform needs, from its Pixelex.Destination.fields/0.

[] for a destination that does not implement the callback: it still delivers, it just cannot be set up from the dashboard.

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.

module(platform)

@spec module(module() | atom()) :: module() | nil

The destination module answering to platform, or nil.

modules()

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

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

put_credentials(site_id, platform, attrs)

@spec put_credentials(String.t(), atom(), map()) ::
  {:ok, Pixelex.Sites.t()} | {:error, term()}

Save one platform's credentials for a site.

Everything the settings screen needs, in one call:

  • ids are run through Pixelex.Destinations.Detect so a pasted <script> snippet works exactly as well as a typed id
  • a blank secret keeps the stored one — the form never receives it, so a blank field means "unchanged", not "erase"
  • secrets are encrypted through Pixelex.Secrets when a key is configured
  • other platforms on the site are untouched

Refuses a site defined in config :pixelex, sites:, which the database can never override — a silent no-op there would be a save button that lies.

test(site_id, platform)

@spec test(String.t(), atom()) :: :ok | {:error, term()}

Send one real page_view to one platform and report what it said.

The point of the settings screen: credentials are only ever wrong in ways that surface as a silent gap in reporting three weeks later. A round trip at save time turns that into a red line under a text box.

The event_id is random here — the one place in this library where that is correct, because a deterministic id would be deduplicated away and the second test would report success without a request leaving the building. Meta's test_event_code is used when set, so the event lands in Test Events rather than in the advertiser's real numbers.