Documentation
¶
Overview ¶
GET /v1/usage/activity — the per-day contribution series (GitHub-style heatmap + timeline) for ONE authorized subject.
subject=user the caller's OWN activity (default); another user only for an org
admin / SuperAdmin, and only a user WITHIN the caller's org
subject=org the caller's OWN org (default); another org only for a SuperAdmin
subject=project honest-empty — per-project attribution is not in the usage ledger
yet (documented gap; lights up when cloud_usage gains a project
column, a one-line projection change)
from,to optional day range (default 90d; clamped to 366d)
Authorization is resolved SERVER-SIDE from the validated principal; a caller can never widen scope past what they're entitled to. org is always the leading bound predicate in the query.
POST /v1/usage/rollup/backfill — the DEPLOY-GATED, run-ONCE seed of the derived rollup from pre-MV ledger history. SuperAdmin only.
The incremental MV captures rows inserted AFTER its creation; this seeds everything before. Because SummingMergeTree accumulates, a second unguarded run would double a day — so it refuses when the rollup is already non-empty unless ?force=true. Pass ?before=<RFC3339> to bound the seed (default now); use the MV-creation instant so the seed and the live MV never overlap.
GET /v1/usage/leaderboard — the ranked board.
scope=personal caller's rank + the top users of the caller's OWN org, identities
anonymized except self + opted-in peers (the "as a participant" view)
scope=org the caller's org board; identities NAMED only for an org admin /
SuperAdmin (they may see their org's members), else anonymized
scope=global the top ORGS; every org NAMED for a SuperAdmin, else only the orgs
that opted into the public board — plus the caller's OWN org rank
metric=tokens|requests|cost period=day|week|month|all limit (clamped 1..100)
Tenant isolation: org is principal.Org (validated), bound positionally in every query. Cross-org cost is SuperAdmin-only. Datastore down → honest-empty.
Package leaderboard mounts the Hanzo Cloud GAMIFIED usage analytics surface: AI usage leaderboards (top users / orgs) + a GitHub-style per-day contribution graph, over the datastore OLAP rollup (#43). It is a DERIVED, read-only lens over the ONE usage ledger (hanzo.cloud_usage) — it adds no metering path and double-counts nothing.
Surface (all /v1, NO /api/ prefix; org-scoped, fail-closed):
GET /v1/usage/leaderboard ranked top users (personal|org) or orgs (global) GET /v1/usage/activity per-day series for a heatmap + timeline (authorized subject) GET /v1/usage/leaderboard/optin the caller's opt-in + their org's opt-in PUT /v1/usage/leaderboard/optin set the caller's OWN public-listing opt-in PUT /v1/usage/leaderboard/optin/org set the ORG's public-board opt-in (org admin) POST /v1/usage/rollup/backfill seed the rollup from ledger history (SuperAdmin, once)
It co-owns the /v1/usage/* prefix with clients/usage (the cost footprint at /v1/usage/summary) — a DISTINCT concern (who leads + your activity graph) at its own paths, registered as a separate subsystem so it stays isolated. Its auto health route is /v1/leaderboard/health (the spec name).
TENANT ISOLATION (the bar). The org is the VALIDATED IAM owner claim (principal.Org — the trusted X-Org-Id the identity middleware minted from the verified bearer, HIP-0026; NEVER a client header) AND a validated principal is required. Every datastore read binds the org POSITIONALLY (never interpolated). A user board only ever contains the caller's own org's rows; an org board carries org-level aggregates only; cross-org detail is structurally impossible. Fail closed: no principal → 401; datastore not connected → honest-empty (available:false), never fabricated ranks.
PRIVACY (opt-in). Public listing is OPT-IN and PRIVATE by default: a user sees their OWN rank always, but is shown to others only after opting in with a chosen handle; an org appears on the cross-org global board only after an org admin opts it in. See view.go for the naming/anonymization policy.
The opt-in surface — PUBLIC-LISTING IS OPT-IN, PRIVATE BY DEFAULT.
GET /v1/usage/leaderboard/optin the caller's own opt-in + their org's opt-in PUT /v1/usage/leaderboard/optin set the caller's OWN listing (self only) PUT /v1/usage/leaderboard/optin/org set the ORG's public-board listing (org admin)
A user writes ONLY their own preference (keyed by their validated ledger id); an org preference is writable only by an admin OF that org. Nothing here is secret.
The datastore rollup (#43): a DERIVED per-day pre-aggregation of hanzo.cloud_usage that makes leaderboard + activity reads cheap. It is NOT a second metering path — it is a rollup of the ONE ledger, so it can never double-count what cloud_usage already records.
ENGINE CHOICE — SummingMergeTree, not AggregatingMergeTree. Every rollup metric is a pure SUM (requests, tokens, cost); distinct-user / distinct-model counts fall out of the (org,user,model,day) grain at read time. SummingMergeTree is the simplest engine that does exactly "collapse rows with the same sort key by summing the rest", stores plain UInt64 (read with a normal sum(), no -Merge), and is the established house pattern (commerce.daily_sales_mv). AggregatingMergeTree would only earn its keep for non-sum aggregate states (uniq/quantile) — we have none.
INCREMENTAL MV. rollupMV is attached to hanzo.cloud_usage: every INSERT into the ledger fires it, pre-aggregating THAT block into the rollup. The MV SELECT is pure and TYPE-EXACT (toDate→Date, sum(UInt32)→UInt64, count()→UInt64, all matching the target columns), so it cannot fail on a valid ledger row — it never endangers the (fire-and-forget) metering write. It captures rows inserted AFTER its creation; pre-existing history is seeded ONCE by the deploy-gated backfill.
Pure, I/O-free core of the usage leaderboard + activity reads: the metric allowlist, the period→day-window resolver, the day literal, and the datastore SQL BUILDERS. Everything here is a pure function so the tests drive it with plain values — no datastore, no HTTP.
INJECTION SAFETY (the bar). The tenant key (organization) and every user- supplied value (subject id, day bounds, self-rank threshold) is ALWAYS a bound `?` parameter, appended to the args slice — NEVER string-interpolated into the SQL. The only tokens interpolated are (a) the metric column, taken from the closed `metricColumn` allowlist (a caller's `metric=` can only ever select one of three fixed column names, or be rejected), and (b) the LIMIT, a server- clamped int. This mirrors the proven house pattern (ai/object cloud_usage.go whereClause, clients/analytics query.go llmWhere): org bound positionally, the bucket/limit a closed enum / validated int. The builders return (sql, args) so a test can assert a hostile org slug or metric lands in args (or is rejected), never in the SQL string.
The opt-in preference store: the durable, OPT-IN-BY-DEFAULT-PRIVATE half of the leaderboard. Two tiny tenant-keyed tables in one Hanzo Base/SQLite file — the same eval/settings discipline (cek.Open, MaxOpenConns(1) to serialize writes against the file lock, a mandatory key predicate on every statement).
- user_optin (user_id PK, org, handle, listed): a user opts THEMSELVES into public listing with a chosen handle. Default absent ⇒ NOT listed (private): the user still sees their own rank, but is never named to others.
- org_optin (org PK, display, listed): an org admin opts the ORG into the public global board with a display name. Default absent ⇒ private.
Isolation: a user row is written only for the CALLER's own user_id; the listing read is org-scoped (`WHERE org=? AND listed=1`). An org row is written only by an admin of that org; the global-listing read is `WHERE listed=1` (org-level display only — no user data). No secret ever lands here.
Response shapes + the PURE naming / anonymization / value-coercion functions. I/O-free so the privacy policy is unit-tested directly with plain values.
PRIVACY MODEL (the product rule).
- A leaderboard row carries an AGGREGATE metric + a display identity. The identity is revealed only when the viewer is authorized to see it:
- self → always ("you")
- opted-in peer → the handle THEY chose (opt-in store)
- admin viewer → the member's username (org admin sees org members; SuperAdmin sees anyone) — the name half of user_id, no IAM round-trip, no cross-service call
- everyone else → "Anonymous" (identity withheld; metric still shown)
- Cross-ORG isolation is enforced upstream by the org-bound SQL: a user board only ever contains the caller's own org's rows; an org board carries only org-level aggregates (never a user identity). So a row can never carry ANOTHER tenant's user detail.
Index ¶
- func BackfillUsageRollup(ctx context.Context, before time.Time) error
- func EnsureUsageRollup(ctx context.Context) error
- func Mount(app cloud.Router, deps cloud.Deps) error
- func Shutdown(_ context.Context) error
- type ActivityPoint
- type ActivityTotals
- type ActivityView
- type LeaderboardRow
- type LeaderboardView
- type SelfRank
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BackfillUsageRollup ¶
BackfillUsageRollup seeds the rollup from ledger history with timestamp < before. It is the DEPLOY-GATED, run-ONCE step: because SummingMergeTree accumulates, a second unguarded run would double a day, so the handler guards on an empty rollup (or an explicit force). `before` should be the MV-creation instant (or now) so the seed and the live MV do not overlap.
func EnsureUsageRollup ¶
EnsureUsageRollup creates the derived rollup table + the incremental MV if they do not exist (the base ledger first, since the MV reads it). Idempotent and latched: a transient datastore blip does not permanently poison later attempts. Every leaderboard/activity read calls it first — the same discipline as EnsureCloudUsageTable — so the feature self-provisions its rollup the first time it runs against a connected datastore.
Types ¶
type ActivityPoint ¶
type ActivityPoint struct {
Day string `json:"day"` // "2006-01-02"
Requests int64 `json:"requests"`
Tokens int64 `json:"tokens"`
CostCents int64 `json:"costCents"`
}
ActivityPoint is one day of a subject's usage — the atom of the contribution heatmap + timeline. CostCents populated only when the viewer may see spend.
type ActivityTotals ¶
type ActivityTotals struct {
Requests int64 `json:"requests"`
Tokens int64 `json:"tokens"`
CostCents int64 `json:"costCents"`
ActiveDays int `json:"activeDays"` // days with any usage
MaxTokens int64 `json:"maxTokens"` // busiest day's tokens (heatmap intensity ceiling)
MaxRequests int64 `json:"maxRequests"`
}
ActivityTotals are the window sums + heatmap-scaling hints.
type ActivityView ¶
type ActivityView struct {
Subject string `json:"subject"` // user|org|project
ID string `json:"id"` // resolved subject id (echoed)
From string `json:"from"`
To string `json:"to"`
Days []ActivityPoint `json:"days"`
Totals ActivityTotals `json:"totals"`
Available bool `json:"available"`
Source string `json:"source"`
Note string `json:"note,omitempty"` // honest note (e.g. project attribution not in the ledger)
}
ActivityView is the per-day series for one authorized subject.
type LeaderboardRow ¶
type LeaderboardRow struct {
Rank int `json:"rank"`
Handle string `json:"handle"`
Anonymous bool `json:"anonymous"`
Self bool `json:"self"`
Requests int64 `json:"requests"`
Tokens int64 `json:"tokens"`
CostCents int64 `json:"costCents"`
Metric int64 `json:"metric"`
}
LeaderboardRow is one ranked subject (a user or an org). Handle is the display identity per the privacy model; Anonymous marks a withheld identity; Self marks the caller's own row. Requests/Tokens are non-sensitive volume aggregates; CostCents is populated ONLY when the viewer is authorized to see this subject's spend (self, admin, or an explicit cost board — see costVisible). Metric is the value the board is ranked by (for bar sizing on the client).
type LeaderboardView ¶
type LeaderboardView struct {
Scope string `json:"scope"` // personal|org|global
Subject string `json:"subject"` // user|org
Metric string `json:"metric"` // tokens|requests|cost
Period string `json:"period"` // day|week|month|all|custom
Start string `json:"start"` // "" for all
End string `json:"end"`
Rows []LeaderboardRow `json:"rows"`
Self *SelfRank `json:"self,omitempty"`
Total int64 `json:"total"` // ranked subjects in the window
Available bool `json:"available"`
Source string `json:"source"`
}
LeaderboardView is the whole leaderboard response.
type SelfRank ¶
type SelfRank struct {
Ranked bool `json:"ranked"`
Rank int `json:"rank"`
OfTotal int64 `json:"ofTotal"`
Requests int64 `json:"requests"`
Tokens int64 `json:"tokens"`
CostCents int64 `json:"costCents"`
Metric int64 `json:"metric"`
Handle string `json:"handle"`
Listed bool `json:"listed"` // is the caller publicly listed (opted in)
}
SelfRank is the caller's own standing on a user board, INCLUDED even when the caller falls outside the top-N page. Ranked=false means the caller has no usage in the window (unranked) — the client shows "—", never a fabricated rank.