Documentation
¶
Overview ¶
Package telemetry implements codeaf's anonymous usage counting exactly as docs/TELEMETRY.md spells it: five typed events, an allowlisted property set, a local spool, periodic delivery while a session is open, and one final deadline-bounded flush. cmd/codeaf owns the process lifecycle wiring.
The law of the package is docs/TELEMETRY.md. When this file and the doc disagree, the doc wins and this package is the defect.
Index ¶
- Constants
- func AllowlistedEvents() []string
- func AllowlistedProps(eventName string) []string
- func BeginUsageSession(mode Mode, sessionID string) func()
- func BucketCost(costUSD float64) string
- func BucketCount(count int) string
- func BucketDuration(d time.Duration) string
- func CaptureUsageRecorder(dimensions UsageDimensions) func(int, int)
- func CommonPropNames() []string
- func Configure(configTelemetryOff bool)
- func CountModelCall(ok bool, costUSD float64)
- func CountTokens(input, output int)
- func CountToolCall(ok bool)
- func CountTurn()
- func CountUsage(input, output int, dimensions UsageDimensions)
- func EnableForTest(t testing.TB, on bool)
- func Enabled() bool
- func Endpoint() string
- func EventPropNames(event string) []string
- func ExampleProp(event, prop string) string
- func Fingerprint(stack []byte) string
- func FingerprintHere() string
- func FirstRunPending() bool
- func Flush(ctx context.Context) error
- func InstallIDHash() string
- func InstallIDHashIfMinted() (string, bool)
- func MarkFirstRunSent()
- func ModelFamily(model string) string
- func OffReason() string
- func PropDoc(event, prop string) string
- func ResetCountersForTest(t testing.TB)
- func RoutingProvider(endpoint string) string
- func Show() string
- func Spool(event Event)
- func SpoolContents() []json.RawMessage
- func SpoolSync(event Event) error
- func StartPeriodicFlush(interval time.Duration) func()
- func StopReasons() []string
- func ValidStopReason(s string) bool
- func VersionForTest(t testing.TB, version string)
- func WriteInstallMethod(method string)
- type Event
- func FaultEvent(details Fault, sessionID string, now time.Time) Event
- func FirstRun(now time.Time) Event
- func SessionEnded(mode Mode, stats SessionStats, sessionID string, now time.Time) Event
- func SessionStarted(mode Mode, resumed bool, sessionID string, now time.Time) Event
- func UsageDelta(mode Mode, input, output int, sessionID string, now time.Time) Event
- func UsageReceipt(mode Mode, input, output int, sessionID string, now time.Time, ...) Event
- type Fault
- type Mode
- type PropValue
- type SessionStats
- type UsageDimensions
Constants ¶
const ( BucketZero = "0" BucketOne = "1" BucketTwo5 = "2-5" BucketSix = "6-20" BucketTwo1 = "21-100" Bucket100 = "100+" )
Count buckets, exactly the strings the contract enumerates. A bucket is a band a person agreed to, not a number they did not.
const ( CostZero = "0" CostUnder1c = "<0.01" Cost1cTo10c = "0.01-0.1" Cost10cTo1 = "0.1-1" Cost1To10 = "1-10" Cost10Plus = "10+" )
Cost buckets in US dollars, from the contract.
const ( DurationUnder1m = "<1m" Duration1To5m = "1-5m" Duration5To30m = "5-30m" Duration30mTo2h = "30m-2h" Duration2hPlus = "2h+" )
Duration buckets for a session, from the contract.
const ( StopDone = "done" StopError = "error" StopIncomplete = "incomplete" StopBudget = "budget" StopTurnCap = "turn-cap" StopDeadline = "deadline" StopPrice = "price" StopQuestion = "question" StopInterrupted = "interrupted" StopUnknown = "unknown" )
StopReason enumerates why a session ended, as the contract spells them.
const ( ScopeMain = "main" ScopeGoroutine = "goroutine" ScopeSurface = "surface" )
Scope is where a fault happened.
const ( InstallHashDoc = "sha256 of a random id kept on this machine; the id itself never leaves" SessionHashDoc = "sha256 of the run id, one per session; absent on first_run" EventIDDoc = "16 random bytes as hex, one per event" EventTimeDoc = "when the event happened, UTC, to the second" )
The identity and envelope fields every event carries beside its props, and what each is, for a listing that shows a person the whole row.
const ( CountBandsDoc = "count bands are 0, 1, 2-5, 6-20, 21-100 and 100+" CostBandsDoc = "dollar bands are 0, under 0.01, 0.01-0.1, 0.1-1, 1-10 and 10+" )
CountBandsDoc and CostBandsDoc spell the bands, as the doc spells them.
const ( MaxSpoolLines = 1000 MaxEventAge = 7 * 24 * time.Hour MaxEventsPerPOST = 50 MaxSpoolReadBytes = 1 << 20 )
Spool limits, from the contract: a thousand lines of history, seven days of age, fifty events on the wire per POST, one megabyte read into memory at most.
const ( OnReason = "" OffEnv = "CODEAF_TELEMETRY=off" OffDoNotTrack = "DO_NOT_TRACK is set" OffConfig = "config telemetry = off" OffEmptyEndpoint = "CODEAF_TELEMETRY_ENDPOINT is empty" OffBuild = "dirty or unstamped build" OffTest = "running under go test" )
OptOut enumerates the reasons the ladder can answer with. The empty value means telemetry would run.
const DefaultEndpoint = "https://agentfield.ai/api/oss/codeaf/telemetry"
DefaultEndpoint is the relay the contract names. CODEAF_TELEMETRY_ENDPOINT moves it; set to empty it turns telemetry off entirely.
const EveryEvent = "every event"
EveryEvent is the key under which PropDoc answers for the six props every event carries, and the exact words the doc's table spells in its first column for them.
const FirstRunMarkerName = "first_run"
FirstRunMarkerName is the file that records that first_run was sent for the current install id. It is exported for the status command the wiring job adds.
const PeriodicFlushInterval = 30 * time.Second
PeriodicFlushInterval is how long a running session may leave a completed usage delta waiting locally. It mirrors the ordinary server-side analytics queue cadence while keeping network work entirely off the model-call path.
Variables ¶
This section is empty.
Functions ¶
func AllowlistedEvents ¶
func AllowlistedEvents() []string
AllowlistedEvents names the five events the contract defines, in a stable order for the doc.
func AllowlistedProps ¶
AllowlistedProps answers which key an event name accepts. It backs the doc drift test and the privacy law: the table above is the allowlist, this is its only reader outside this file's own tests.
func BeginUsageSession ¶
BeginUsageSession gives the provider accounting door the session identity needed for usage_delta rows. Its returned function closes only this session, waits for its disk appends, and is safe to call more than once.
func BucketCost ¶
BucketCost folds a dollar figure into the contract's cost band. Negative costs mean no cost: a refund is not a price.
func BucketCount ¶
BucketCount folds a number into the contract's count band. Negative and unknown counts are nothing; there is no band a fault can inflate into.
func BucketDuration ¶
BucketDuration folds a session length into the contract's duration bands. A negative duration is not a length; it is a clock that disagrees with itself, and it answers as the empty band.
func CaptureUsageRecorder ¶
func CaptureUsageRecorder(dimensions UsageDimensions) func(int, int)
CaptureUsageRecorder binds a later provider receipt to the original session. A receipt worker may finish after that session closes or a new one opens; looking up the active session then would attribute its tokens to the wrong work. This callback records at most once and still honors current opt-out.
func CommonPropNames ¶
func CommonPropNames() []string
CommonPropNames names the six props every event carries, in contract order.
func Configure ¶
func Configure(configTelemetryOff bool)
Configure stores the config's telemetry answer once: true means the config turns telemetry off. It is the caller's side of the opt-out ladder, and it is what makes the package impossible to misuse — Spool, SpoolSync and Flush are no-ops unless the full ladder answers on.
func CountModelCall ¶
CountModelCall records one model call answered: ok false also counts the failure, because model_calls_failed is a field the constructor needs and a second call at the chokepoint would double-count the call itself. costUSD is folded into the integer tally; a call with no cost figure hands over 0 and adds nothing, which is the record's own answer.
func CountTokens ¶
func CountTokens(input, output int)
CountTokens records provider-reported input and output tokens for one completed call. Cache reads are already part of input; adding them again would overcount. Unlike the in-memory session counters above, token usage is useful while a long session is still running, so it becomes a queued delta event when telemetry is on and a session identity has been configured.
func CountToolCall ¶
func CountToolCall(ok bool)
CountToolCall records one tool call answered, with its own failure count for the same reason CountModelCall keeps one.
func CountTurn ¶
func CountTurn()
CountTurn records one sealed turn. It is one atomic add and nothing else: no allocation, no ladder read, no lock.
func CountUsage ¶
func CountUsage(input, output int, dimensions UsageDimensions)
CountUsage records one provider receipt, including an explicitly missing receipt. Its dimensions are normalized before any value reaches the spool.
func EnableForTest ¶
EnableForTest turns the ladder on (on=true) or back to its honest answer (on=false) for the duration of one test, restoring the previous value when the test ends. It is the only way past the go-test and unstamped-build rungs of [offReason].
It is deliberately exported and deliberately in the testhook file rather than a _test.go one, because the lifecycle tests in cmd/codeaf need it from their own package — and it is still a test-only surface by construction: nothing in the production call graph reads it, and a caller that is not a test has nothing to restore it with.
func Enabled ¶
func Enabled() bool
Enabled reports whether telemetry would run, reading the config answer stored by Configure. Nothing is spooled or sent when this answers false.
func Endpoint ¶
func Endpoint() string
Endpoint names where events go: the contract relay unless the override says otherwise.
func EventPropNames ¶ added in v0.4.0
EventPropNames answers the props an event adds beyond the every-event six, in the doc's order, so a listing reads the way the doc reads.
func ExampleProp ¶ added in v0.4.0
ExampleProp answers the example value for one event prop, or "" for a prop the table does not hold; a test holds the table to the allowlist.
func Fingerprint ¶
Fingerprint reduces a panic stack to 16 lowercase hex characters: the first eight bytes of sha256 over the function names of the top five stack frames that start with github.com/Agent-Field/codeaf/.
Function names ONLY. No file paths, no line numbers, no panic value, ever — a path is a name of the person's machine and a panic message is often their words. A stack with no codeaf frames at all hashes the empty name list, so a fault inside a dependency still groups identically for everyone rather than quietly becoming a fingerprint of their directory layout.
func FingerprintHere ¶
func FingerprintHere() string
FingerprintHere fingerprints the calling goroutine's own stack, the shape a deferred recover has on hand.
func FirstRunPending ¶
func FirstRunPending() bool
FirstRunPending reports whether this install id has yet to send first_run. A marker naming a different install id — the id was deleted, the state moved — counts as pending again, because a new identity has not sent anything.
func Flush ¶
Flush sends every spooled event it can within the deadline the caller carries — callers pass a hard budget, one second at exit — POSTing up to fifty at a time to the endpoint. Lines that were sent are removed, lines older than seven days are dropped, and a failure of any kind is silent. The returned error is always nil; it exists so a future caller can log without this package ever being able to fail a run.
The spool is first renamed to a process-unique sending file, so a flush owns the lines it is sending: another process appending to spool.jsonl mid-flush cannot be deleted unsent, and this process's unsent lines are appended back rather than rewritten around the other's.
func InstallIDHash ¶
func InstallIDHash() string
InstallIDHash returns sha256 of the install id, creating the id first if this machine has never sent anything. The raw id never leaves this function and is never returned; the hash is the only identity the wire ever sees.
func InstallIDHashIfMinted ¶ added in v0.4.0
InstallIDHashIfMinted answers the install id hash when this machine has an install id already, and false when it has not: a reading verb must not mint one, because the id is written on the first SEND and a machine that has sent nothing has no identity to show.
func MarkFirstRunSent ¶
func MarkFirstRunSent()
MarkFirstRunSent writes the marker so first_run is emitted once per install id. A failure is silent: the worst case is one extra first_run after a crash, which no person ever sees.
func ModelFamily ¶
ModelFamily reduces a model slug to a public family. The exact name, variant and any operator-defined suffix are never part of the event.
func OffReason ¶
func OffReason() string
OffReason is Enabled with the why, for a status command. It returns "" when telemetry would run, otherwise one stable phrase naming the rung that switched it off.
func PropDoc ¶ added in v0.4.0
PropDoc answers what one prop is, for the event named — EveryEvent for the six every event carries — or "" for a prop the table does not hold.
func ResetCountersForTest ¶
ResetCountersForTest zeroes the process-wide session counters for one test and again when it ends, so a test that asserts exact bands is not reading what an earlier test in the same binary counted. Test-only for the same reason EnableForTest is: nothing in the production call graph reaches it.
func RoutingProvider ¶
RoutingProvider names the service receiving the request, not the vendor of the model it serves. No URL or custom hostname is returned.
func Show ¶
func Show() string
Show returns the spool as pretty JSON: everything that has not left yet. It was what `codeaf telemetry show` printed until 2026-10-01; the tests are its only readers now, and the spool itself is plain JSON lines a person can open.
func Spool ¶
func Spool(event Event)
Spool appends one event to the spool file and returns immediately. It never blocks the caller for more than a few milliseconds: it does no network work, and its one append runs on its own goroutine through guard.Go, the repo's one door for fire-and-forget work. A failure here is silent.
func SpoolContents ¶
func SpoolContents() []json.RawMessage
SpoolContents returns the spooled events as raw JSON rows, oldest first, in the order they would be sent. Show formats them for a person.
func SpoolSync ¶
SpoolSync appends one event and waits for the append, for tests and for a caller that must see the line on disk. It shares every law with Spool and is intentionally used at the completed provider receipt boundary; network sending never shares its append lock.
func StartPeriodicFlush ¶
StartPeriodicFlush sends queued events while a session stays open. It never runs network work on a model goroutine, and the returned stop waits until the loop has exited so the caller can perform one final bounded flush safely.
func StopReasons ¶ added in v0.4.0
func StopReasons() []string
StopReasons lists every stop reason, in the contract's order.
func ValidStopReason ¶
ValidStopReason reports whether s is one of the contract's stop reasons, so a constructor can drop an unrecognised one rather than invent a key.
func VersionForTest ¶ added in v0.3.0
VersionForTest makes every event built during one test carry version, and restores the build's own answer when the test ends. Test-only for the same reason EnableForTest is: a test binary has no stamped version, and the send path drops what cannot name one.
func WriteInstallMethod ¶
func WriteInstallMethod(method string)
WriteInstallMethod records how codeaf arrived on this machine, as the installer would. Only the two contract values are kept; anything else is stored as unknown so a stray word from an installer cannot become a prop.
Types ¶
type Event ¶
type Event struct {
Name string
ID string
InstallHash string
SessionHash string // empty on first_run
Time string
Props map[string]any
}
Event is one wire row. Only the contract's keys exist; the allowlist lives in how each constructor fills Props, and the doc test holds the two together. MarshalJSON writes the envelope exactly as the contract spells it.
func FaultEvent ¶
Fault builds the fault event. The fingerprint is derived from the stack here, by fingerprint.go; the panic value itself is never carried.
func FirstRun ¶
FirstRun builds the once-per-install event. It carries no session hash, by contract: an install has no run yet.
func SessionEnded ¶
SessionEnded closes one run. An unrecognised stop reason is recorded as unknown — the fact that the run ended survives, the vocabulary it ended in does not — and the exit code is clamped to the contract's 0..5.
func SessionStarted ¶
SessionStarted announces one run. mode must be chat or task; anything else collapses to chat, the mode a bare `codeaf` opens, rather than becoming a value the contract does not list.
func UsageDelta ¶
UsageDelta records provider-reported tokens as soon as a completed model call reaches the process's accounting door. A remote engine may fold several calls into one turn before this process sees them, so the event deliberately says "delta" rather than claiming every row is exactly one call. Deltas can be summed without counting the same session again at its end.
func UsageReceipt ¶
func UsageReceipt(mode Mode, input, output int, sessionID string, now time.Time, dimensions UsageDimensions) Event
UsageReceipt extends a token delta with categories that make missing usage visible without collecting raw model names or endpoint addresses.
func (Event) MarshalJSON ¶
MarshalJSON renders the event under its wire names. The struct carries the typed fields; this decides the bytes.
type Fault ¶
type Fault struct {
Mode string // chat or task; anything else is sent as other
Scope string // main, goroutine or surface
Stack []byte
}
Fault describes one recovered panic for the fault event.
type Mode ¶
type Mode string
Mode is how a session ran. The zero value is chat, which is what a bare `codeaf` opens.
type PropValue ¶ added in v0.4.0
PropValue is one every-event prop with the value this machine would send for it right now.
func CommonPropValues ¶ added in v0.4.0
func CommonPropValues() []PropValue
CommonPropValues answers the six every-event props as this binary on this machine would fill them, in contract order — the same reader every event constructor uses, so what a listing shows is what an event would carry. It reads the install marker and the build; it writes nothing.
type SessionStats ¶
type SessionStats struct {
Duration time.Duration
Turns int
ModelCalls int
ModelCallsFailed int
ToolCalls int
ToolCallsFailed int
CostUSD float64
StopReason string
ExitCode int
}
SessionStats is what a run produced. It is a struct of typed Go values so the caller cannot hand over a pre-bucketed string, a raw error, or anything else the allowlist would have to trust.
func Snapshot ¶
func Snapshot() SessionStats
Snapshot is the tally as the SessionEnded constructor takes it: the SessionStats fields it needs — turns, model calls, model calls failed, tool calls, tool calls failed, cost — named as SessionStats names them, so the wiring hands the struct straight to the constructor without adapting. It builds ONE struct and reads each atomic once; the six numbers are therefore not from a single instant, which is fine — a run's last model call can land after its last turn sealed, and a snapshot that could not be taken mid-run would be a snapshot that could not be taken at all.
The fields SessionEnded fills and Snapshot does not return — Duration, StopReason, ExitCode — belong to the run's END, which is the caller's fact: elapsed time and exit status live where the process does, and Snapshot carries only what the chokepoints counted.
type UsageDimensions ¶
type UsageDimensions struct {
RoutingProvider string
ModelFamily string
UsageStatus string
AccountingSource string
ReceiptID string
}
UsageDimensions contains only bounded diagnostic categories. ReceiptID is used locally to derive a replay-stable event identity and is never sent raw.