analytics

package
v0.2.0-rc.2 Latest Latest
Warning

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

Go to latest
Published: Nov 12, 2026 License: MIT Imports: 36 Imported by: 0

README

Local analytics

internal/analytics answers GET /api/analytics for the app's Analytics page: what the person's coding agents did in their registered clones, what it would cost at API prices, what baum itself ran, and what the person delivered on GitHub. The goal is keeping track of what agents are up to, so every figure is observational: it does not show that an agent caused a change or a bug.

GET /api/analytics?days=7|30|90&repo=owner/name&agent=claude_code|codex|cursor

days defaults to 30; it is whole local days, today included. repo must be a picked repository or a registered clone's; agent one of the three. Any other value is a 400. Every figure that can be unknown is null, never 0, with the reasons beside it where there are any.

The session index

Index keeps one entry per session file, one row each in the analytics_sessions table of app.sqlite in the daemon's state directory ($XDG_STATE_HOME/baum). It holds aggregates only: the agent, the clone and its repository, the top-level session's branch, its start and end, active seconds (in total and by hour), human prompts, tool calls and failed ones, subagents, model calls, token sums by model, speed and price band, and the commits the session recorded creating (their ids). No prompt, message, tool input or output, or file path is stored.

  • Only sessions whose working directory is inside a registered clone (any of its worktrees) are read, from the same stores and with the same discovery as the PR page (sessions/collector, sessions.Map); a refresh scans the Codex store once for all clones, and unchanged rollouts are not reopened. Sessions last written more than 90 days ago are skipped, and their entries dropped.
  • The index format version invalidates derived evidence after parser changes, including Codex code-mode command extraction.
  • An entry is keyed by its files' sizes and modification times (a digest for Cursor's generated tree), so a refresh reads only what changed. Files that are gone, and the entries of unregistered clones, are dropped; a clone that is not available keeps its entries until it is back.
  • The daemon builds it in the background when it starts and refreshes it every five minutes and when an agent session ends. While the first pass runs, the report answers with what it has and indexing: {done, total, complete}.
  • A save writes only the rows of entries added, replaced or dropped since the last one, in one transaction, so a refresh that changed nothing writes nothing and a failed save is retried whole on the next refresh. A first build saves after every 25 sessions read, and a stopping daemon saves what it read, so a restart resumes from the last save instead of starting over. The one-row analytics_index table holds the index format version and the price list's long-prompt thresholds the entries were summed with; rows stored under others are ignored and deleted with the next save. Refreshes run one at a time. An older baum kept the whole index in the cache/analytics-index document; the first open moves its entries into rows and deletes it in one transaction (a document of another version or thresholds is deleted unread). The head of each Claude transcript (working directory, start) is remembered by path, size and modification time, so an unchanged transcript is not opened again; the memory is per process and forgets deleted files.
  • baum's own generation runs are kept as marked entries, so they are not read again, and never count.
  • A session that does not map counts with unknown usage.

Counting

The hosted service's counting rules apply:

  • A session is a root session: nested agents add subagents, never sessions. One session read twice (two registrations of one clone) counts once, from its latest reading.
  • A human prompt is a root input of kind prompt or slash_command from a person, with text left once wrapper constructs are stripped (sessions/prompttext). A parent's instructions to a child agent are not.
  • A session is in the window when it started in it; its figures then count whole.
  • Active time sums the gaps before each agent event (a model call, a reply, a tool call or its result) of at most five minutes (IdleGap): time waiting for the person, or a session left open, adds nothing. A session with no transcript has unknown active time, not zero. Days and hours are local.
  • Tokens: every token processed, cache reads included (Codex input already includes its cached input). A model call or usage row that reported nothing makes the session's tokens a floor (tokens_incomplete).
  • Cost is priced at read time from pricing.Active(), so a corrected price corrects history. Requests are grouped by model, speed and long-prompt band, as the hosted cost queries group them; an aggregate the vendor reported without its requests is priced only below every threshold. A group with no price has no cost, never $0.00, and marks the total incomplete with the reason in words. It is what the usage would cost at API prices, not what a subscription charges, and it only reports: no budget, cap or decision reads it.
  • Failed tool calls are calls whose result was an error. A tool error is not an interruption.
  • Each day's sessions and active time are split by agent and by model. A session counts whole under its main model, the one that took most of its tokens ("Unknown model" when none reported any), so both splits sum to the day.

Agent-written share

The person's commits are those on the local branches of the clones in scope authored by the clone's user.email, committed in the window, merges left out, each counted once per repository however many clones hold it. A commit is agent-written when an indexed session of the repository recorded creating it (the matcher's commit_ref evidence), it carries an Entire-Checkpoint trailer, or a Co-Authored-By trailer names an agent by address (locallink.CoauthoredAgents); each commit counts once, under the strongest. The heuristic file-overlap match is not used, so the share is a floor. Under an agent filter only that agent's sessions and trailers count; a checkpoint names no agent, so its commit stays in the denominator only. A clone without user.email, or that git cannot read, is named in reasons. Commit lists are cached for five minutes.

baum's work

From the jobs ledger (the jobs table in app.sqlite), jobs created in the window: reviews, group reviews and fixes, retries of a review's failed section details (counted apart, never as reviews; one that stood down because its version changed or the pull request moved is not counted as cancelled), automatic ones, their states, tokens and spend (a run with usage but no price makes it incomplete), failures by reason code (the app puts them in words), fixes that pushed or failed, and review threads a fix replied to and resolved; reviews_completed counts the reviews and group reviews that finished. Merges, branch updates and check re-runs made from the app are recorded by Actions in the state/analytics-actions document in app.sqlite (time, kind, repository, pull request number) when GitHub accepts them, kept 90 days; they are null when there is no ledger. The agent filter matches a job's harness. decisions counts decision capture's calls started in the window from its ledger (Captures; null without one): calls, those that succeeded, failed and were abandoned when the daemon stopped, failures by reason code, and the spend of settled calls, incomplete when one has no cost and unknown, never zero, when none has. The agent filter matches the backend that made the call.

Pull request flow

flow reads the pull requests the Timeline's feature log keeps (Pulls, decisionlog.Store.PullFlow) for each picked repository in scope. Per local day it counts, for the team and for the person (their GitHub login from Login; yours is false when it is not known), the non-draft pull requests opened and how many of those have merged since, for the merge rate, and the pull requests merged that day with each one's time to merge. Pull requests opened by bots are left out. A repository the feature log does not read, with decisions off or no clone, is listed in unlisted, and one whose pull requests could not be read in unread; the agent filter does not apply. The repository is passed as the feature log stores it, so the store's key finds its rows. Without the store, available is false.

Delivery

Delivery caches one GitHub search per picked repository (repo:owner/name is:pr author:@me created:>=…, github.AuthoredPullRequests) for the last 90 days, for an hour (five minutes after a failure). A request never waits for it: a repository not searched yet makes the section loading and queues the search, and the app reads the page again. Figures cover non-draft pull requests opened in the window; drafts are counted separately. Time to merge and to the first review by someone else are medians. The conflict and failing-checks share is what GitHub reports for each pull request's latest head now, not its history. Line survival is not measured and must not be added. A failed search is a reason in fixed words, never GitHub's text. An account change forgets every answer and the queue.

Queued searches run on three workers. Every GitHub request they make, each page (50 pull requests, up to five a search) and each retry, first waits for a slot 2.5 seconds after the last: at most 24 search requests a minute across the workers, under GitHub's search limit of 30. The pace reaches the client through github.WithRequestPacer. Repositories whose answers expire together are therefore spread over time rather than searched at once, and the section shows the ones read so far (loading only while one has no answer and no failure).

A rate limit stops the queue: the repository that hit it, and every one still queued, keep their earlier answer and carry the rate-limit reason, and none is searched again before the limit's reset (at least five minutes). A request that has waited for its slot checks the limit again before it is sent. The limit is shared with the background refresh in both directions (background.Service.Paused and Limited): while the refresh holds a pause, searches are not sent and fail with the same reason, and a limit a search hits pauses the refresh until its reset. Searches do not count against the refresh's hourly request budget.

Plan usage

planusage.History samples the plans every 15 minutes while the daemon runs (through the plan usage cache, so Claude's usage endpoint is asked at most then) and keeps changed figures, plus one an hour while unchanged, for 90 days in the state/plan-usage-history document in app.sqlite. The report draws each provider window's samples in the window and counts the times it reached 100%. The agent filter picks the provider.

Tests

Tests use small Claude Code transcripts under a temporary store, a temporary git clone, an injected clock, a fake GitHub search and fake writes; they never read the real home directory.

Storage uses the shared application database; the index is a disposable cache and action/plan histories are durable state.

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

View Source
const (
	ActionMerge        = "merge"
	ActionUpdateBranch = "update_branch"
	ActionRerunChecks  = "rerun_checks"
)

The GitHub writes the app makes besides a fix, as the ledger records them.

View Source
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
)
View Source
const (
	DeliveryTTL = time.Hour
)

DeliveryTTL is how long one repository's search answer serves, and deliveryRetry how soon a failed search is tried again.

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

View Source
const LongestSessions = 10

LongestSessions is how many of the longest sessions the report lists.

Variables

View Source
var Agents = []string{"claude_code", "codex", "cursor"}

Agents are the agent filter's values: the vendors whose sessions the index reads.

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

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

type Actions struct {
	Path string
	Now  func() time.Time
	// contains filtered or unexported fields
}

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

func (a *Actions) Record(kind, repository string, pr int) error

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

func (a *Actions) RecordMerges(m Merger) Merger

RecordMerges records each merge m makes.

func (*Actions) RecordUpdates

func (a *Actions) RecordUpdates(u Updater) Updater

RecordUpdates records each branch update and check re-run u makes.

func (*Actions) Since

func (a *Actions) Since(t time.Time) []Action

Since lists the writes recorded at or after t.

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 BaumWeeks

type BaumWeeks struct {
	This     BaumWeek `json:"this"`
	Previous BaumWeek `json:"previous"`
}

BaumWeeks is two weeks of baum's work, for its trend.

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 Clones

type Clones interface {
	List(ctx context.Context) ([]repos.Clone, error)
}

Clones lists the registered clones; *repos.Registry is one.

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.
	Available bool     `json:"available"`
	Reasons   []string `json:"reasons"`
	Commits   int      `json:"commits"`
	// 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.
	Agent      int               `json:"agent_commits"`
	BySession  int               `json:"by_session"`
	ByCheck    int               `json:"by_checkpoint"`
	ByTrailer  int               `json:"by_trailer"`
	Share      *float64          `json:"share"`
	Repository []RepositoryShare `json:"by_repository"`
}

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 DayAgent

type DayAgent struct {
	Sessions      int   `json:"sessions"`
	ActiveSeconds int64 `json:"active_seconds"`
}

DayAgent is one agent's part of a 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

func (d *Delivery) Pulls(repositories []string) []repositoryPulls

Pulls returns what is cached for each repository and queues a search for those with nothing fresh.

func (*Delivery) Reset

func (d *Delivery) Reset()

Reset forgets every answer, as when the signed-in GitHub account changes.

func (*Delivery) Run

func (d *Delivery) Run(ctx context.Context)

Run gives the searches the daemon's lifetime: they stop when ctx ends.

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"`
	ConflictOrFailingShare   *float64 `json:"conflict_or_failing_share"`
}

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 Filters

type Filters struct {
	Repository string `json:"repository"`
	Agent      string `json:"agent"`
}

Filters echoes what the report was narrowed to; "" is everything.

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 Hour

type Hour struct {
	Hour          int   `json:"hour"`
	ActiveSeconds int64 `json:"active_seconds"`
}

Hour is the active time that fell in one local hour of the day, over the window.

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

func (x *Index) Refresh(ctx context.Context) error

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

func (x *Index) Run(ctx context.Context)

Run refreshes at once, then every RefreshInterval and on Trigger, until ctx ends.

func (*Index) Snapshot

func (x *Index) Snapshot() ([]Entry, Progress)

Snapshot returns the entries the index holds and how far it has come.

func (*Index) Trigger

func (x *Index) Trigger()

Trigger asks for a refresh soon, as when an agent session ended.

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

type Options struct {
	Repositories []string `json:"repositories"`
	Agents       []string `json:"agents"`
}

Options are the filter choices: the person's repositories, and the agents any indexed session ran.

type PlanPoint

type PlanPoint struct {
	At          time.Time `json:"at"`
	UsedPercent float64   `json:"used_percent"`
}

PlanPoint is one sampled figure.

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 Query

type Query struct {
	Days       int
	Repository string
	Agent      string
}

Query is what a report covers.

func ParseQuery

func ParseQuery(values map[string][]string) (Query, string)

ParseQuery reads ?days=7|30|90&repo=owner/name&agent=claude_code|codex|cursor. days defaults to 30; repo and agent default to all.

type ReasonCount

type ReasonCount struct {
	Reason string `json:"reason"`
	Count  int    `json:"count"`
}

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 {
	Repository string   `json:"repository"`
	Commits    int      `json:"commits"`
	Agent      int      `json:"agent_commits"`
	Share      *float64 `json:"share"`
}

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.

func (*Service) Handler

func (s *Service) Handler() http.Handler

Handler serves GET /api/analytics. A repository that is neither picked nor registered is a 400, so a request cannot name what baum does not watch.

func (*Service) Report

func (s *Service) Report(ctx context.Context, q Query) (Report, error)

Report builds the report for q.

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

type Stores struct {
	ClaudeProjectsDir string
	CodexSessionsDir  string
	CursorStorePaths  []string
}

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"`
	AgentShare       *float64 `json:"agent_share"`
}

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.

func (Usage) Tokens

func (u Usage) Tokens(vendor string) int64

Tokens is every token the group processed, cache reads included.

type Window

type Window struct {
	Days  int       `json:"days"`
	Start time.Time `json:"start"`
	End   time.Time `json:"end"`
}

Window is the report's span: whole local days, today included.

Jump to

Keyboard shortcuts

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