Documentation
¶
Overview ¶
Package destinations is your events forwarded to the ad and analytics tools you use.
Server-side conversion forwarding: connect Google Analytics 4, Meta, X, LinkedIn, TikTok, Reddit, PostHog or Umami, and every event the org captures is translated into that platform's own conversion schema and sent from the server, with PII hashed on the way out.
It is a CONSUMER of the canonical /v1/event stream (apps/analytics), installed as a sink at Mount — never a second collector and never a second ingest door.
The plane is four decomplected concerns, one per file group:
- Destination interface + per-platform adapters (this file + ga4/meta/… .go): an adapter renders the normalized Conversion into its platform's wire shape and delivers it. Adapters self-register from init() — a new platform is a new file, never a change to the fan-out.
- translator (translate.go): maps the canonical EVENTS vocabulary once onto the normalized StandardEvent taxonomy + lifts match keys — the ONE interlingua every adapter renders from.
- per-org registry (store.go + KMS custody in destinations.go): the connected destinations + their non-secret ids (measurement/pixel), with the API secrets KMS-sealed per org.
- fan-out consumer (fanout.go): the apps/analytics sink — for each of an org's enabled destinations, translate + Send, bounded and fail-soft.
SECRET CUSTODY mirrors apps/integrations: a destination's API secret lives ONLY in KMS (sealed, per org, at /orgs/{org}/destinations/{platform}); the store holds only the non-secret ids. A destination may instead ride an existing integrations connection's token (Meta CAPI reuses the meta_ads OAuth token) — Spec.Fallback.
Index ¶
- func Mount(app cloud.Router, deps cloud.Deps) error
- func Shutdown() error
- type Config
- type Conversion
- type Destination
- type DestinationField
- type DestinationStatus
- type Item
- type Result
- type Row
- type Spec
- type StandardEvent
- type Store
- func (s *Store) Close() error
- func (s *Store) Delete(ctx context.Context, org, platform string) (bool, error)
- func (s *Store) Get(ctx context.Context, org, platform string) (Row, bool, error)
- func (s *Store) List(ctx context.Context, org string) ([]Row, error)
- func (s *Store) ListEnabled(ctx context.Context, org string) ([]Row, error)
- func (s *Store) Upsert(ctx context.Context, r Row) error
- type UserData
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Config ¶
Config is an org's non-secret destination configuration — the stored ids the connect body fills and the fan-out passes to Send. It never contains a secret.
type Conversion ¶
type Conversion struct {
Standard StandardEvent
Name string // raw canonical event name (order_completed, …)
EventID string // dedup id (pixel <-> CAPI)
Time time.Time
Value float64
Currency string
User UserData
URL string
Referrer string // referring URL (a first-party analytics dimension)
Items []Item // ecommerce line items (nil for non-commerce events)
Properties map[string]any
}
Conversion is one canonical event translated to the normalized model every adapter renders. Standard is the normalized type (EventCustom ⇒ forward Name raw); EventID is the dedup id shared between a browser pixel and the server CAPI event.
func Translate ¶
func Translate(ev analytics.SinkEvent) Conversion
Translate maps one canonical event onto the normalized Conversion every adapter renders. It is pure — no I/O — so the mapping is driven directly by tests. The raw (pre-warehouse-scrub) properties carry the match keys the User set is lifted from.
type Destination ¶
type Destination interface {
ID() string // stable slug: ga4|meta|tiktok|linkedin|x|reddit
Name() string // display name
Category() string // Analytics | Advertising
Spec() Spec
// Send delivers batch for one org. secret is the resolved primary credential
// (its own KMS secret, else the Spec.Fallback integrations token).
Send(ctx context.Context, cfg Config, secret string, batch []Conversion) (Result, error)
}
Destination is one external ad/analytics platform Hanzo forwards to. Adapters are pure over (Config, secret, batch): given an org's resolved non-secret config, its resolved credential, and the normalized conversions, Send renders the platform's wire shape and delivers it. An adapter reads no global state and no other tenant's data — the fan-out hands it exactly one org's config + credential + batch.
type DestinationField ¶ added in v1.801.350
type DestinationField struct {
Key string `json:"key"` // the camelCase key on both the connect body and the stored config
Label string `json:"label"` // human label for the console card's input
Required bool `json:"required"` // when true, a connect that leaves it empty is refused 400
Example string `json:"example,omitempty"` // a sample value of the right shape ("G-XXXXXXX"), when one helps
}
DestinationField is one NON-SECRET config input a destination needs (a measurement or pixel id). It drives the connect contract and the console card. Key is the camelCase key on both the connect body and the stored config.
type DestinationStatus ¶ added in v1.801.350
type DestinationStatus struct {
Platform string `json:"platform"` // the platform slug, and the path segment every route addresses it by
Name string `json:"name"` // the platform's display name ("Google Analytics 4")
Category string `json:"category"` // groups the card: Analytics | Advertising
// Connected is true when this org has a stored row for the platform — it has
// been configured here at least once. It says nothing about whether a
// credential still resolves; that is Live.
Connected bool `json:"connected"`
// Enabled is whether the fan-out forwards to this destination. False on a
// destination that is connected but paused, and on one never connected.
Enabled bool `json:"enabled"`
// Live is whether a credential resolves RIGHT NOW: a KMS-sealed secret for this
// org, else the integrations connection named by the platform's Fallback, else
// no credential needed at all (a public-ingest sink like Umami). False on a
// connected destination whose secret has gone missing — Connected && !Live is
// exactly the "reconnect me" state.
Live bool `json:"live"`
// Account is the operator's own label for the connected account, as supplied on
// connect. Absent when unset.
Account string `json:"account,omitempty"`
// Config is the org's stored NON-SECRET configuration — the measurement/pixel
// ids keyed by DestinationField.Key. A secret is never in here; secrets live in
// KMS and only their names are published, in Secrets.
Config Config `json:"config,omitempty"`
// Fields are the non-secret inputs this platform needs, which the console card
// renders and the connect body fills.
Fields []DestinationField `json:"fields"`
// Secrets are the KMS secret NAMES this platform custodies for the org — names
// only, never values. The connect body accepts each under its camelCase form.
Secrets []string `json:"secrets"`
}
DestinationStatus is a destination's card for an org: its Spec (fields the console renders), this org's connection state, and whether a credential is resolvable (live).
func Connect ¶
func Connect(ctx context.Context, org, platform string, in map[string]any) (DestinationStatus, error)
Connect provisions (or updates) a destination's NON-SECRET config for org — the seam the guide's destinations_connect MCP tool drives. It NEVER accepts a secret (secrets flow only via the authenticated HTTP connect body → KMS), so the tool args and the guide action ledger never carry one. It requires the destination's REQUIRED non-secret fields (the guide cannot fabricate a measurement/pixel id), returning an honest error otherwise, and reports whether the destination is now live. Fails closed when unmounted / invalid org / unknown platform.
type Item ¶
type Item struct {
ID string // SKU / product id → GA4 item_id, Meta content id
Name string // → GA4 item_name
Category string // → GA4 item_category
Brand string // → GA4 item_brand
Variant string // → GA4 item_variant
Price float64 // unit price → GA4 price, Meta item_price
Quantity float64 // → GA4 quantity, Meta quantity
}
Item is one normalized ecommerce line item — the interlingua between an event's raw items/products array (or a first-class product id) and each platform's product schema (GA4 items[], Meta contents[]/content_ids[]). The translator lifts it ONCE (liftItems); every adapter renders it into its platform's shape. An empty field is omitted by each renderer, so a sink only ever sees what the event actually carried.
type Result ¶
Result is a Send outcome: how many events the platform accepted. Some APIs do not report a count; on a 2xx those set Sent to the batch length.
type Row ¶
type Row struct {
Org string `json:"-"`
Platform string `json:"platform"`
Enabled bool `json:"enabled"`
Config Config `json:"config"`
AccountLabel string `json:"account,omitempty"`
ConnectedAt int64 `json:"connectedAt"`
UpdatedAt int64 `json:"updatedAt"`
}
Row is one org's connected destination: the platform, whether the fan-out forwards to it, and the non-secret Config. The secret is NOT here (KMS).
type Spec ¶
type Spec struct {
Fields []DestinationField `json:"fields"`
Secrets []string `json:"secrets"`
Fallback string `json:"fallback,omitempty"`
}
Spec is a destination's declared shape: the non-secret Fields it needs, the KMS secret names it custodies, and an optional integrations provider id to source the primary secret from when none is sealed locally (Meta CAPI → meta_ads token). The connect body key for a secret is the camelCase of its KMS name (api_secret → apiSecret); both forms are accepted.
type StandardEvent ¶
type StandardEvent string
StandardEvent is Hanzo's normalized conversion taxonomy — the interlingua between the canonical EVENTS vocabulary (@hanzo/event) and each platform's own standard-event names. The translator maps canonical → StandardEvent ONCE (translate.go); each adapter maps StandardEvent → its platform's name. The empty value means "no standard mapping": the event is forwarded under its raw canonical name as a custom event.
const ( EventPageView StandardEvent = "page_view" EventViewContent StandardEvent = "view_content" EventSearch StandardEvent = "search" EventLead StandardEvent = "lead" EventSignUp StandardEvent = "signup" EventStartCheckout StandardEvent = "start_checkout" EventAddToCart StandardEvent = "add_to_cart" EventPurchase StandardEvent = "purchase" EventContact StandardEvent = "contact" EventCustom StandardEvent = "" // forwarded under the raw canonical name )
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the destinations database. ONE SQLite file — the system namespace's "destinations" — holds every org's connected destinations; tenant isolation is the `org` column, enforced on EVERY query (the ads/integrations pattern). The row holds only the NON-SECRET config (measurement/pixel ids) as JSON — the API secret lives in KMS, never here. MaxOpenConns(1) serializes writes against the single-writer file.
func (*Store) Delete ¶
Delete removes a destination. Reports whether a row went (idempotent caller).
func (*Store) Get ¶
Get returns the destination for (org,platform). found=false (nil error) when there is no row.
func (*Store) ListEnabled ¶
ListEnabled returns the org's ENABLED destinations — the fan-out set.
type UserData ¶
type UserData struct {
Email string
Phone string
ExternalID string
IP string
UserAgent string
FBP string // Meta browser-id cookie (_fbp), if present
Clicks map[string]string // fbclid|gclid|ttclid|twclid|rdt_cid|li_fat_id|msclkid → value
}
UserData is the raw match-key set the translator lifts from an event. Adapters SHA-256 the PII fields (email/phone/externalId) before send (advanced matching); click ids, ip, and user agent ride per each platform's contract. NOTHING here is stored — it is built per batch, used to render the outbound payload, and dropped.