go-session/analytics.go
Claude a10e30d9db
chore(ax): pass 2 AX compliance sweep
Resolve all deferred items from pass 1 and deeper violations found in
this pass: rename abbreviated variables (a, e, f, s, ts, inp, inputStr,
firstTS/lastTS, argv, id) to full descriptive names; add usage-example
comments to all unexported helpers (parseFromReader, extractToolInput,
extractResultContent, truncate, shortID, formatDuration, generateTape,
extractCommand, lookupExecutable, isExecutablePath, runCommand,
resultError, repeatString, containsAny, indexOf, trimQuotes); rename
single-letter helper parameters to match AX principle 1.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-31 08:23:49 +01:00

141 lines
3.9 KiB
Go

// SPDX-Licence-Identifier: EUPL-1.2
package session
import (
"maps"
"slices"
"time"
core "dappco.re/go/core"
)
// SessionAnalytics holds computed metrics for a parsed session.
//
// Example:
// analytics := session.Analyse(sess)
type SessionAnalytics struct {
Duration time.Duration
ActiveTime time.Duration
EventCount int
ToolCounts map[string]int
ErrorCounts map[string]int
SuccessRate float64
AvgLatency map[string]time.Duration
MaxLatency map[string]time.Duration
EstimatedInputTokens int
EstimatedOutputTokens int
}
// Analyse iterates session events and computes analytics. Pure function, no I/O.
//
// Example:
// analytics := session.Analyse(sess)
func Analyse(sess *Session) *SessionAnalytics {
analytics := &SessionAnalytics{
ToolCounts: make(map[string]int),
ErrorCounts: make(map[string]int),
AvgLatency: make(map[string]time.Duration),
MaxLatency: make(map[string]time.Duration),
}
if sess == nil {
return analytics
}
analytics.Duration = sess.EndTime.Sub(sess.StartTime)
analytics.EventCount = len(sess.Events)
// Track totals for latency averaging
type latencyAccum struct {
total time.Duration
count int
}
latencies := make(map[string]*latencyAccum)
var totalToolCalls int
var totalErrors int
for evt := range sess.EventsSeq() {
// Token estimation: ~4 chars per token
analytics.EstimatedInputTokens += len(evt.Input) / 4
analytics.EstimatedOutputTokens += len(evt.Output) / 4
if evt.Type != "tool_use" {
continue
}
totalToolCalls++
analytics.ToolCounts[evt.Tool]++
if !evt.Success {
totalErrors++
analytics.ErrorCounts[evt.Tool]++
}
// Active time: sum of tool call durations
analytics.ActiveTime += evt.Duration
// Latency tracking
if _, ok := latencies[evt.Tool]; !ok {
latencies[evt.Tool] = &latencyAccum{}
}
latencies[evt.Tool].total += evt.Duration
latencies[evt.Tool].count++
if evt.Duration > analytics.MaxLatency[evt.Tool] {
analytics.MaxLatency[evt.Tool] = evt.Duration
}
}
// Compute averages
for tool, accumulator := range latencies {
if accumulator.count > 0 {
analytics.AvgLatency[tool] = accumulator.total / time.Duration(accumulator.count)
}
}
// Success rate
if totalToolCalls > 0 {
analytics.SuccessRate = float64(totalToolCalls-totalErrors) / float64(totalToolCalls)
}
return analytics
}
// FormatAnalytics returns a tabular text summary suitable for CLI display.
//
// Example:
// summary := session.FormatAnalytics(analytics)
func FormatAnalytics(a *SessionAnalytics) string {
builder := core.NewBuilder()
builder.WriteString("Session Analytics\n")
builder.WriteString(repeatString("=", 50) + "\n\n")
builder.WriteString(core.Sprintf(" Duration: %s\n", formatDuration(a.Duration)))
builder.WriteString(core.Sprintf(" Active Time: %s\n", formatDuration(a.ActiveTime)))
builder.WriteString(core.Sprintf(" Events: %d\n", a.EventCount))
builder.WriteString(core.Sprintf(" Success Rate: %.1f%%\n", a.SuccessRate*100))
builder.WriteString(core.Sprintf(" Est. Input Tk: %d\n", a.EstimatedInputTokens))
builder.WriteString(core.Sprintf(" Est. Output Tk: %d\n", a.EstimatedOutputTokens))
if len(a.ToolCounts) > 0 {
builder.WriteString("\n Tool Breakdown\n")
builder.WriteString(" " + repeatString("-", 48) + "\n")
builder.WriteString(core.Sprintf(" %-14s %6s %6s %10s %10s\n",
"Tool", "Calls", "Errors", "Avg", "Max"))
builder.WriteString(" " + repeatString("-", 48) + "\n")
// Sort tools for deterministic output
for _, tool := range slices.Sorted(maps.Keys(a.ToolCounts)) {
errors := a.ErrorCounts[tool]
avg := a.AvgLatency[tool]
max := a.MaxLatency[tool]
builder.WriteString(core.Sprintf(" %-14s %6d %6d %10s %10s\n",
tool, a.ToolCounts[tool], errors,
formatDuration(avg), formatDuration(max)))
}
}
return builder.String()
}