Skip to main content

systemprompt_cli/commands/analytics/
mod.rs

1//! Top-level `analytics` command group spanning all metric domains.
2//!
3//! [`AnalyticsCommands`] routes to the per-domain subcommand trees (overview,
4//! conversations, agents, tools, requests, sessions, content, traffic, costs).
5//! Each variant dispatches to its module's `execute` against the invocation's
6//! [`CommandContext`]. The [`shared`] module holds the time-range and output
7//! formatting helpers used across all domains.
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12pub mod agents;
13pub mod content;
14pub mod conversations;
15pub mod costs;
16pub mod overview;
17pub mod projection;
18pub mod requests;
19pub mod sessions;
20pub mod shared;
21pub mod tools;
22pub mod traffic;
23
24use anyhow::{Context, Result};
25use clap::Subcommand;
26
27use crate::context::CommandContext;
28use crate::shared::render_result;
29
30#[derive(Debug, Subcommand)]
31pub enum AnalyticsCommands {
32    #[command(subcommand, about = "Reporting projection status and recovery")]
33    Projection(projection::ProjectionCommands),
34    #[command(about = "Dashboard overview of all analytics")]
35    Overview(overview::OverviewArgs),
36
37    #[command(subcommand, about = "Conversation analytics")]
38    Conversations(conversations::ConversationsCommands),
39
40    #[command(subcommand, about = "Agent performance analytics")]
41    Agents(agents::AgentsCommands),
42
43    #[command(subcommand, about = "Tool usage analytics")]
44    Tools(tools::ToolsCommands),
45
46    #[command(about = "AI request analytics; defaults to the stats aggregate")]
47    Requests(requests::RequestsArgs),
48
49    #[command(subcommand, about = "Session analytics")]
50    Sessions(sessions::SessionsCommands),
51
52    #[command(subcommand, about = "Content performance analytics")]
53    Content(content::ContentCommands),
54
55    #[command(subcommand, about = "Traffic analytics")]
56    Traffic(traffic::TrafficCommands),
57
58    #[command(about = "Cost analytics; defaults to the spend summary")]
59    Costs(costs::CostsArgs),
60}
61
62pub async fn execute(command: AnalyticsCommands, ctx: &CommandContext) -> Result<()> {
63    if !matches!(&command, AnalyticsCommands::Projection(_)) {
64        let database = ctx.database().await?;
65        let status = systemprompt_runtime::reporting::status(database.db_pool())
66            .await
67            .context("Reporting schema unavailable; apply database migrations first")?;
68        if !status.initialized {
69            match status.rebuild_source.as_deref() {
70                Some(source) => anyhow::bail!(
71                    "Reporting baseline rebuild in progress: {source} ({} rows written, last heartbeat {}); retry shortly",
72                    status.rebuild_rows,
73                    status
74                        .rebuild_heartbeat_at
75                        .map_or_else(|| "never".to_owned(), |at| at.to_rfc3339())
76                ),
77                None => anyhow::bail!(
78                    "Reporting baseline is not initialized; the server builds it at startup, or run analytics projection rebuild"
79                ),
80            }
81        }
82    }
83    match command {
84        AnalyticsCommands::Projection(command) => projection::execute(command, ctx).await,
85        AnalyticsCommands::Overview(args) => {
86            let result =
87                overview::execute_with_pool(args, &ctx.database().await?, &ctx.cli).await?;
88            render_result(&result, &ctx.cli);
89            Ok(())
90        },
91        AnalyticsCommands::Conversations(cmd) => conversations::execute(cmd, ctx).await,
92        AnalyticsCommands::Agents(cmd) => agents::execute(cmd, ctx).await,
93        AnalyticsCommands::Tools(cmd) => tools::execute(cmd, ctx).await,
94        AnalyticsCommands::Requests(cmd) => requests::execute(cmd, ctx).await,
95        AnalyticsCommands::Sessions(cmd) => sessions::execute(cmd, ctx).await,
96        AnalyticsCommands::Content(cmd) => content::execute(cmd, ctx).await,
97        AnalyticsCommands::Traffic(cmd) => traffic::execute(cmd, ctx).await,
98        AnalyticsCommands::Costs(cmd) => costs::execute(cmd, ctx).await,
99    }
100}