telemetry

package
v0.3.1-rc.1 Latest Latest
Warning

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

Go to latest
Published: Sep 22, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

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

View Source
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.

View Source
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.

View Source
const (
	DurationUnder1m = "<1m"
	Duration1To5m   = "1-5m"
	Duration5To30m  = "5-30m"
	Duration30mTo2h = "30m-2h"
	Duration2hPlus  = "2h+"
)

Duration buckets for a session, from the contract.

View Source
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.

View Source
const (
	ScopeMain      = "main"
	ScopeGoroutine = "goroutine"
	ScopeSurface   = "surface"
)

Scope is where a fault happened.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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

func AllowlistedProps(eventName string) []string

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

func BucketCost(costUSD float64) string

BucketCost folds a dollar figure into the contract's cost band. Negative costs mean no cost: a refund is not a price.

func BucketCount

func BucketCount(count int) string

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

func BucketDuration(d time.Duration) string

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

func CountModelCall(ok bool, costUSD float64)

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

func EnableForTest(t testing.TB, on bool)

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

func EventPropNames(event string) []string

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

func ExampleProp(event, prop string) string

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

func Fingerprint(stack []byte) string

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

func Flush(ctx context.Context) error

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

func InstallIDHashIfMinted() (string, bool)

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 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 PropDoc added in v0.4.0

func PropDoc(event, prop string) string

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

func ResetCountersForTest(t testing.TB)

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

func SpoolSync(event Event) error

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 StopReasons added in v0.4.0

func StopReasons() []string

StopReasons lists every stop reason, in the contract's order.

func ValidStopReason

func ValidStopReason(s string) bool

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

func VersionForTest(t testing.TB, version string)

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

func FaultEvent(details Fault, sessionID string, now time.Time) Event

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

func FirstRun(now time.Time) Event

FirstRun builds the once-per-install event. It carries no session hash, by contract: an install has no run yet.

func SessionEnded

func SessionEnded(mode Mode, stats SessionStats, sessionID string, now time.Time) Event

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

func SessionStarted(mode Mode, resumed bool, sessionID string, now time.Time) Event

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

func (e Event) MarshalJSON() ([]byte, error)

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.

const (
	ModeChat Mode = "chat"
	ModeTask Mode = "task"
)

The contract's two session modes.

type PropValue added in v0.4.0

type PropValue struct {
	Name  string
	Value string
}

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.

Jump to

Keyboard shortcuts

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