Documentation
¶
Overview ¶
Package destinations is the native fan-out plane: it TRANSLATES the canonical /v1/event stream (clients/analytics) into each connected ad/analytics platform's own conversion schema and forwards it server-side — Google Analytics 4 (Measurement Protocol) + Meta (Conversions API) end-to-end, with X/LinkedIn/ TikTok/Reddit against the same interface.
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 clients/analytics sink — for each of an org's enabled destinations, translate + Send, bounded and fail-soft.
SECRET CUSTODY mirrors clients/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 Field
- type Item
- type Result
- type Row
- type Spec
- type StandardEvent
- type Status
- 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 Field ¶
type Field struct {
Key string `json:"key"`
Label string `json:"label"`
Required bool `json:"required"`
Example string `json:"example,omitempty"`
}
Field 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 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 []Field `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 = "sign_up" EventStartCheckout StandardEvent = "start_checkout" EventAddToCart StandardEvent = "add_to_cart" EventPurchase StandardEvent = "purchase" EventContact StandardEvent = "contact" EventCustom StandardEvent = "" // forwarded under the raw canonical name )
type Status ¶
type Status struct {
Platform string `json:"platform"`
Name string `json:"name"`
Category string `json:"category"`
Connected bool `json:"connected"`
Enabled bool `json:"enabled"`
Live bool `json:"live"`
Account string `json:"account,omitempty"`
Config Config `json:"config,omitempty"`
Fields []Field `json:"fields"`
Secrets []string `json:"secrets"`
}
Status 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 ¶
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 Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the destinations database. ONE SQLite file ({DataDir}/destinations.db) 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.