Documentation
¶
Overview ¶
Package analytics reports what the person's coding agents did in their registered clones, what it cost and what was delivered, for the app's Analytics page. See README.md.
Index ¶
- Constants
- Variables
- type Action
- type Actions
- type BaumWeek
- type BaumWeeks
- type BaumWork
- type Clones
- type CommitShare
- type Cost
- type Day
- type DayAgent
- type DecisionCapture
- type Delivery
- type DeliveryReport
- type Entry
- type Filters
- type FlowCounts
- type FlowDay
- type FlowReport
- type Hour
- type Index
- type Merger
- type ModelRow
- type Options
- type PlanPoint
- type PlanSeries
- type PlanUsageReport
- type Progress
- type PullRequestSearcher
- type Query
- type ReasonCount
- type Registry
- type Report
- type RepositoryShare
- type Row
- type Service
- type SessionRow
- type Stores
- type Summary
- type Updater
- type Usage
- type Window
Constants ¶
const ( ActionMerge = "merge" ActionUpdateBranch = "update_branch" ActionRerunChecks = "rerun_checks" )
The GitHub writes the app makes besides a fix, as the ledger records them.
const ( // Retention bounds the index: a session last written longer ago is not // read, and its entry is dropped. Retention = 90 * 24 * time.Hour // RefreshInterval is how often the index looks for new and changed // sessions while the daemon runs; a session ending also starts a look. RefreshInterval = 5 * time.Minute )
const (
DeliveryTTL = time.Hour
)
DeliveryTTL is how long one repository's search answer serves, and deliveryRetry how soon a failed search is tried again.
const IdleGap = 5 * time.Minute
IdleGap is the longest pause before an agent event (a model call, a reply, a tool call or its result) that still counts as the agent working: a longer one is the session waiting, for the person or left open, and adds nothing to its active time. Time before a person's input never counts.
const LongestSessions = 10
LongestSessions is how many of the longest sessions the report lists.
Variables ¶
var Agents = []string{"claude_code", "codex", "cursor"}
Agents are the agent filter's values: the vendors whose sessions the index reads.
var FilterAgents = []string{"claude_code", "codex"}
FilterAgents are the agents the report offers as filter choices: the desktop app offers Claude Code and Codex only. A Cursor session already recorded still counts in the figures and still filters when a query names it.
var Windows = []int{7, 30, 90}
Windows are the lengths, in days, a report can cover.
Functions ¶
This section is empty.
Types ¶
type Action ¶
type Action struct {
At time.Time `json:"at"`
Kind string `json:"kind"`
Repository string `json:"repository"`
PR int `json:"pr"`
}
Action is one GitHub write the person made from the app.
type Actions ¶
Actions is the small ledger of merges, branch updates and check re-runs made from the app, which the jobs list does not hold. It keeps Retention of them in the application database.
func (*Actions) Record ¶
Record notes a write that succeeded. A failure to save is not the write's failure, so it is only returned for the caller to log.
func (*Actions) RecordMerges ¶
RecordMerges records each merge m makes.
func (*Actions) RecordUpdates ¶
RecordUpdates records each branch update and check re-run u makes.
type BaumWeek ¶
type BaumWeek struct {
Reviews int `json:"reviews"`
FixesPushed int `json:"fixes_pushed"`
ThreadsResolved int `json:"threads_resolved"`
Merges *int `json:"merges"`
BranchUpdates *int `json:"branch_updates"`
CheckReruns *int `json:"check_reruns"`
Decisions *int `json:"decisions"`
}
BaumWeek is one week of what baum did for the person; a figure without its ledger is nil.
type BaumWork ¶
type BaumWork struct {
Available bool `json:"available"`
Reviews int `json:"reviews"`
GroupReviews int `json:"group_reviews"`
// ReviewsCompleted counts the reviews and group reviews that finished.
ReviewsCompleted int `json:"reviews_completed"`
Fixes int `json:"fixes"`
// Questions counts questions asked of a review's agent (jobs.KindChat).
Questions int `json:"questions"`
// DetailsRetries counts retries of a saved review's failed section
// details (jobs.KindDetails), which are not reviews; one that stood down
// counts nowhere else.
DetailsRetries int `json:"details_retries"`
Automatic int `json:"automatic"`
Completed int `json:"completed"`
Failed int `json:"failed"`
Cancelled int `json:"cancelled"`
Active int `json:"active"`
Tokens int `json:"tokens"`
Cost Cost `json:"cost"`
Failures []ReasonCount `json:"failures"`
FixesPushed int `json:"fixes_pushed"`
FixesFailed int `json:"fixes_failed"`
ThreadsResolved int `json:"threads_resolved"`
ThreadsReplied int `json:"threads_replied"`
// Merges, BranchUpdates and CheckReruns are nil without the ledger.
Merges *int `json:"merges"`
BranchUpdates *int `json:"branch_updates"`
CheckReruns *int `json:"check_reruns"`
// Decisions is decision capture's model calls, nil without its ledger.
Decisions *DecisionCapture `json:"decisions"`
// Weeks is the last seven days' figures against the seven before; nil
// in a week's own figures.
Weeks *BaumWeeks `json:"weeks,omitempty"`
}
BaumWork is what baum itself ran: reviews and fixes from the jobs ledger, and the other writes from the actions ledger.
type CommitShare ¶
type CommitShare struct {
// Available is false when no clone in scope could be read; Reasons say
// why, and also name clones left out of an available share.
// Agent counts the commits linked to an agent; each commit once, under
// its strongest evidence: a session that recorded creating it, then an
// Entire checkpoint, then a co-author trailer.
}
CommitShare is the agent-written share of the person's commits.
type Cost ¶
type Cost struct {
USD *float64 `json:"usd"`
Incomplete bool `json:"incomplete"`
Reasons []string `json:"reasons"`
}
Cost is an estimate of what the usage would cost at API prices. USD is nil when nothing was priced; Incomplete says part of the usage has no price or reported no tokens, so USD is a floor; Reasons say why, in words.
type Day ¶
type Day struct {
Date string `json:"date"`
Agents map[string]DayAgent `json:"agents"`
Models map[string]DayAgent `json:"models"`
}
Day is one local day's sessions (by when they started) and active time (by when it happened), per agent and per model. A session counts under its main model, the one with the most tokens, so either split sums to the day.
type DecisionCapture ¶
type DecisionCapture struct {
Attempts int `json:"attempts"`
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
// Abandoned calls were cut short by the daemon stopping.
Abandoned int `json:"abandoned"`
// Cost sums the known costs of settled calls; any without one makes it
// incomplete, and none known leaves it unknown.
Cost Cost `json:"cost"`
Failures []ReasonCount `json:"failures"`
}
DecisionCapture counts decision capture's calls started in the window.
type Delivery ¶
type Delivery struct {
Search PullRequestSearcher
Now func() time.Time
// Paused is when the host's GitHub rate limit lets requests resume, as
// the background refresh knows it; nil means unknown. Limited records a
// rate limit a search hit, so the refresh waits it out too.
Paused func(host string) time.Time
Limited func(host string, until time.Time)
// contains filtered or unexported fields
}
Delivery caches, per picked repository, one GitHub search for the pull requests the person opened in the last Retention. A request never waits for GitHub: it gets what is cached and queues a search for what is missing or older than DeliveryTTL. Queued searches run on at most deliveryWorkers workers, paced to stay under GitHub's search limit; a rate limit pauses the rest of the queue, whose repositories keep what they had and are tried again after the pause.
func (*Delivery) Pulls ¶
Pulls returns what is cached for each repository and queues a search for those with nothing fresh.
type DeliveryReport ¶
type DeliveryReport struct {
Status string `json:"status"`
Reasons []string `json:"reasons"`
FetchedAt *time.Time `json:"fetched_at"`
Repositories int `json:"repositories"`
// Opened and the figures below cover pull requests that are not drafts;
// Drafts counts the drafts opened in the window.
Opened int `json:"opened"`
Merged int `json:"merged"`
Drafts int `json:"drafts"`
MedianMergeSeconds *float64 `json:"median_merge_seconds"`
Reviewed int `json:"reviewed"`
MedianFirstReviewSeconds *float64 `json:"median_first_review_seconds"`
ConflictOrFailing int `json:"conflict_or_failing"`
}
DeliveryReport is the person's pull requests opened in the window, from GitHub. Status is "loading" while a search has not answered, "unavailable" when none could, and "ready".
type Entry ¶
type Entry struct {
// Key changes whenever one of the tree's files does (sizes and
// modification times, or a digest of generated content).
Key string `json:"key"`
Source string `json:"source"`
SessionID string `json:"session_id"`
Clone string `json:"clone"`
Repository string `json:"repository"`
// Branch is the top-level session's branch, as the agent recorded it.
Branch string `json:"branch,omitempty"`
ModifiedAt time.Time `json:"modified_at"`
StartedAt *time.Time `json:"started_at,omitempty"`
EndedAt *time.Time `json:"ended_at,omitempty"`
// Generation marks one of baum's own runs, which is kept only so it is
// not read again: it never counts as the person's agent work.
Generation bool `json:"generation,omitempty"`
// ActiveSeconds is nil for a session without a transcript (metadata
// only): its activity is unknown, not zero. Hours holds the same seconds
// by the unix hour they fell in.
ActiveSeconds *int64 `json:"active_seconds,omitempty"`
Hours map[int64]int64 `json:"hours,omitempty"`
Prompts int `json:"prompts"`
ToolCalls int `json:"tool_calls"`
FailedTools int `json:"failed_tool_calls"`
Subagents int `json:"subagents,omitempty"`
ModelCalls int `json:"model_calls"`
Usage []Usage `json:"usage,omitempty"`
// TokensUnknown says some of the session's model calls reported no
// usage, or none was recorded at all, so its token sums are a floor.
TokensUnknown bool `json:"tokens_unknown,omitempty"`
// Commits are the commits the session recorded creating, as the agent
// printed them (possibly abbreviated).
Commits []string `json:"commits,omitempty"`
}
Entry is what the index keeps of one session tree: counts and token sums, never a prompt, a message, a tool's input or output, or a file path.
type FlowCounts ¶
type FlowCounts struct {
Opened int `json:"opened"`
OpenedMerged int `json:"opened_merged"`
Merged int `json:"merged"`
MergeSeconds []float64 `json:"merge_seconds"`
}
FlowCounts counts a day's pull requests not opened by a bot. Opened leaves drafts out; OpenedMerged is how many of those opened have merged since, for the merge rate; Merged counts those merged that day, whenever they opened, with each one's time to merge.
type FlowDay ¶
type FlowDay struct {
Date string `json:"date"`
Mine FlowCounts `json:"mine"`
Team FlowCounts `json:"team"`
}
FlowDay is one local day's flow.
type FlowReport ¶
type FlowReport struct {
// Available is false without the decision store the feature log keeps.
Available bool `json:"available"`
// Yours is false when the person's GitHub login is not known, so their
// own pull requests cannot be told from the team's.
Yours bool `json:"yours"`
// Unlisted names the repositories in scope the feature log has not
// listed: those with decisions off, or no clone.
Unlisted []string `json:"unlisted"`
// Unread names those whose pull requests could not be read.
Unread []string `json:"unread"`
Days []FlowDay `json:"days"`
}
FlowReport is the pull request flow of the repositories in scope, from the feature log's pull requests: per local day, those opened and those merged, the person's own and everyone's.
type Index ¶
type Index struct {
Path string
Clones Clones
Stores Stores
Log *slog.Logger
Now func() time.Time
// contains filtered or unexported fields
}
Index keeps one Entry per session file recorded in a registered clone in the last Retention, in the application database. A refresh reads only the files whose size or modification time changed since it last read them, and drops the entries of files that are gone, too old, or no longer in a registered clone. Requests read what it has while it works.
func (*Index) Refresh ¶
Refresh brings the index up to date with the session stores. Only sessions whose working directory is inside a registered clone (any of its worktrees) are read; a clone that is not available keeps its entries until it is back or unregistered.
func (*Index) Run ¶
Run refreshes at once, then every RefreshInterval and on Trigger, until ctx ends.
type Merger ¶
type Merger interface {
MergeOptions(ctx context.Context, repository github.Repository, number int) (github.MergeOptions, error)
MergePullRequest(ctx context.Context, repository github.Repository, number int, method, expectedHead string) error
}
Merger is the merge write uiserver.MergeHandler makes.
type ModelRow ¶
type ModelRow struct {
Model string `json:"model"`
Agent string `json:"agent"`
Sessions int `json:"sessions"`
Calls int64 `json:"calls"`
Tokens int64 `json:"tokens"`
Cost Cost `json:"cost"`
}
ModelRow is one model's figures; a session using two models counts in both.
type Options ¶
Options are the filter choices: the person's repositories, and the agents any indexed session ran.
type PlanSeries ¶
type PlanSeries struct {
Provider string `json:"provider"`
Window string `json:"window"`
Points []PlanPoint `json:"points"`
Peak float64 `json:"peak"`
// LimitHits counts the times the window reached 100%.
LimitHits int `json:"limit_hits"`
}
PlanSeries is one provider's window over time.
type PlanUsageReport ¶
type PlanUsageReport struct {
Available bool `json:"available"`
Series []PlanSeries `json:"series"`
}
PlanUsageReport is the sampled plan usage in the window.
type Progress ¶
type Progress struct {
Done int `json:"done"`
Total int `json:"total"`
Complete bool `json:"complete"`
}
Progress is how far a build has come: Done of Total sessions read or found unchanged. Complete is set once a whole pass has finished in this process.
type PullRequestSearcher ¶
type PullRequestSearcher interface {
AuthoredPullRequests(ctx context.Context, repository github.Repository, since time.Time) ([]github.AuthoredPullRequest, error)
}
PullRequestSearcher finds the pull requests the signed-in account opened; *github.Client is one.
type ReasonCount ¶
ReasonCount is how many failed jobs failed for one reason (a job reason code, which the app puts in words).
type Registry ¶
type Registry interface {
List(ctx context.Context) ([]repos.Clone, error)
Repositories(ctx context.Context) ([]repos.Repository, error)
}
Registry is what the report reads of the registered repositories; *repos.Registry is one.
type Report ¶
type Report struct {
Window Window `json:"window"`
Filters Filters `json:"filters"`
Options Options `json:"options"`
Indexing Progress `json:"indexing"`
Summary Summary `json:"summary"`
Daily []Day `json:"daily"`
Hours []Hour `json:"hours"`
ByAgent []Row `json:"by_agent"`
ByModel []ModelRow `json:"by_model"`
ByRepo []Row `json:"by_repository"`
Longest []SessionRow `json:"longest"`
Commits CommitShare `json:"commits"`
Baum BaumWork `json:"baum"`
Delivery DeliveryReport `json:"delivery"`
PlanUsage PlanUsageReport `json:"plan_usage"`
Flow FlowReport `json:"flow"`
}
Report is the answer of GET /api/analytics. Every figure that can be unknown is a pointer, null when unknown, never 0, with the reason beside it where one is known.
type RepositoryShare ¶
type RepositoryShare struct {
}
RepositoryShare is one repository's part of the share.
type Row ¶
type Row struct {
Key string `json:"key"`
Sessions int `json:"sessions"`
Prompts int `json:"prompts"`
ToolCalls int `json:"tool_calls"`
FailedToolCalls int `json:"failed_tool_calls"`
FailedRate *float64 `json:"failed_rate"`
ActiveSeconds *int64 `json:"active_seconds"`
// Tokens is nil when no session reported any; TokensIncomplete when
// some did not.
Tokens *int64 `json:"tokens"`
TokensIncomplete bool `json:"tokens_incomplete"`
Cost Cost `json:"cost"`
}
Row is one agent's or repository's figures.
type Service ¶
type Service struct {
Registry Registry
Index *Index
Jobs func() []jobs.Job
Actions *Actions
Plans *planusage.History
Delivery *Delivery
// Captures reads decision capture's ledger from since on; nil without
// decision capture.
Captures func(ctx context.Context, since time.Time) ([]decisionlog.LedgerEntry, error)
// Pulls reads the feature log's pull requests of a repository opened or
// merged from since on, and whether it listed the repository; nil
// without the decision store. Login is the person's GitHub login.
Pulls func(ctx context.Context, repository string, since time.Time) ([]decisionlog.FlowPull, bool, error)
Login func(ctx context.Context) string
// Location is the zone days and hours are counted in; nil is local.
Location *time.Location
Now func() time.Time
// contains filtered or unexported fields
}
Service builds reports from the index, the jobs ledger, the actions ledger, the plan usage history and GitHub's answers. Any of the last four may be nil; their sections then say they are unavailable.
type SessionRow ¶
type SessionRow struct {
Agent string `json:"agent"`
Repository string `json:"repository"`
Branch string `json:"branch"`
StartedAt *time.Time `json:"started_at"`
EndedAt *time.Time `json:"ended_at"`
ActiveSeconds *int64 `json:"active_seconds"`
Prompts int `json:"prompts"`
ToolCalls int `json:"tool_calls"`
FailedToolCalls int `json:"failed_tool_calls"`
Subagents int `json:"subagents"`
Tokens *int64 `json:"tokens"`
Cost Cost `json:"cost"`
}
SessionRow is one session among the longest.
type Stores ¶
Stores overrides where sessions are read from, for tests; the zero value reads each agent's default store in the home directory.
type Summary ¶
type Summary struct {
Sessions int `json:"sessions"`
// ActiveSeconds is nil when no session in the window recorded its
// activity; ActiveIncomplete when some did not.
ActiveSeconds *int64 `json:"active_seconds"`
ActiveIncomplete bool `json:"active_incomplete"`
Prompts int `json:"prompts"`
Subagents int `json:"subagents"`
Cost Cost `json:"cost"`
}
Summary is the page's headline figures.
type Updater ¶
type Updater interface {
UpdateOptions(ctx context.Context, repository github.Repository, number int) (prupdate.UpdateOptions, error)
UpdateBranch(ctx context.Context, repository github.Repository, number int, expectedHead string) error
RerunOptions(ctx context.Context, repository github.Repository, number int) (prupdate.RerunOptions, error)
RerunFailedChecks(ctx context.Context, repository github.Repository, number int, expectedHead string) (prupdate.RerunResult, error)
ReviewThreads(ctx context.Context, repository github.Repository, number, unresolved int) (prupdate.Threads, error)
ResolveThread(ctx context.Context, repository github.Repository, number int, id string) error
FailingChecks(ctx context.Context, repository github.Repository, number int, head string) (prupdate.Checks, error)
RerunJob(ctx context.Context, repository github.Repository, number int, expectedHead string, job int64) error
AllChecks(ctx context.Context, repository github.Repository, number int, head string) (prupdate.CheckList, error)
Conflicts(ctx context.Context, repository github.Repository, number int) (prupdate.ConflictFiles, error)
Description(ctx context.Context, repository github.Repository, number int) (prupdate.Description, error)
}
Updater is the branch update, check re-run and thread resolution writes uiserver.UpdateHandler makes. Re-running one job counts as a check re-run; resolving a thread is not recorded.
type Usage ¶
type Usage struct {
Model string `json:"model"`
Speed string `json:"speed,omitempty"`
Single bool `json:"single,omitempty"`
Band int `json:"band,omitempty"`
Calls int64 `json:"calls"`
Input int64 `json:"input"`
Output int64 `json:"output"`
CacheRead int64 `json:"cache_read"`
CacheWrite int64 `json:"cache_write"`
CacheWrite1h int64 `json:"cache_write_1h,omitempty"`
MinPrompt int64 `json:"min_prompt"`
MaxPrompt int64 `json:"max_prompt"`
}
Usage sums one model's tokens over requests that a price can be applied to together: the same speed, and every prompt on the same side of every long-prompt threshold of the price list (Single), or an aggregate the vendor reported without its requests, whose prompts are only known to be at most MaxPrompt.