telemetry

package
v0.0.3 Latest Latest
Warning

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

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

Documentation

Overview

Package telemetry records how jade's tools are actually used.

Why this exists. jade's design bet is that giving a coding agent a wide enough tool surface keeps it inside jade, where edits are revision-tracked, validated and guarded — and that every gap sends the agent to bash, where none of that applies. That bet has been evaluated so far by the agent hand-writing docs/feedback.md from recollection, which is exactly the unreliable instrument the tooling was supposed to replace. This measures it instead.

The question it is built to answer is narrower than "usage stats": which tool errors plausibly send a caller back to the shell. An ambiguous anchor, a symbol that was not found, a stale revision, a timeout — each is a moment where the jade path failed and bash was one keystroke away. Those are classified and counted separately from ordinary usage for that reason.

What is deliberately not recorded

No tool arguments, no response bodies, and no error message text. Only the tool name, wall time, response size, and a failure *class*.

Arguments carry source code, file paths and search queries; an error message routinely quotes the source line it failed on. A telemetry file containing those is a copy of the repository by another name — it could not be attached to a bug report, shared with the jade authors, or committed, which would defeat the entire purpose of collecting it. Keeping it content-free is what makes it shareable, and shareable is the point.

Index

Constants

View Source
const (
	Dir  = ".jade"
	File = "telemetry.jsonl"
)

Dir and File name the log's location. It shares .jade/ with the command registry, so a repo has exactly one jade-owned directory.

View Source
const StateDirEnv = "JADE_STATE_DIR"

StateDirEnv names the environment variable that moves Jade's telemetry out of the workspace.

Variables

View Source
var ErrDisabled = errors.New("telemetry is disabled (JADE_TELEMETRY=0)")

ErrDisabled reports that telemetry is switched off, so a caller asking for a summary gets an explanation rather than a confusing empty one.

View Source
var RelPath = filepath.Join(Dir, File)

RelPath is the log's path relative to the workspace root.

Functions

func SummaryLine

func SummaryLine(s Summary) string

SummaryLine renders the one-line headline used in responses.

Types

type FailureCount

type FailureCount struct {
	Outcome Outcome
	Count   int
}

FailureCount is one failure class and how often it occurred.

type Outcome

type Outcome string

Outcome classifies how a tool call ended.

const (
	// OK is a call that returned without error.
	OK Outcome = "ok"

	// NotFound: a symbol, file or anchor the caller named does not exist.
	NotFound Outcome = "not_found"
	// Ambiguous: an anchor or name matched more than once, so jade refused
	// rather than guessing.
	Ambiguous Outcome = "ambiguous"
	// StaleRevision: the workspace moved under a preconditioned edit.
	StaleRevision Outcome = "stale_revision"
	// Timeout: a job or command exceeded its bound.
	Timeout Outcome = "timeout"
	// InvalidInput: the request was malformed or missing a required field.
	InvalidInput Outcome = "invalid_input"
	// Unavailable: an external dependency (gopls, a formatter, git) is
	// missing or failed. Distinct from the rest because the fix is
	// installation, not usage.
	Unavailable Outcome = "unavailable"
	// Other: an error that did not match any known class. A rising count here
	// means the classifier needs a new case, so it is worth watching.
	Other Outcome = "other"
)

func Classify

func Classify(err error) Outcome

Classify maps an error to a failure class.

It matches on message text because jade's errors cross package boundaries as wrapped strings rather than as a single sentinel hierarchy. That is fragile by nature, which is why Other exists as an explicit bucket: a rising Other count is the signal that this function has fallen behind the errors it is classifying, and is more useful than a misclassification that looks like a real trend.

func ClassifyResponse

func ClassifyResponse(value interface{}) (Outcome, bool)

ClassifyResponse inspects a typed response for a failure reported *in band* — as a field on a successful response rather than as an error.

Some jade tools report failure this way by design. read_symbol resolves a name to "exact", "ambiguous" or "not_found" and returns the candidates alongside, which is more useful than an error that throws them away. But the call still failed from the caller's point of view, and counting it as a success understates precisely the number this package exists to measure. Found when the first telemetry tests recorded a not_found read_symbol as OK.

The bool reports whether an in-band outcome was found at all, so a caller can tell "this response says it failed" from "this response has nothing to say about it".

What is deliberately not treated as a failure

A check, test run or declared command that reports Passed=false is a *verdict*, not a jade failure: the build is broken, and jade did its job by saying so clearly. Counting it here would swamp the fallback signal with ordinary red builds and make the number meaningless.

Likewise a search or grep returning zero matches. "Nothing matches" is a correct and often expected answer; treating every empty result as a failure would punish the tools for being asked honest questions.

type Record

type Record struct {
	Time    time.Time `json:"t"`
	Tool    string    `json:"tool"`
	Millis  int64     `json:"ms"`
	Bytes   int       `json:"bytes"`
	Outcome Outcome   `json:"outcome"`
}

Record is one tool call. Field names are short because this is written once per call and read in bulk.

type Recorder

type Recorder struct {
	// contains filtered or unexported fields
}

Recorder appends tool-call records to the workspace log.

Every method is safe to call concurrently and none of them can fail a tool call. A telemetry write that errors is dropped silently: losing a measurement is a small cost, while failing a working edit because the measurement could not be written would be an absurd trade — and would make jade less reliable than the bash it is competing with.

func New

func New(root string) *Recorder

New builds a Recorder for the workspace at root.

Setting JADE_TELEMETRY=0 disables recording entirely. An off switch is not optional for something that writes a file into the user's repository on every call.

Setting JADE_STATE_DIR puts the log under that directory instead of the workspace, in a subdirectory per workspace so several can share one state directory without mixing their measurements. This is the setting for a harness that roots Jade at a worktree it later commits wholesale.

func (*Recorder) DisplayPath added in v0.0.3

func (r *Recorder) DisplayPath() string

DisplayPath is where the log lives, as a caller should be told it: relative when it is inside the workspace, absolute when it is not.

func (*Recorder) Read

func (r *Recorder) Read() ([]Record, error)

Read returns every record in the log, oldest first. A malformed line is skipped rather than failing the read: a partially written record from a killed process should not make the whole history unreadable.

func (*Recorder) Record

func (r *Recorder) Record(tool string, duration time.Duration, bytes int, err error)

Record appends one call. err may be nil.

func (*Recorder) RecordOutcome

func (r *Recorder) RecordOutcome(tool string, duration time.Duration, bytes int, outcome Outcome)

RecordOutcome appends one call with an already-decided outcome, for callers that know more than the error does — notably a response that reports its own failure in band. See ClassifyResponse.

func (*Recorder) RecordRejected added in v0.0.3

func (r *Recorder) RecordRejected(tool string, outcome Outcome)

RecordRejected records a call Jade refused before running anything — an unknown tool name — but only into a log that already exists.

An unknown name is still a gap signal worth keeping: the agent expected a capability Jade lacks. But creating state in a workspace for a call that did nothing is exactly the stray file a harness then commits as part of someone else's change, so a rejected call never creates the log itself.

func (*Recorder) Reset

func (r *Recorder) Reset() error

Reset clears the log. Exposed so a measurement run can start from a known state rather than requiring the caller to know where the file lives.

func (*Recorder) Summarize

func (r *Recorder) Summarize() (Summary, error)

Summarize aggregates the whole log. Tools are ordered by call count descending, then by name, so the ranking leads and ties stay stable across calls rather than following map iteration order.

type Summary

type Summary struct {
	Tools []ToolStats
	// Fallbacks counts failure classes across all tools — the headline
	// number, since each is a moment the jade path failed and the shell was
	// available.
	Fallbacks   []FailureCount
	TotalCalls  int
	TotalErrors int
	TotalBytes  int64
	Path        string
	Since       time.Time
}

Summary aggregates the log.

type ToolStats

type ToolStats struct {
	Tool     string
	Calls    int
	Errors   int
	Bytes    int64
	TotalMS  int64
	MaxMS    int64
	Failures []FailureCount
}

ToolStats aggregates one tool's calls.

Jump to

Keyboard shortcuts

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