How Session Statistics Analytics Are Calculated in AgentsView: SQLite vs Go Implementation
AgentsView calculates session statistics analytics through a dual-path architecture that uses optimized SQLite queries for simple filters and a pure-Go aggregation engine for complex time-of-day or model-specific filtering, returning comprehensive metrics including totals, averages, medians, percentiles, and project concentration ratios.
The analytics API in the kenn-io/agentsview repository gathers session-level statistics through the internal/db/analytics.go file. The core entry point (*DB).GetAnalyticsSummary processes an AnalyticsFilter to return high-level metrics across matching sessions, implementing both a high-performance SQL path and a flexible Go-based fallback.
Analytics Filter Construction and Session Pre-filtering
The calculation begins with AnalyticsFilter, a struct that encapsulates all query parameters including date ranges, machine IDs, projects, agents, models, timezones, and hour-of-day or day-of-week constraints.
The filter system uses two primary helpers:
buildWhereandbuildWhereWithDategenerate the SQLWHEREclause and argument slice for the base query (internal/db/analytics.go#L10-L28).- When time-of-day filters are active,
filteredSessionIDs(orfilteredSessionIDsModelfor model-specific queries) pre-filters sessions that satisfy hour and day constraints before aggregation begins (internal/db/analytics.go#L88-L100).
This pre-filtering ensures that subsequent calculations only operate on relevant session rows, regardless of whether the SQLite or Go path is taken.
Dual-Path Calculation: SQLite Fast-Path vs Go Fallback
GetAnalyticsSummary implements a performance optimization that attempts a pure-SQLite query first, falling back to Go only when necessary (internal/db/analytics.go#L70-L76).
The SQLite fast-path activates when:
- The filter can be expressed entirely in SQLite via
f.canUseSQLiteTimeSQL(). - No model-specific filtering is required.
If either condition fails, the function switches to getAnalyticsSummaryGo (internal/db/analytics.go#L121-L124), which loads matching sessions into memory and performs aggregation using Go standard library functions.
SQLite Aggregation Query Deep Dive
When conditions permit, the SQLite path executes a sophisticated Common Table Expression (CTE) query that computes all metrics in a single database round-trip.
The query structure consists of three CTEs:
filtered: Selects relevant sessions with timezone-adjustedlocal_datecalculations.ranked: Uses window functions (ROW_NUMBERandCOUNT) to establish ordinal positions for percentile calculations.project_totals: Aggregates message counts per project to identify the most active project and calculate concentration metrics.
WITH filtered AS (
SELECT id, project, agent, message_count,
total_output_tokens, has_total_output_tokens,
<dateExpr> AS local_date
FROM sessions
WHERE <where>
),
ranked AS (
SELECT message_count,
ROW_NUMBER() OVER (ORDER BY message_count ASC) AS rn,
COUNT(*) OVER () AS n
FROM filtered
),
project_totals AS (
SELECT project, SUM(message_count) AS messages
FROM filtered
GROUP BY project
)
SELECT
COUNT(*) AS total_sessions,
COALESCE(SUM(message_count),0) AS total_messages,
COALESCE(SUM(CASE WHEN has_total_output_tokens
THEN total_output_tokens ELSE 0 END),0) AS total_output_tokens,
COALESCE(SUM(CASE WHEN has_total_output_tokens THEN 1 ELSE 0 END),0) AS token_reporting_sessions,
COUNT(DISTINCT project) AS active_projects,
COUNT(DISTINCT local_date) AS active_days,
COALESCE(ROUND(AVG(message_count),1),0) AS avg_messages,
-- median via two-row window
COALESCE((
SELECT CAST(AVG(message_count) AS INTEGER)
FROM ranked
WHERE rn IN (CAST(((n+1)/2) AS INTEGER),
CAST(((n+2)/2) AS INTEGER))
),0) AS median_messages,
-- 90th percentile
COALESCE((
SELECT message_count
FROM ranked
WHERE rn = MIN(CAST(n*0.9 AS INTEGER)+1, n)
LIMIT 1
),0) AS p90_messages,
COALESCE((
SELECT project
FROM project_totals
ORDER BY messages DESC, project ASC
LIMIT 1
),'') AS most_active,
COALESCE(ROUND((
SELECT SUM(messages)
FROM (SELECT messages FROM project_totals ORDER BY messages DESC LIMIT 3)
) * 1.0 / NULLIF(SUM(message_count),0), 3),0) AS concentration
FROM filtered;
Key calculation methods in the SQL path:
- Median: Averages the middle one or two rows from the
rankedwindow usingrn IN ((n+1)/2, (n+2)/2). - P90: Selects the row at index
⌈0.9·n⌉usingWHERE rn = MIN(CAST(n*0.9 AS INTEGER)+1, n). - Most active project: Orders
project_totalsby message count descending, then alphabetically by project name, takingLIMIT 1. - Concentration: Calculates the proportion of messages contained in the top-3 projects by summing their counts and dividing by total messages, rounded to three decimal places.
Results are scanned directly into an AnalyticsSummary struct (internal/db/analytics.go#L43-L66).
Go Fallback Implementation for Complex Filters
When the SQLite path is unavailable (typically due to model filters or complex time-of-day constraints), getAnalyticsSummaryGo executes a four-stage aggregation process:
- Session loading: Queries the database for sessions matching the generic filter (
internal/db/analytics.go#L138-L143). - Model filtering: If a model filter exists, retrieves per-session message statistics via
getAnalyticsFilteredMessageStats(internal/db/analytics.go#L144-L155). - Data construction: Iterates through rows to build a slice of
sessionRowstructs containing raw session data (internal/db/analytics.go#L151-L170). - Aggregation: Accumulates totals, per-project message counts, calendar day sets, and per-agent aggregates in memory (
internal/db/analytics.go#L226-L244).
Derived metric calculations in Go:
AvgMessages: ComputestotalMessages / totalSessionsas a float64, rounded to one decimal place (internal/db/analytics.go#L263-L267).MedianMessages: Sorts the message count slice and selects the middle value (or average of two middle values) (internal/db/analytics.go#L268-L274).P90Messages: Indexes into the sorted slice at positionint(0.9 * n)(internal/db/analytics.go#L275-L279).MostActive: Iterates the projects map to find the highest message count, using lexical comparison for tie-breaking (internal/db/analytics.go#L281-L288).Concentration: Sums the top-3 project counts and divides by total messages, rounded to three decimals (internal/db/analytics.go#L290-L304).
The Go path also collects distinct model names from the selected sessions (internal/db/analytics.go#L245-L258), populating the optional models field in the result.
AnalyticsSummary Result Structure and API Response
The calculation returns an AnalyticsSummary struct (defined at internal/db/analytics.go#L44-L64) containing the following fields:
total_sessions: Count of sessions matching the filter.total_messages: Sum ofmessage_countacross all matched sessions.total_output_tokens: Aggregated token usage from sessions reporting token data.token_reporting_sessions: Count of sessions that exposed token usage metrics.active_projects: Number of unique project identifiers.active_days: Distinct calendar days (timezone-adjusted) with activity.avg_messages: Mean messages per session (rounded to 0.1).median_messages: Median message count per session.p90_messages: 90th percentile of messages per session.most_active: Project identifier with highest message volume.concentration: Fraction of total messages contained in the top-3 projects (0.0-1.0).agents: Per-agent breakdown of session and message totals.models: Distinct model names used (optional, populated only in Go path).
The HTTP handler in internal/server/analytics.go exposes these statistics via the /api/analytics/summary endpoint. It parses query parameters into an AnalyticsFilter, invokes db.GetAnalyticsSummary, and returns the JSON payload. The frontend consumes these types through frontend/src/lib/api/types/analytics.ts:
// TypeScript client example
const response = await fetch('/api/analytics/summary?from=2024-01-01&to=2024-12-31');
const summary: AnalyticsSummary = await response.json();
console.log('Average messages per session:', summary.avg_messages);
console.log('Top project:', summary.most_active);
console.log('Concentration ratio:', summary.concentration);
Summary
- AgentsView implements session statistics analytics through a dual-path system in
internal/db/analytics.go, optimizing for performance while maintaining correctness across all filter combinations. - The SQLite fast-path computes totals, averages, medians, percentiles, and project concentration ratios using window functions and CTEs when filters are simple and model-agnostic.
- The Go fallback handles complex filters (model-specific or time-of-day) by loading sessions into memory and calculating statistics using sorted slices and map aggregations.
- Key metrics include
median_messages(calculated via window functions or sorted indexing),p90_messages(90th percentile),concentration(top-3 project share), andmost_active(winner-take-all project identification). - The API surface at
/api/analytics/summarybridges the database layer with the frontend TypeScript definitions, providing a type-safe contract for analytics consumption.
Frequently Asked Questions
What triggers the Go fallback instead of the SQLite fast-path?
The Go fallback activates when f.canUseSQLiteTimeSQL() returns false or when a model filter is present in the AnalyticsFilter. Complex time-of-day filters and model-specific queries require logic that cannot be expressed efficiently in SQLite window functions, forcing the system to load matching sessions into memory and perform aggregation using Go's standard library.
How does AgentsView calculate the median and 90th percentile for session messages?
In the SQLite path, the median uses a ranked CTE with ROW_NUMBER() to identify the middle row (or average of two middle rows for even counts), while the P90 selects the row at index ⌈0.9·n⌉. In the Go path, the code sorts the message count slice and indexes directly into the sorted array at positions n/2 for median and int(0.9*n) for P90.
What is the "concentration" metric in session statistics analytics?
Concentration measures message distribution inequality across projects, defined as the fraction of total messages contained within the top-3 most active projects. The value ranges from 0.0 to 1.0, with higher values indicating that activity is concentrated in fewer projects. The metric is calculated by summing the message counts of the top-3 projects and dividing by total_messages, rounded to three decimal places.
How are time-of-day filters handled when calculating session statistics?
Time-of-day filters first execute filteredSessionIDs (or filteredSessionIDsModel when model filtering is active) to pre-select session IDs that match the hour and day-of-week constraints (internal/db/analytics.go#L88-L100). These IDs are then used to constrain the main aggregation query. If the filter complexity prevents SQLite optimization, the Go path loads only these pre-filtered sessions before performing in-memory calculations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →