Skip to content

Commit c2ad4a7

Browse files
committed
Add posthog for but cli operations
1 parent 7bb995d commit c2ad4a7

File tree

5 files changed

+163
-24
lines changed

5 files changed

+163
-24
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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@ For examples `but rub --help`.")]
5454
#[clap(hide = true)]
5555
Metrics {
5656
#[clap(long, value_enum)]
57-
command_name: MetricsCommandName,
57+
command_name: CommandName,
58+
#[clap(long)]
59+
props: String,
5860
},
5961
}
6062

6163
#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)]
62-
pub enum MetricsCommandName {
64+
pub enum CommandName {
6365
#[clap(alias = "log")]
6466
Log,
6567
#[clap(alias = "status")]

crates/but/src/main.rs

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +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};
6+
use metrics::{Event, Metrics, Props, metrics_if_configured};
77
mod command;
88
mod id;
99
mod log;
@@ -20,6 +20,7 @@ async fn main() -> Result<()> {
2020

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

2425
match &args.cmd {
2526
Subcommands::Mcp { internal } => {
@@ -39,37 +40,70 @@ async fn main() -> Result<()> {
3940
}
4041
None => command::list_actions(&args.current_dir, args.json, 0, 10),
4142
},
42-
Subcommands::Metrics { command_name } => {
43+
Subcommands::Metrics {
44+
command_name,
45+
props,
46+
} => {
4347
let event = &mut Event::new((*command_name).into());
48+
if let Ok(props) = Props::from_json_string(props) {
49+
props.update_event(event);
50+
}
4451
Metrics::capture_blocking(&app_settings, event.clone()).await;
4552
Ok(())
4653
}
4754
Subcommands::Claude(claude::Platform { cmd }) => match cmd {
4855
claude::Subcommands::PreTool => {
49-
let out = command::claude::handle_pre_tool_call()?;
50-
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();
5160
Ok(())
5261
}
5362
claude::Subcommands::PostTool => {
54-
let out = command::claude::handle_post_tool_call()?;
55-
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();
5667
Ok(())
5768
}
5869
claude::Subcommands::Stop => {
59-
let out = command::claude::handle_stop().await?;
60-
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();
6174
Ok(())
6275
}
6376
},
64-
Subcommands::Log => log::commit_graph(&args.current_dir, args.json),
65-
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+
}
6687
Subcommands::Rub { source, target } => {
6788
let result = rub::handle(&args.current_dir, args.json, source, target)
6889
.context("Rubbed the wrong way.");
6990
if let Err(e) = &result {
7091
eprintln!("{} {}", e, e.root_cause());
7192
}
93+
metrics_if_configured(app_settings, CommandName::Rub, props(start, &result)).ok();
7294
Ok(())
7395
}
7496
}
7597
}
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+
}

crates/but/src/metrics/mod.rs

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use std::{collections::HashMap, env};
22

33
use but_settings::AppSettings;
4+
use clap::ValueEnum;
5+
use command_group::AsyncCommandGroup;
46
use posthog_rs::Client;
57
use serde::{Deserialize, Serialize};
68

7-
use crate::args::MetricsCommandName;
9+
use crate::args::CommandName;
810

911
#[derive(Debug, Clone)]
1012
pub struct Metrics {
@@ -25,20 +27,53 @@ pub enum EventKind {
2527
Unknown,
2628
}
2729

28-
impl From<MetricsCommandName> for EventKind {
29-
fn from(command_name: MetricsCommandName) -> Self {
30+
impl From<CommandName> for EventKind {
31+
fn from(command_name: CommandName) -> Self {
3032
match command_name {
31-
MetricsCommandName::Log => EventKind::CliLog,
32-
MetricsCommandName::Status => EventKind::CliStatus,
33-
MetricsCommandName::Rub => EventKind::CliRub,
34-
MetricsCommandName::ClaudePreTool => EventKind::ClaudePreTool,
35-
MetricsCommandName::ClaudePostTool => EventKind::ClaudePostTool,
36-
MetricsCommandName::ClaudeStop => EventKind::ClaudeStop,
33+
CommandName::Log => EventKind::CliLog,
34+
CommandName::Status => EventKind::CliStatus,
35+
CommandName::Rub => EventKind::CliRub,
36+
CommandName::ClaudePreTool => EventKind::ClaudePreTool,
37+
CommandName::ClaudePostTool => EventKind::ClaudePostTool,
38+
CommandName::ClaudeStop => EventKind::ClaudeStop,
3739
_ => EventKind::Unknown,
3840
}
3941
}
4042
}
4143

44+
pub struct Props {
45+
values: HashMap<String, serde_json::Value>,
46+
}
47+
48+
impl Props {
49+
pub fn new() -> Self {
50+
Props {
51+
values: HashMap::new(),
52+
}
53+
}
54+
55+
pub fn insert<K: Into<String>, V: Serialize>(&mut self, key: K, value: V) {
56+
if let Ok(value) = serde_json::to_value(value) {
57+
self.values.insert(key.into(), value);
58+
}
59+
}
60+
61+
pub fn as_json_string(&self) -> String {
62+
serde_json::to_string(&self.values).unwrap_or_default()
63+
}
64+
65+
pub fn from_json_string(json: &str) -> Result<Self, serde_json::Error> {
66+
let values: HashMap<String, serde_json::Value> = serde_json::from_str(json)?;
67+
Ok(Props { values })
68+
}
69+
70+
pub fn update_event(&self, event: &mut Event) {
71+
for (key, value) in &self.values {
72+
event.insert_prop(key, value);
73+
}
74+
}
75+
}
76+
4277
#[derive(Debug, Clone)]
4378
pub struct Event {
4479
event_name: EventKind,
@@ -149,3 +184,30 @@ fn posthog_client(app_settings: AppSettings) -> Option<impl Future<Output = post
149184
None
150185
}
151186
}
187+
188+
/// If metrics are configured, this function spawns a process to handle metrics logging so that this CLI process can exit as soon as possible.
189+
pub(crate) fn metrics_if_configured(
190+
app_settings: AppSettings,
191+
cmd: CommandName,
192+
props: Props,
193+
) -> anyhow::Result<()> {
194+
if !app_settings.telemetry.app_metrics_enabled {
195+
return Ok(());
196+
}
197+
if let Some(v) = cmd.to_possible_value() {
198+
let binary_path = std::env::current_exe().unwrap_or_default();
199+
let mut group = tokio::process::Command::new(binary_path)
200+
.arg("metrics")
201+
.arg("--command-name")
202+
.arg(v.get_name())
203+
.arg("--props")
204+
.arg(props.as_json_string())
205+
.stderr(std::process::Stdio::null())
206+
.stdout(std::process::Stdio::null())
207+
.group()
208+
.kill_on_drop(false)
209+
.spawn()?;
210+
group.inner().id().map(|id| sysinfo::Pid::from(id as usize));
211+
}
212+
Ok(())
213+
}

0 commit comments

Comments
 (0)