go-session/analytics.go

133 lines
3.5 KiB
Go
Raw Normal View History

// SPDX-Licence-Identifier: EUPL-1.2
package session
import (
"fmt"
"maps"
"slices"
"strings"
"time"
)
// SessionAnalytics holds computed metrics for a parsed session.
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.
func Analyse(sess *Session) *SessionAnalytics {
a := &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 a
}
a.Duration = sess.EndTime.Sub(sess.StartTime)
a.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.Events {
// Token estimation: ~4 chars per token
a.EstimatedInputTokens += len(evt.Input) / 4
a.EstimatedOutputTokens += len(evt.Output) / 4
if evt.Type != "tool_use" {
continue
}
totalToolCalls++
a.ToolCounts[evt.Tool]++
if !evt.Success {
totalErrors++
a.ErrorCounts[evt.Tool]++
}
// Active time: sum of tool call durations
a.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 > a.MaxLatency[evt.Tool] {
a.MaxLatency[evt.Tool] = evt.Duration
}
}
// Compute averages
for tool, acc := range latencies {
if acc.count > 0 {
a.AvgLatency[tool] = acc.total / time.Duration(acc.count)
}
}
// Success rate
if totalToolCalls > 0 {
a.SuccessRate = float64(totalToolCalls-totalErrors) / float64(totalToolCalls)
}
return a
}
// FormatAnalytics returns a tabular text summary suitable for CLI display.
func FormatAnalytics(a *SessionAnalytics) string {
var b strings.Builder
b.WriteString("Session Analytics\n")
b.WriteString(strings.Repeat("=", 50) + "\n\n")
b.WriteString(fmt.Sprintf(" Duration: %s\n", formatDuration(a.Duration)))
b.WriteString(fmt.Sprintf(" Active Time: %s\n", formatDuration(a.ActiveTime)))
b.WriteString(fmt.Sprintf(" Events: %d\n", a.EventCount))
b.WriteString(fmt.Sprintf(" Success Rate: %.1f%%\n", a.SuccessRate*100))
b.WriteString(fmt.Sprintf(" Est. Input Tk: %d\n", a.EstimatedInputTokens))
b.WriteString(fmt.Sprintf(" Est. Output Tk: %d\n", a.EstimatedOutputTokens))
if len(a.ToolCounts) > 0 {
b.WriteString("\n Tool Breakdown\n")
b.WriteString(" " + strings.Repeat("-", 48) + "\n")
b.WriteString(fmt.Sprintf(" %-14s %6s %6s %10s %10s\n",
"Tool", "Calls", "Errors", "Avg", "Max"))
b.WriteString(" " + strings.Repeat("-", 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]
b.WriteString(fmt.Sprintf(" %-14s %6d %6d %10s %10s\n",
tool, a.ToolCounts[tool], errors,
formatDuration(avg), formatDuration(max)))
}
}
return b.String()
}