analytics

package
v1.801.388 Latest Latest
Warning

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

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

Documentation

Overview

Package analytics is product analytics: send an event, read back who did what.

It is the product-event plane: it owns the ingest door every Hanzo client posts to, lands each event in the `hanzo` warehouse, and serves the per-org read lenses — KPIs, time series, rankings, captured errors — over what it wrote.

It is BOTH halves, and that is deliberate: one write core (ingestEvents) behind N doors (event.go), and the read lenses over the same warehouse, so a fact is admitted, stamped with the SERVER-resolved tenant, and read back through one vocabulary. Two lenses share one warehouse:

  • LLM lens (REAL today): hanzo.cloud_usage, the live per-org usage ledger the cloud o11y path already writes (requests, tokens, spend, models, errors).
  • Web/commerce lens: event.fact (signal='act') on the o11y-owned event plane — what this package's own doors ingest (as facts, landed by the sink in warehouse.go).

The accepted batch is also handed to registered SINKS (forward.go) — apps/ destinations forwards it to the org's connected ad platforms. analytics never imports a consumer; the seam is one-way and fail-soft.

ONE datastore client. This package does NOT open its own connection: it reads through apps/datastore, the leaf that holds the warehouse connection for the whole binary and opens it from the environment on first use. DRY: one transport, one pool, one set of KMS-injected DATASTORE_* creds — never hard-coded, never a second design. (It used to reach the connection through ai/object's Bootstrap; that path cost 1,933 packages to call four functions, which is why the connection moved to a leaf importing only orm/datastore.)

TENANT ISOLATION is the security bar and is enforced SERVER-SIDE on every request. The org is c.Org() — the value SanitizeIdentity minted from the VALIDATED bearer owner claim (HIP-0026), never a client header — AND every request must carry a validated principal (c.User() set, which SanitizeIdentity sets ONLY for a verified bearer). This closes the Phase-1 "no-bearer + forged X-Org-Id direct-to-pod" cross-tenant read exactly as apps/s3 does. Every datastore query binds the org POSITIONALLY (query.go llmWhere/eventsWhere), so a maxpower token can NEVER read another org's analytics.

Surface (all org-scoped; /v1 only):

READ
GET  /v1/analytics/overview     per-org KPIs (llm real; web/commerce honest-empty)
GET  /v1/analytics/timeseries   requests/tokens/spend over time (hour|day buckets)
GET  /v1/analytics/top          top models (real) + products + behavior lenses
                                (topPages/topReferrers/topSources over the events lens)
GET  /v1/errors                 the caller org's most recently captured errors
GET  /v1/insights/events        the caller org's most recent product events
GET  /v1/insights/health        the insights surface is serving
GET  /v1/analytics/health       subsystem health (datastore connectivity + lens tables)

WRITE (the ingest door — see doors, event.go)
POST /v1/event                  the canonical wire (object | array | {batch:[…]});
                                decodeEvent sniffs the PostHog wire here too
POST /v1/event/:project/envelope|store   the Sentry error wire, same door

The six reads above /v1/analytics/health are TYPED ops, so each publishes its prose, its In/Out schema, an MCP tool and a CLI command. /v1/analytics/health and the ingest doors are untyped and cannot be typed without moving their wire; routes (below) names each one's blocker where it is registered.

Registered as id "analytics" with cloud.HealthOwner + order 132: it serves its OWN /v1/analytics/health (below), and cloud.HealthOwner makes serve.go skip the generic GET /v1/<name>/health so the always-ok route never shadows the real probe — the same flag the kms/paas/s3 subsystems use. Order 132 binds /v1/analytics/* before the ai subsystem's /v1/* catch-all (150).

Index

Constants

View Source
const EventOrgKey = "org"

EventOrgKey is the ONE field an envelope on this plane names its tenant with. Every consumer resolves the org by reading it, so a publisher that spelled it differently would deliver to nobody rather than fail loudly — which is precisely why the spelling is a constant here, in the plane's own package, and not a string literal at each end.

View Source
const EventSignalKey = "signal"

EventSignalKey is the field that says WHICH VOCABULARY a message on this plane speaks, and it exists because two of them share it.

A fact (message, below) carries it; the subscriber-facing EventEnvelope does not. The two are separate contracts by design, and their subject spaces WOULD have kept them apart — event.<signal> is a closed set of five, event.<folded product name> is whatever a caller names an event — except that the fold maps a product event named "$error" straight onto event.error, which is precisely the subject the fact plane's error writer drains, and "$error" is what a browser error with no name of its own is called. An open namespace minting a reserved token is the collision; the fold's mapping is a PUBLISHED contract (orgs subscribe to it) and so cannot be the thing that moves.

So the discriminator is the BODY, not the subject: each consumer takes the vocabulary it speaks and leaves the other alone. The warehouse lands facts and ignores envelopes (warehouse.go); webhooks delivers envelopes and ignores facts (apps/webhooks). It is a constant HERE, in the plane's own package, for the same reason EventOrgKey is — a consumer that spelled it itself would silently take the wrong half.

Variables

View Source
var EventStream = strings.ToUpper(plane)

EventStream is the JetStream stream every signal lands on, and EventSubjects is the ONE wildcard it binds. Upper-case is the NATS convention for a stream name; it is the same word as the database and the subject root (plane, fact.go) — one name, three layers.

They are EXPORTED because JetStream enforces single ownership: a second stream binding event.> is refused with "subjects overlap with an existing stream", which takes down whichever subsystem loses the race. So a consumer (apps/webhooks) names THIS stream rather than declaring one of its own — the compiler is what keeps the two from drifting into an outage.

View Source
var EventSubjects = []string{plane + ".>"}

EventSubjects is the stream's binding: every signal, present and future. A new signal is a new constant in fact.go and a consumer that filters for it — never a stream edit.

Functions

func AddSink added in v1.801.350

func AddSink(fn func(org string, evs []SinkEvent)) (remove func())

AddSink registers a downstream fan-out consumer and returns its remover. Every registered sink receives every accepted batch, each on its own detached, panic-guarded dispatch — one seam, N consumers, none of which can block or fail ingest or each other.

func EnsureEventStream added in v1.801.350

func EnsureEventStream(ctx context.Context, cl *infra.PubSubClient) error

EnsureEventStream RECONCILES the event plane to the configuration this package chose for it, creating it when absent. It is the ONE declaration of that configuration anywhere in the platform.

It is EXPORTED so a consumer can make sure the plane exists before binding a durable to it — a consumer that starts before the first ingest would otherwise find no stream — WITHOUT holding a second copy of the config. Whoever calls it first applies it, and because there is only one config, it does not matter who that is.

RECONCILES, NOT CREATE-IF-MISSING, and the difference is the whole point. A create-if-missing ensure returns success the moment the stream exists and never looks at what it looks like, so every constant below was decorative on a live deployment: raising the ceiling, shortening the hand-off window, or fixing the discard policy changed the source and nothing else, forever. CreateOrUpdateStream applies them.

DISCARD NEW, NOT OLD, which is the config that was silently wrong. On a full stream the JetStream default (DiscardOld) evicts the OLDEST messages to make room — and the oldest messages on a hand-off stream are precisely the ones no consumer has drained yet. That is data the door already answered 200 for, deleted to make room for data the door has not answered for yet, with no error at either end. DiscardNew inverts it: a full stream REFUSES THE PUBLISH, so publishToStream fails, the door answers 503, and the client retries — backpressure the caller can see instead of loss nobody can. The ceiling stops being a silent shredder and becomes what it reads like: a limit.

MaxAge still expires drained-or-not after streamAge. That is the deliberate hand-off window, not a capacity failure, and a consumer down for three days is an outage to alarm on rather than a case to size storage for.

It goes through cl.JetStream() rather than infra.EnsureStream because the wrapper models neither operation this needs: its StreamConfig has no Discard field at all, and it returns early the moment the stream exists. That accessor is exported for exactly this — a caller that needs a capability the thin wrapper does not carry — and this package is the stream's owner, so the full config belongs here in the vocabulary that can express it. The wrapper's own create-if-missing behavior is a platform-wide defect (every other stream in the fleet is declared through it and is equally undeclarable after creation); fixing it there is a change to hanzoai/commerce and to every stream owner at once, which is not this package's to make. A STREAM IS NOT RENAMEABLE, which is the failure this function has to survive. CreateOrUpdateStream reconciles a stream by NAME; JetStream binds subjects by OWNERSHIP. So when the plane's name changes — EVENTS to EVENT, the day apps/webhooks stopped declaring a plane it only consumes — the old stream keeps event.> and the new name can never bind it. Every publish then 503s, forever, and no restart, redeploy or rollback clears it: the store is durable, so the stale stream outlives the code that made it. That is not a race that resolves, it is a deadlock that needs a migration, and a plane that cannot migrate itself is a plane that takes ingest down until somebody notices.

So the ensure RETIRES the earlier generation. See retire for what it will and will not remove.

func Mount

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

Mount wires the analytics surface onto app per HIP-0106.

func PublishEvents added in v1.801.350

func PublishEvents(org string, evs []SinkEvent)

PublishEvents puts an accepted batch on the plane, one publish per event, under the batch's SERVER-resolved org. It is the ONE way a product event reaches the platform bus, and it lives HERE because this package owns the plane: the stream, the subject grammar, and the envelope are one decision, and a consumer that also published would be a second owner of all three.

FAIL-SOFT, unlike the fact path above. The batch is already COMMITTED as facts (publish, above) by the time this runs, so a bus that is down for this second vocabulary loses only envelope deliveries, never data — the ingest already answered 200 for the durable copy. It is called detached (forward.go), so a slow bus costs a goroutine and never an ingest.

func Shutdown added in v1.801.350

func Shutdown(context.Context) error

Shutdown stops the sink and releases the ingest connection to the bus. Idempotent.

The order is deliberate: stop CONSUMING before closing the PUBLISHING side, so an in-flight insert is never abandoned mid-commit by a connection that went away underneath it.

Types

type Breakdown

type Breakdown struct {
	// Available is false when the product-event table could not be read.
	Available bool `json:"available"`
	// Reason says why the lens is unavailable. Omitted when it is available.
	Reason string `json:"reason,omitempty"`
	// Items is the ranked buckets, most pageviews first. Empty rather than absent.
	Items []BreakdownRow `json:"items"`
	// Source is the warehouse table the lens read.
	Source string `json:"source"`
}

Breakdown is a ranked behavior lens over event.fact. Honest-empty (Available=false) when the events table is absent/errored — never fabricated.

type BreakdownRow

type BreakdownRow struct {
	// Key is the bucket: a requested path, a referrer domain ("(direct)" for none or
	// a same-origin one), or a utm_source ("(none)" when absent).
	Key string `json:"key"`
	// Pageviews is how many $pageview events fell in this bucket.
	Pageviews int64 `json:"pageviews"`
	// Visitors is how many distinct people they came from.
	Visitors int64 `json:"visitors"`
	// Pct is this bucket's share of ALL in-window pageviews, 0..100, one decimal —
	// not of the returned rows, so a top-N shows the long tail honestly.
	Pct float64 `json:"pct"`
}

BreakdownRow is one bucket of a behavior lens (a path, a referrer domain, a utm source). pct is the bucket's share of TOTAL pageviews in-window (the window-fn denominator), so a top-N list honestly shows the long tail rather than re-normalizing to the shown rows.

type CampaignEvents

type CampaignEvents struct {
	Available   bool    `json:"available"`
	Impressions int64   `json:"impressions"`
	Clicks      int64   `json:"clicks"`
	Conversions int64   `json:"conversions"`
	Revenue     float64 `json:"revenue"`
	Visitors    int64   `json:"visitors"`
	Source      string  `json:"source"`
}

CampaignEvents is the per-campaign funnel read from event.event, scoped to (org, utm_campaign[, utm_content]). Impressions/clicks/conversions are counts of the campaign's tagged events; Available is false (honest-empty) when the events warehouse is not connected or the plane is not yet provisioned — never fabricated. Spend is deliberately absent: it is the channel connector's reported number, joined by the campaign plane, not an analytics value.

func CampaignMetrics

func CampaignMetrics(ctx context.Context, org, campaignID, variant string, start, end time.Time) (CampaignEvents, error)

CampaignMetrics reads the (org, campaignID) funnel from the ONE analytics warehouse. variant=="" reads the whole campaign (all creatives); a non-empty variant reads a single creative's slice (utm_content) — the evidence read for a creative A/B. It degrades to honest-empty (Available=false, nil error) when the datastore is not connected, so a campaign metrics view still renders its spend + channels. A genuine query failure against a connected warehouse returns the error (the caller logs it and shows honest-empty) — never a fabricated funnel.

type CaptureBatch

type CaptureBatch struct {
	Batch  []CaptureEvent `json:"batch"`
	Events []CaptureEvent `json:"events"`
}

CaptureBatch is the ingest envelope. `batch` is canonical; `events` is accepted as an alias so a Segment-shaped client works unchanged.

type CaptureEvent

type CaptureEvent struct {
	MessageID   string `json:"messageId"`  // client idempotency id; server mints one if empty
	Type        string `json:"type"`       // pageview | event | identify | group
	Event       string `json:"event"`      // event name (type=event); pageview→$pageview
	Timestamp   string `json:"timestamp"`  // RFC3339; clamped to server-now on skew/absent
	DistinctID  string `json:"distinctId"` // resolved person/visitor id
	AnonymousID string `json:"anonymousId"`
	PersonID    string `json:"personId"`
	SessionID   string `json:"sessionId"`
	Product     string `json:"product"` // emitting surface: console|chat|app|site|admin
	URL         string `json:"url"`
	Path        string `json:"path"`
	Referrer    string `json:"referrer"`
	UTM         UTM    `json:"utm"`
	RefCode     string `json:"refCode"`
	Channel     string `json:"channel"`
	GroupID     string `json:"groupId"`
	// GroupType names WHICH grouping the id belongs to (organization, workspace,
	// account). It is a map key rather than a column so the second grouping is a data
	// change: the plane stores `groups[type] = id`, never group0..group4, because a
	// fifth positional slot is a sixth one waiting to become a version suffix.
	// Absent means the only grouping anything sends today, `organization`.
	GroupType  string     `json:"groupType"`
	SignupWeek string     `json:"signupWeek"`
	ProductID  string     `json:"productId"`
	Quantity   uint32     `json:"quantity"`
	Revenue    float64    `json:"revenue"`
	Currency   string     `json:"currency"`
	Error      *Exception `json:"error"` // set on type:'error' events (folded into properties.$exception)

	// THE OTEL SIGNALS. One envelope carries all four — event, log, span, metric —
	// because they differ in their BODY, not in who sent them or when. Splitting the
	// envelope per signal duplicates org, time, session and identity four ways and
	// lets them drift; the fact plane (fact.go) reads one shape and writes one row.
	//
	// Each is a pointer so absent is distinct from empty: a pageview carries no Span,
	// and a zero-valued one would be a span of duration 0 rather than no span at all.
	Log    *LogBody    `json:"log"`
	Span   *SpanBody   `json:"span"`
	Metric *MetricBody `json:"metric"`
	Clip   *ClipBody   `json:"clip"`

	// Kind narrows Type when the surface knows more than the wire word does — a
	// span's client/server role, a page's navigation kind. Empty means the route's
	// own default, which is what every existing client sends.
	Kind string `json:"kind"`

	// SITE is the deployed property a signal came from, and it is carried on EVERY
	// event, not only on a failure.
	//
	// It lived on the exception alone, so sentry.hanzo.ai could group faults by site
	// while analytics.hanzo.ai — reading the event stream — had no site column at
	// all, and "all sites" was a question the data could not answer. One property
	// per row is what makes the two surfaces read the same world.
	Site string `json:"site"`

	// Level, Release, Environment and Service qualify a signal the same way for
	// every kind: which severity, which build, which deployment, which service. They
	// are not error-only either, for the same reason Site is not.
	Level       string `json:"level"`
	Release     string `json:"release"`
	Environment string `json:"environment"`
	Service     string `json:"service"`

	// TraceID and SpanID correlate a signal to a trace whatever its body is, so a log
	// and the span it was emitted inside join without either owning the other.
	TraceID string `json:"traceId"`
	SpanID  string `json:"spanId"`

	// Resource is the fingerprint of the emitting resource — the host, pod and
	// service attributes a collector already deduplicates into its own dimension
	// table. It joins event.log_resource / event.span_resource on `fingerprint`.
	// ONE name for it: the plane previously carried this fact as `resource UInt64`
	// AND `resource_fingerprint String` on the same row, one of them always zero.
	Resource string `json:"resource"`

	Properties map[string]any `json:"properties"`
	Library    string         `json:"library"`
	LibraryVer string         `json:"libraryVersion"`
}

CaptureEvent is one client-emitted analytics event. The client sends a batch of these; the server owns the tenant (tenant_id is NOT a field here — it can never be set by the client).

type CaptureResult

type CaptureResult struct {
	Accepted int `json:"accepted"`
	Dropped  int `json:"dropped"`
}

CaptureResult is the honest receipt: persisted vs dropped (unroutable) counts.

type ClipBody added in v1.801.381

type ClipBody struct {
	// Object is the blob's address in object storage.
	Object string `json:"object"`
	// Bytes is its size, so a session list can show weight without opening it.
	Bytes uint64 `json:"bytes"`
	// Duration is the wall time the clip covers, in nanoseconds — the same column a
	// span's elapsed time uses, because it is the same fact.
	Duration uint64 `json:"duration"`
}

ClipBody is the body of a session-replay clip: WHERE THE BLOB IS, not the blob.

A clip's recording is a multi-megabyte time-ordered binary. It is wrong for a bus message (which is a hand-off, sized for many small facts) and wrong for a warehouse row (which is a column store optimized for scanning narrow values), so it goes neither place. It is written to object storage by whoever recorded it, and the fact plane carries the INDEX: the address, the size and the span of time it covers.

That is what makes a whole product cost two columns. Everything else a replay needs — which session, which org, which page, when — is already the envelope.

type CommerceOverview

type CommerceOverview struct {
	// Available is false when the product-event table could not be read — the lens is
	// reported missing rather than as zeros that look like no sales.
	Available bool `json:"available"`
	// Reason says why the lens is unavailable. Omitted when it is available.
	Reason string `json:"reason,omitempty"`
	// Orders is how many order_completed events landed in the window.
	Orders int64 `json:"orders"`
	// Revenue is the total those orders carried, in the events' own currency unit.
	Revenue float64 `json:"revenue"`
	// AOV is average order value — Revenue/Orders, rounded to two places. Zero when
	// there were no orders.
	AOV float64 `json:"aov"`
	// Source is the warehouse table the lens read.
	Source string `json:"source"`
}

CommerceOverview is the commerce lens over event.fact. Honest-empty until commerce emits order events.

type Event

type Event struct {
	Event      string         `json:"event"`      // event name (required; empty ⇒ dropped as unroutable)
	Type       string         `json:"type"`       // canonical kind: pageview | error | identify | group | event (default)
	DistinctID string         `json:"distinctId"` // the person/visitor id the caller owns
	Time       string         `json:"time"`       // optional RFC3339; clamped to server-now on skew/absent
	Properties map[string]any `json:"properties"` // everything non-core
}

Event is the canonical analytics event — the entire ingest contract in five fields. Only these are first-class; everything else a caller wants to record travels in Properties (the scrubber runs over it downstream, same as every event). The tenant is NOT a field: it is resolved server-side from IAM, so a caller can only ever write into its OWN org's partition.

type EventEnvelope added in v1.801.350

type EventEnvelope struct {
	Org         string         `json:"org"`
	ID          string         `json:"id"`
	Name        string         `json:"name"`
	DistinctID  string         `json:"distinct_id,omitempty"`
	AnonymousID string         `json:"anonymous_id,omitempty"`
	Time        time.Time      `json:"time"`
	URL         string         `json:"url,omitempty"`
	Path        string         `json:"path,omitempty"`
	Referrer    string         `json:"referrer,omitempty"`
	Revenue     float64        `json:"revenue,omitempty"`
	Currency    string         `json:"currency,omitempty"`
	ProductID   string         `json:"product_id,omitempty"`
	Quantity    uint32         `json:"quantity,omitempty"`
	Properties  map[string]any `json:"properties,omitempty"`
}

EventEnvelope is what an ACCEPTED PRODUCT EVENT looks like on the plane: the subscriber-facing projection of a SinkEvent, carrying the commerce and identity fields a downstream integration converts on.

Org is FIRST and it is the tenant, spelled EventOrgKey — the delivery engine (apps/webhooks) resolves the subscriber's org from it, and an envelope without one is delivered to nobody. It is stamped from the SERVER-resolved tenant, never from the wire, on the same terms as fact.org.

It is a SEPARATE type from message, and deliberately: message is the warehouse's contract (its field names are the column names), this is the SUBSCRIBER's contract, and folding them together would make a webhook payload change every time a table gains a column. What they share is the one thing that must be shared — the tenant key.

type Exception

type Exception struct {
	Type    string `json:"type,omitempty"`
	Message string `json:"message"`
	Stack   string `json:"stack,omitempty"`
	Handled *bool  `json:"handled,omitempty"`

	// Frames is the STRUCTURED stack, when the SDK sent one. Stack stays as the raw
	// text a client without a parser sends, so neither is derived from the other and
	// a client may send either or both.
	//
	// It is what lets a fault be grouped and rendered by function, file and line
	// rather than by a string compare over a whole trace — the raw text cannot
	// answer "is this frame ours" (framesOf marks own) and cannot be scrubbed field
	// by field.
	Frames []Frame `json:"frames,omitempty"`
}

Exception is the captured error carried on a type:'error' WireEvent (mirrors @hanzo/event's Exception). The ingest folds it into properties.$exception so the ONE events schema needs no new columns and the /v1/errors lens can surface it straight from the properties JSON.

type Frame added in v1.801.350

type Frame struct {
	// Function is the called function's name.
	Function string `json:"function,omitempty"`
	// File is the source file. It is treated as a URL and scrubbed, because a
	// bundler emits one with a query string that can carry a token.
	File string `json:"file,omitempty"`
	// Line is the 1-based line number.
	Line uint32 `json:"line,omitempty"`
	// Column is the 1-based column number.
	Column uint32 `json:"column,omitempty"`
}

Frame is one call site in a structured stack. Line and Column are unsigned because a position is never negative and the fact plane stores them that way.

type LLMOverview

type LLMOverview struct {
	// Available is true whenever the ledger answered — including with no usage in the
	// window, which is honest zeros rather than a missing lens.
	Available bool `json:"available"`
	// Requests is how many LLM calls the org made in the window.
	Requests int64 `json:"requests"`
	// Tokens is prompt plus completion tokens over those calls.
	Tokens int64 `json:"tokens"`
	// PromptTokens is the input half of Tokens.
	PromptTokens int64 `json:"promptTokens"`
	// CompletionTokens is the output half of Tokens.
	CompletionTokens int64 `json:"completionTokens"`
	// SpendCents is what those calls cost, in cents.
	SpendCents int64 `json:"spendCents"`
	// Models is how many distinct models the org called.
	Models int64 `json:"models"`
	// Providers is how many distinct providers served them.
	Providers int64 `json:"providers"`
	// Errors is how many of Requests failed.
	Errors int64 `json:"errors"`
	// ErrorRate is Errors/Requests, 0..1, rounded to three places. Zero when there
	// were no requests.
	ErrorRate float64 `json:"errorRate"`
	// Source is the warehouse table the lens read.
	Source string `json:"source"`
}

LLMOverview is the flagship lens: real per-org KPIs from hanzo.cloud_usage.

type LogBody added in v1.801.350

type LogBody struct {
	// Severity is the OTel severity text, lowercased on the way into a fact.
	Severity string `json:"severity"`
	// Number is the OTel severity NUMBER (1..24), which orders severities without
	// parsing their text and survives a client that spells one differently.
	Number uint8 `json:"number"`
	// Body is the log message. It is scrubbed where it enters a fact, not here, so
	// there is one scrub on one path.
	Body string `json:"body"`
}

LogBody is the body of a log signal: OTel severity, its numeric rank, and the message. The body is scrubbed at the one point it enters a fact, never here.

type MetricBody added in v1.801.350

type MetricBody struct {
	// Name is what was measured, and it is also the fact's event name for a metric.
	Name string `json:"name"`
	// Value is the sample.
	Value float64 `json:"value"`
	// Labels are the dimensions the sample is sliced by.
	Labels map[string]any `json:"labels"`
}

MetricBody is the body of a metric sample: what was measured, its value, and the labels it is sliced by.

type ModelRow

type ModelRow struct {
	// Model is the model id, e.g. zen5-coder.
	Model string `json:"model"`
	// Provider is who served it.
	Provider string `json:"provider"`
	// Requests is how many calls went to this model.
	Requests int64 `json:"requests"`
	// Tokens is prompt plus completion tokens over those calls.
	Tokens int64 `json:"tokens"`
	// SpendCents is what they cost, in cents.
	SpendCents int64 `json:"spendCents"`
	// Pct is this model's share of the window's returned spend, 0..100, one decimal.
	Pct float64 `json:"pct"`
}

ModelRow is one model's usage in the window, ranked by spend.

type Overview

type Overview struct {
	// Range is the window that was actually applied: 24h, 7d, 30d or custom.
	Range string `json:"range"`
	// Start is the window's inclusive lower bound, RFC3339 UTC.
	Start string `json:"start"`
	// End is the window's exclusive upper bound, RFC3339 UTC.
	End string `json:"end"`
	// Interval is the bucket width the window implies: hour or day.
	Interval types.Interval `json:"interval"`
	// Scope names the tenant these numbers belong to.
	Scope Scope `json:"scope"`
	// LLM is the LLM usage lens — real per-org data.
	LLM LLMOverview `json:"llm"`
	// Web is the web-traffic lens over product events.
	Web WebOverview `json:"web"`
	// Commerce is the orders/revenue lens over product events.
	Commerce CommerceOverview `json:"commerce"`
}

Overview is one window's KPIs across all three lenses — the console's landing view.

type ProductRow

type ProductRow struct {
	// ProductID is the product the order events named.
	ProductID string `json:"productId"`
	// Orders is how many order_completed events carried it.
	Orders int64 `json:"orders"`
	// Revenue is the total they carried, in the events' own currency unit.
	Revenue float64 `json:"revenue"`
	// Units is the summed quantity sold.
	Units int64 `json:"units"`
}

ProductRow is one product's commerce result in the window, ranked by revenue.

type Scope

type Scope struct {
	// Org is the IAM org slug the rows were read under: the validated principal's,
	// resolved server-side.
	Org string `json:"org"`
}

Scope names WHOSE data a lens answered with — the tenant the server resolved, so a reader can see the answer is its own org's and not a parameter it passed.

type SinkEvent

type SinkEvent struct {
	MessageID   string
	Name        string
	DistinctID  string
	AnonymousID string
	Time        time.Time
	URL         string
	Path        string
	Referrer    string
	Revenue     float64
	Currency    string
	ProductID   string
	Quantity    uint32
	Properties  map[string]any
}

SinkEvent is one accepted event handed to the downstream fan-out. It carries the resolved canonical name plus the commerce + identity fields a conversion needs; Properties is the RAW (pre-scrub) property bag the translator lifts match keys and custom data from. The tenant is the org argument to the sink, never a field here.

type SpanBody added in v1.801.350

type SpanBody struct {
	// ID is this span's own id, which a client emitting a span knows precisely.
	ID string `json:"id"`
	// Trace is the trace this span belongs to.
	Trace string `json:"trace"`
	// Parent is the enclosing span's id, empty for a root span.
	Parent string `json:"parent"`
	// Kind is the span's role — client, server, producer, consumer, internal.
	Kind string `json:"kind"`
	// Status is how the span ended: ok, error, or unset.
	Status string `json:"status"`
	// Duration is the elapsed time in nanoseconds. Unsigned because a span cannot
	// take negative time.
	Duration uint64 `json:"duration"`
}

SpanBody is the body of a span: its place in the trace and how it ended. ID and Trace are carried here as well as on the envelope because a client that sends a span knows them precisely, while a log only correlates.

type SubjectOutcome

type SubjectOutcome struct {
	Subject   string
	Exposed   bool
	Converted bool
}

SubjectOutcome is one subject's (distinct_id's) participation in an experiment window: whether it fired the Exposed (enrolled / saw the arm) event and whether it fired the Converted (metric) event. It is the per-subject grain the experiments primitive joins to a flags variant assignment to produce per-variant samples.

func Outcomes

func Outcomes(ctx context.Context, org, exposureEvent, metricEvent string, start, end time.Time) ([]SubjectOutcome, error)

Outcomes returns, for one org over [start,end), each subject's exposure + conversion for an experiment's two event names, read from event.fact. It is the measurement seam the experiments primitive composes: flags assignment joins to these outcomes by distinct_id. The plane is never created here — its DDL owner is hanzoai/o11y — so a missing table surfaces as the query's own error.

TENANT ISOLATION is the eventsWhere invariant — org is bound POSITIONALLY, never interpolated — and every event name is a BOUND parameter too, so neither a hostile org slug nor a hostile event name can escape into SQL. exposureEvent may be "" (then every returned subject is Exposed: the population is "appeared in-window"); metricEvent is required. Fails closed with a 503 when the warehouse is absent.

type Timeseries

type Timeseries struct {
	// Range is the window that was actually applied: 24h, 7d, 30d or custom.
	Range string `json:"range"`
	// Start is the window's inclusive lower bound, RFC3339 UTC.
	Start string `json:"start"`
	// End is the window's exclusive upper bound, RFC3339 UTC.
	End string `json:"end"`
	// Interval is the bucket width: hour or day.
	Interval types.Interval `json:"interval"`
	// Scope names the tenant these numbers belong to.
	Scope Scope `json:"scope"`
	// Series is one point per bucket, oldest first, with empty buckets zero-filled.
	Series []UsagePoint `json:"series"`
	// Source is the warehouse table the series read.
	Source string `json:"source"`
}

Timeseries is the LLM usage series over one window, gap-filled so a client charts a continuous line.

type Top

type Top struct {
	// Range is the window that was actually applied: 24h, 7d, 30d or custom.
	Range string `json:"range"`
	// Start is the window's inclusive lower bound, RFC3339 UTC.
	Start string `json:"start"`
	// End is the window's exclusive upper bound, RFC3339 UTC.
	End string `json:"end"`
	// Scope names the tenant these rankings belong to.
	Scope Scope `json:"scope"`
	// Models ranks the window's LLM models by spend — real per-org data.
	Models TopModels `json:"models"`
	// Products ranks the window's products by revenue.
	Products TopProducts `json:"products"`
	// Pages ranks the paths visitors requested, by pageviews.
	Pages Breakdown `json:"topPages"`
	// Referrers ranks the external domains visitors arrived from, by pageviews.
	Referrers Breakdown `json:"topReferrers"`
	// Sources ranks the utm_source campaigns visitors arrived on, by pageviews.
	Sources Breakdown `json:"topSources"`
}

Top is one window's five ranked lenses — the console's "what is driving this" view.

type TopModels

type TopModels struct {
	// Available is true whenever the ledger answered, including with no rows.
	Available bool `json:"available"`
	// Items is the ranked models, highest spend first.
	Items []ModelRow `json:"items"`
	// Source is the warehouse table the lens read.
	Source string `json:"source"`
}

TopModels is the models lens: the window's models ranked by spend, then requests.

type TopProducts

type TopProducts struct {
	// Available is false when the product-event table could not be read.
	Available bool `json:"available"`
	// Reason says why the lens is unavailable. Omitted when it is available.
	Reason string `json:"reason,omitempty"`
	// Items is the ranked products, highest revenue first. Empty rather than absent.
	Items []ProductRow `json:"items"`
	// Source is the warehouse table the lens read.
	Source string `json:"source"`
}

TopProducts is the products lens over product events. Honest-empty until commerce emits order events.

type UTM

type UTM struct {
	Source   string `json:"source"`
	Medium   string `json:"medium"`
	Campaign string `json:"campaign"`
	Term     string `json:"term"`
	Content  string `json:"content"`
}

UTM is the first-touch attribution the client persists and re-sends per event.

type UsagePoint added in v1.801.350

type UsagePoint struct {
	// T is the bucket's start, RFC3339 UTC, aligned to the interval.
	T string `json:"t"`
	// Requests is how many LLM calls fell in this bucket.
	Requests int64 `json:"requests"`
	// Tokens is prompt plus completion tokens over those calls.
	Tokens int64 `json:"tokens"`
	// SpendCents is what they cost, in cents.
	SpendCents int64 `json:"spendCents"`
}

UsagePoint is one bucket of the LLM usage series. Named for the value rather than for its shape: the OpenAPI schema namespace is FLAT across the whole fleet, and `SeriesPoint` is already the admin launch board's {t, value} pair — one name with two shapes is what the fleet weave refuses, since every generated SDK would bind whichever it read last.

type WebOverview

type WebOverview struct {
	// Available is false when the product-event table could not be read — the lens is
	// reported missing rather than as zeros that look like real traffic.
	Available bool `json:"available"`
	// Reason says why the lens is unavailable. Omitted when it is available.
	Reason string `json:"reason,omitempty"`
	// Pageviews is how many $pageview events landed in the window.
	Pageviews int64 `json:"pageviews"`
	// Visitors is how many distinct people those pageviews came from.
	Visitors int64 `json:"visitors"`
	// Sessions is how many distinct visits they span.
	Sessions int64 `json:"sessions"`
	// Source is the warehouse table the lens read.
	Source string `json:"source"`
}

WebOverview is the web lens over event.fact. Honest-empty (Available=false) until the collector emits web events.

Jump to

Keyboard shortcuts

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