Skip to content

Commit f512809

Browse files
authored
Merge pull request #9416 from gitbutlerapp/but-cli-posthog
but-cli-posthog
2 parents 4d524a7 + c2ad4a7 commit f512809

File tree

5 files changed

+263
-33
lines changed

5 files changed

+263
-33
lines changed

Cargo.lock

Lines changed: 40 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/but/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ bstr.workspace = true
2727
anyhow.workspace = true
2828
# rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", branch = "main" }
2929
rmcp = "0.1.5"
30+
command-group = { version = "5.0.1", features = ["with-tokio"] }
31+
sysinfo = "0.36.0"
3032
gitbutler-project.workspace = true
3133
gix.workspace = true
3234
but-core.workspace = true

crates/but/src/args.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,47 @@ For examples `but rub --help`.")]
5050
// Claude hooks
5151
#[clap(hide = true)]
5252
Claude(claude::Platform),
53+
/// If metrics are permitted, this subcommand handles posthog event creation.
54+
#[clap(hide = true)]
55+
Metrics {
56+
#[clap(long, value_enum)]
57+
command_name: CommandName,
58+
#[clap(long)]
59+
props: String,
60+
},
61+
}
62+
63+
#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)]
64+
pub enum CommandName {
65+
#[clap(alias = "log")]
66+
Log,
67+
#[clap(alias = "status")]
68+
Status,
69+
#[clap(alias = "rub")]
70+
Rub,
71+
#[clap(
72+
alias = "claude-pre-tool",
73+
alias = "claudepretool",
74+
alias = "claudePreTool",
75+
alias = "ClaudePreTool"
76+
)]
77+
ClaudePreTool,
78+
#[clap(
79+
alias = "claude-post-tool",
80+
alias = "claudeposttool",
81+
alias = "claudePostTool",
82+
alias = "ClaudePostTool"
83+
)]
84+
ClaudePostTool,
85+
#[clap(
86+
alias = "claude-stop",
87+
alias = "claudestop",
88+
alias = "claudeStop",
89+
alias = "ClaudeStop"
90+
)]
91+
ClaudeStop,
92+
#[default]
93+
Unknown,
5394
}
5495

5596
pub mod actions {

crates/but/src/main.rs

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use anyhow::{Context, Ok, Result};
1+
use anyhow::{Context, Result};
22

33
mod args;
4-
use args::{Args, Subcommands, actions, claude};
4+
use args::{Args, CommandName, Subcommands, actions, claude};
55
use but_settings::AppSettings;
6+
use metrics::{Event, Metrics, Props, metrics_if_configured};
67
mod command;
78
mod id;
89
mod log;
@@ -19,6 +20,7 @@ async fn main() -> Result<()> {
1920

2021
let namespace = option_env!("IDENTIFIER").unwrap_or("com.gitbutler.app");
2122
gitbutler_secret::secret::set_application_namespace(namespace);
23+
let start = std::time::Instant::now();
2224

2325
match &args.cmd {
2426
Subcommands::Mcp { internal } => {
@@ -38,32 +40,70 @@ async fn main() -> Result<()> {
3840
}
3941
None => command::list_actions(&args.current_dir, args.json, 0, 10),
4042
},
43+
Subcommands::Metrics {
44+
command_name,
45+
props,
46+
} => {
47+
let event = &mut Event::new((*command_name).into());
48+
if let Ok(props) = Props::from_json_string(props) {
49+
props.update_event(event);
50+
}
51+
Metrics::capture_blocking(&app_settings, event.clone()).await;
52+
Ok(())
53+
}
4154
Subcommands::Claude(claude::Platform { cmd }) => match cmd {
4255
claude::Subcommands::PreTool => {
43-
let out = command::claude::handle_pre_tool_call()?;
44-
println!("{}", serde_json::to_string(&out)?);
56+
let result = command::claude::handle_pre_tool_call();
57+
let p = props(start, &result);
58+
println!("{}", serde_json::to_string(&result?)?);
59+
metrics_if_configured(app_settings, CommandName::ClaudePreTool, p).ok();
4560
Ok(())
4661
}
4762
claude::Subcommands::PostTool => {
48-
let out = command::claude::handle_post_tool_call()?;
49-
println!("{}", serde_json::to_string(&out)?);
63+
let result = command::claude::handle_post_tool_call();
64+
let p = props(start, &result);
65+
println!("{}", serde_json::to_string(&result?)?);
66+
metrics_if_configured(app_settings, CommandName::ClaudePostTool, p).ok();
5067
Ok(())
5168
}
5269
claude::Subcommands::Stop => {
53-
let out = command::claude::handle_stop().await?;
54-
println!("{}", serde_json::to_string(&out)?);
70+
let result = command::claude::handle_stop().await;
71+
let p = props(start, &result);
72+
println!("{}", serde_json::to_string(&result?)?);
73+
metrics_if_configured(app_settings, CommandName::ClaudeStop, p).ok();
5574
Ok(())
5675
}
5776
},
58-
Subcommands::Log => log::commit_graph(&args.current_dir, args.json),
59-
Subcommands::Status => status::worktree(&args.current_dir, args.json),
77+
Subcommands::Log => {
78+
let result = log::commit_graph(&args.current_dir, args.json);
79+
metrics_if_configured(app_settings, CommandName::Log, props(start, &result)).ok();
80+
Ok(())
81+
}
82+
Subcommands::Status => {
83+
let result = status::worktree(&args.current_dir, args.json);
84+
metrics_if_configured(app_settings, CommandName::Status, props(start, &result)).ok();
85+
Ok(())
86+
}
6087
Subcommands::Rub { source, target } => {
6188
let result = rub::handle(&args.current_dir, args.json, source, target)
6289
.context("Rubbed the wrong way.");
6390
if let Err(e) = &result {
6491
eprintln!("{} {}", e, e.root_cause());
6592
}
93+
metrics_if_configured(app_settings, CommandName::Rub, props(start, &result)).ok();
6694
Ok(())
6795
}
6896
}
6997
}
98+
99+
fn props<E, T, R>(start: std::time::Instant, result: R) -> Props
100+
where
101+
R: std::ops::Deref<Target = Result<T, E>>,
102+
E: std::fmt::Display,
103+
{
104+
let error = result.as_ref().err().map(|e| e.to_string());
105+
let mut props = Props::new();
106+
props.insert("durationMs", start.elapsed().as_millis());
107+
props.insert("error", error);
108+
props
109+
}

0 commit comments

Comments
 (0)