Documentation
¶
Overview ¶
Package telemetry implements codeaf's anonymous usage counting exactly as docs/TELEMETRY.md spells it: four typed events, an allowlisted property set, a local spool, and one deadline-bounded flush. It is a library only — nothing in cmd/codeaf, internal/session or internal/config reads it yet; the wiring is a later job.
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 BucketCost(costUSD float64) string
- func BucketCount(count int) string
- func BucketDuration(d time.Duration) string
- func CommonPropNames() []string
- func Configure(configTelemetryOff bool)
- func CountModelCall(ok bool, costUSD float64)
- func CountToolCall(ok bool)
- func CountTurn()
- func EnableForTest(t testing.TB, on bool)
- func Enabled() bool
- func Endpoint() string
- func Fingerprint(stack []byte) string
- func FingerprintHere() string
- func FirstRunPending() bool
- func Flush(ctx context.Context) error
- func InstallIDHash() string
- func MarkFirstRunSent()
- func MarkNoticeShown()
- func NoticeShown() bool
- func OffReason() string
- func PrintNotice()
- func ResetCountersForTest(t testing.TB)
- func Show() string
- func Spool(event Event)
- func SpoolContents() []json.RawMessage
- func SpoolSync(event Event) error
- func ValidStopReason(s string) bool
- func WriteInstallMethod(method string)
- type Event
- type Fault
- type Mode
- type SessionStats
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 ( 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 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 Notice = `` /* 362-byte string literal not displayed */
Notice is the exact text the contract fixes for the one line codeaf prints to stderr before the first session's events are ever sent, and that the installer prints. It is a constant, byte for byte, and a test holds the bytes.
Variables ¶
This section is empty.
Functions ¶
func AllowlistedEvents ¶
func AllowlistedEvents() []string
AllowlistedEvents names the four 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 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 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 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 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 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 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 MarkNoticeShown ¶
func MarkNoticeShown()
MarkNoticeShown records that the person has seen the notice, which opens the gate on flushing. A failure is silent for the same reason every other write here is: the worst case is a notice seen twice.
func NoticeShown ¶
func NoticeShown() bool
NoticeShown reports whether the notice has been marked shown. Until it has, events may be spooled but are never flushed.
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 PrintNotice ¶
func PrintNotice()
PrintNotice writes the notice to stderr once per process, the shape the surface job calls before the first session's events are sent. It never touches the on-disk marker — that is MarkNoticeShown's job, and the two are separate so a notice printed by the installer does not stand in for the surface having shown it.
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 Show ¶
func Show() string
Show returns the spool as pretty JSON — the exact answer a future `codeaf telemetry show` prints, so a person can read everything that has not left yet.
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 not for the product's hot path.
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 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 (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 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.