Co-located span helpers for the webhook ingest surface (CONTEXT D-22).
Mirrors Mailglass.Telemetry.send_span/2 placement convention
(Phase 3 D-26): per-domain helpers live in their own module under
the domain's lib/ directory. The helpers in this module are the
single-module surface for the webhook telemetry contract, which
means Phase 6 LINT-02 (NoPiiInTelemetryMeta) has exactly one
module to lint (plus the call sites).
Events emitted
| Event | Type | Stop metadata keys (D-23 whitelist) |
|---|---|---|
[:mailglass, :webhook, :ingest, :start | :stop | :exception] | full span | provider, tenant_id, status, event_count, duplicate, failure_reason, delivery_id_matched |
[:mailglass, :webhook, :signature, :verify, :start | :stop | :exception] | full span | provider, status, failure_reason |
[:mailglass, :webhook, :normalize, :stop] | single emit | provider, event_type, mapped |
[:mailglass, :webhook, :orphan, :stop] | single emit | provider, event_type, tenant_id, age_seconds |
[:mailglass, :webhook, :duplicate, :stop] | single emit | provider, event_type |
[:mailglass, :webhook, :reconcile, :start | :stop | :exception] | full span | tenant_id, scanned_count, linked_count, remaining_orphan_count, status |
Single-emit helpers delegate to Mailglass.Telemetry.execute/3
(Phase 1). Full-span helpers call :telemetry.span/3 directly
because the Plug needs per-request stop metadata enrichment
(status, failure_reason, event_count, duplicate) — the
Mailglass.Telemetry.span/3 wrapper closes metadata at call time,
which cannot express "I know the status once the inner function
returns." :telemetry.span/3 itself provides D-27 handler
isolation: handlers that raise are auto-detached and
[:telemetry, :handler, :failure] fires — a handler crash cannot
propagate into the webhook pipeline. Callers MUST NOT reach for
:telemetry.span/3 directly; use the helpers below so LINT-02
has a single module surface to lint.
Per-request stop metadata enrichment
The full-span helpers (ingest_span/2, verify_span/2,
reconcile_span/2) accept a zero-arity function returning either:
result— bare value; stop metadata equals themetadataargument passed at call time.{result, stop_metadata}— tuple; stop metadata is the returned map. Used by the Plug to attach:status,:failure_reason,:event_count,:duplicateonto the:stopevent after classifying the outcome.
Start metadata is always the metadata argument at call time
(before outcome is known).
Whitelist discipline (D-23)
NEVER include in any metadata map:
:ip,:remote_ip,:user_agent:to,:from,:subject,:body,:html_body,:headers,:recipient,:email:raw_payload,:raw_body
Adopters wanting IP-based abuse triage attach their own handler on
[:mailglass, :webhook, :signature, :verify, :stop] and pull
conn.remote_ip from their own plug lineage (see
guides/webhooks.md).
Phase 6 LINT-02 (NoPiiInTelemetryMeta) lints THIS module plus
every caller against the forbidden-key set.
LINT-10 single-emit exception
The three single-emit helpers (normalize_emit/1, orphan_emit/1,
duplicate_emit/1) are deliberate exceptions to the "every event
is a full :start/:stop/:exception span" rule. They preserve
the 4-level path structure ([:mailglass, :webhook, :action, :stop])
but skip the start/exception pair because they fire from INSIDE the
larger [:mailglass, :webhook, :ingest, *] span (which IS a full
span) and represent per-event signals inside a wrapped operation.
Phase 6 LINT-10 whitelists these three event paths.
Summary
Functions
Single-emit per-ingest duplicate signal
([:mailglass, :webhook, :duplicate, :stop]).
Wrap the entire webhook ingest path in a [:mailglass, :webhook, :ingest, *] span.
Single-emit per-event normalize signal
([:mailglass, :webhook, :normalize, :stop]).
Single-emit per-event orphan signal
([:mailglass, :webhook, :orphan, :stop]).
Wrap Mailglass.Webhook.Reconciler.reconcile/2 in a
[:mailglass, :webhook, :reconcile, *] span.
Wrap Provider.verify!/3 in a [:mailglass, :webhook, :signature, :verify, *] span.
Functions
@spec duplicate_emit(map()) :: :ok
Single-emit per-ingest duplicate signal
([:mailglass, :webhook, :duplicate, :stop]).
Metadata SHOULD include :provider, :event_type. Lets adopters
distinguish provider retry storms from real traffic cheaply via
Grafana panels on the emit rate (D-24 alternative to log-scraping).
Wrap the entire webhook ingest path in a [:mailglass, :webhook, :ingest, *] span.
Stop metadata SHOULD include :provider, :tenant_id, :status,
:event_count, :duplicate, :delivery_id_matched. NEVER include
PII (see the module doc whitelist).
fun may return a bare result OR {result, stop_metadata} — see
the moduledoc's "Per-request stop metadata enrichment" section.
@spec normalize_emit(map()) :: :ok
Single-emit per-event normalize signal
([:mailglass, :webhook, :normalize, :stop]).
Metadata SHOULD include :provider, :event_type, :mapped.
Alertable on sustained mapped: false rate (D-22).
@spec orphan_emit(map()) :: :ok
Single-emit per-event orphan signal
([:mailglass, :webhook, :orphan, :stop]).
Metadata SHOULD include :provider, :event_type, :tenant_id,
:age_seconds. Fires once per normalized event that lands without
a matching Delivery. Plan 07 Reconciler closes the race by
appending a :reconciled event when the matching Delivery surfaces.
Wrap Mailglass.Webhook.Reconciler.reconcile/2 in a
[:mailglass, :webhook, :reconcile, *] span.
Stop metadata SHOULD include :tenant_id, :scanned_count,
:linked_count, :remaining_orphan_count, :status.
fun may return a bare result OR {result, stop_metadata}.
Wrap Provider.verify!/3 in a [:mailglass, :webhook, :signature, :verify, *] span.
Stop metadata SHOULD include :provider, :status, :failure_reason.
fun may return a bare result OR {result, stop_metadata}.