destinations

package
v1.801.458 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func Mount

func Mount(app cloud.Router, deps cloud.Deps) error

Mount wires /v1/destinations/* onto app. Complex flavour (a package global for the seam + Shutdown, and it installs the analytics fan-out sink), so it constructs the Service value directly.

func Shutdown

func Shutdown() error

Shutdown closes the store and clears the sink. Idempotent.

Types

type Config

type Config map[string]string

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.

func List

func List(ctx context.Context, org string) ([]DestinationStatus, error)

List returns the org's destination status for every registered platform — the seam a sibling (the guide) reads to report what is connected. Fails closed when unmounted.

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

type Result struct {
	Sent    int    `json:"sent"`
	Message string `json:"message,omitempty"`
}

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) Close

func (s *Store) Close() error

Close closes the underlying database. Idempotent-safe via sql.DB.

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, org, platform string) (bool, error)

Delete removes a destination. Reports whether a row went (idempotent caller).

func (*Store) Get

func (s *Store) Get(ctx context.Context, org, platform string) (Row, bool, error)

Get returns the destination for (org,platform). found=false (nil error) when there is no row.

func (*Store) List

func (s *Store) List(ctx context.Context, org string) ([]Row, error)

List returns every destination for org (enabled and disabled), newest first.

func (*Store) ListEnabled

func (s *Store) ListEnabled(ctx context.Context, org string) ([]Row, error)

ListEnabled returns the org's ENABLED destinations — the fan-out set.

func (*Store) Upsert

func (s *Store) Upsert(ctx context.Context, r Row) error

Upsert stores (or refreshes) a destination. On a re-connect the original connected_at is PRESERVED ("connected since"); only config/enabled/label and updated_at advance.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL