Skip to content

Commit 7bb995d

Browse files
committed
Implement posthog metrics for cli
1 parent 4d524a7 commit 7bb995d

File tree

3 files changed

+113
-22
lines changed

3 files changed

+113
-22
lines changed

crates/but/src/args.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,45 @@ 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: MetricsCommandName,
58+
},
59+
}
60+
61+
#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)]
62+
pub enum MetricsCommandName {
63+
#[clap(alias = "log")]
64+
Log,
65+
#[clap(alias = "status")]
66+
Status,
67+
#[clap(alias = "rub")]
68+
Rub,
69+
#[clap(
70+
alias = "claude-pre-tool",
71+
alias = "claudepretool",
72+
alias = "claudePreTool",
73+
alias = "ClaudePreTool"
74+
)]
75+
ClaudePreTool,
76+
#[clap(
77+
alias = "claude-post-tool",
78+
alias = "claudeposttool",
79+
alias = "claudePostTool",
80+
alias = "ClaudePostTool"
81+
)]
82+
ClaudePostTool,
83+
#[clap(
84+
alias = "claude-stop",
85+
alias = "claudestop",
86+
alias = "claudeStop",
87+
alias = "ClaudeStop"
88+
)]
89+
ClaudeStop,
90+
#[default]
91+
Unknown,
5392
}
5493

5594
pub mod actions {

crates/but/src/main.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use anyhow::{Context, Ok, Result};
33
mod args;
44
use args::{Args, Subcommands, actions, claude};
55
use but_settings::AppSettings;
6+
use metrics::{Event, Metrics};
67
mod command;
78
mod id;
89
mod log;
@@ -38,6 +39,11 @@ async fn main() -> Result<()> {
3839
}
3940
None => command::list_actions(&args.current_dir, args.json, 0, 10),
4041
},
42+
Subcommands::Metrics { command_name } => {
43+
let event = &mut Event::new((*command_name).into());
44+
Metrics::capture_blocking(&app_settings, event.clone()).await;
45+
Ok(())
46+
}
4147
Subcommands::Claude(claude::Platform { cmd }) => match cmd {
4248
claude::Subcommands::PreTool => {
4349
let out = command::claude::handle_pre_tool_call()?;

crates/but/src/metrics/mod.rs

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

33
use but_settings::AppSettings;
4+
use posthog_rs::Client;
45
use serde::{Deserialize, Serialize};
56

7+
use crate::args::MetricsCommandName;
8+
69
#[derive(Debug, Clone)]
710
pub struct Metrics {
811
sender: Option<tokio::sync::mpsc::UnboundedSender<Event>>,
@@ -13,7 +16,29 @@ pub struct Metrics {
1316
pub enum EventKind {
1417
Mcp,
1518
McpInternal,
19+
CliLog,
20+
CliStatus,
21+
CliRub,
22+
ClaudePreTool,
23+
ClaudePostTool,
24+
ClaudeStop,
25+
Unknown,
26+
}
27+
28+
impl From<MetricsCommandName> for EventKind {
29+
fn from(command_name: MetricsCommandName) -> Self {
30+
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,
37+
_ => EventKind::Unknown,
38+
}
39+
}
1640
}
41+
1742
#[derive(Debug, Clone)]
1843
pub struct Event {
1944
event_name: EventKind,
@@ -55,18 +80,7 @@ impl Metrics {
5580
pub fn new_with_background_handling(app_settings: &AppSettings) -> Self {
5681
let metrics_permitted = app_settings.telemetry.app_metrics_enabled;
5782
// Only create client and sender if metrics are permitted
58-
let client = if metrics_permitted {
59-
option_env!("POSTHOG_API_KEY").and_then(|api_key| {
60-
let options = posthog_rs::ClientOptionsBuilder::default()
61-
.api_key(api_key.to_string())
62-
.api_endpoint("https://eu.i.posthog.com/i/v0/e/".to_string())
63-
.build()
64-
.ok()?;
65-
Some(posthog_rs::client(options))
66-
})
67-
} else {
68-
None
69-
};
83+
let client = posthog_client(app_settings.clone());
7084
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
7185
let sender = if metrics_permitted {
7286
Some(sender)
@@ -77,19 +91,11 @@ impl Metrics {
7791

7892
if let Some(client_future) = client {
7993
let mut receiver = receiver;
80-
let distinct_id = app_settings.telemetry.app_distinct_id.clone();
94+
let app_settings = app_settings.clone();
8195
tokio::task::spawn(async move {
8296
let client = client_future.await;
8397
while let Some(event) = receiver.recv().await {
84-
let mut posthog_event = if let Some(id) = &distinct_id {
85-
posthog_rs::Event::new(event.event_name.to_string(), id.clone())
86-
} else {
87-
posthog_rs::Event::new_anon(event.event_name.to_string())
88-
};
89-
for (key, prop) in event.props {
90-
let _ = posthog_event.insert_prop(key, prop);
91-
}
92-
let _ = client.capture(posthog_event).await;
98+
do_capture(&client, event, &app_settings).await.ok();
9399
}
94100
});
95101
}
@@ -102,4 +108,44 @@ impl Metrics {
102108
let _ = sender.send(event.clone());
103109
}
104110
}
111+
112+
pub async fn capture_blocking(app_settings: &AppSettings, event: Event) {
113+
if let Some(client) = posthog_client(app_settings.clone()) {
114+
do_capture(&client.await, event, app_settings).await.ok();
115+
}
116+
}
117+
}
118+
119+
fn do_capture(
120+
client: &Client,
121+
event: Event,
122+
app_settings: &AppSettings,
123+
) -> impl Future<Output = Result<(), posthog_rs::Error>> {
124+
let mut posthog_event = if let Some(id) = &app_settings.telemetry.app_distinct_id.clone() {
125+
posthog_rs::Event::new(event.event_name.to_string(), id.clone())
126+
} else {
127+
posthog_rs::Event::new_anon(event.event_name.to_string())
128+
};
129+
for (key, prop) in event.props {
130+
let _ = posthog_event.insert_prop(key, prop);
131+
}
132+
client.capture(posthog_event)
133+
}
134+
135+
/// Creates a PostHog client if metrics are enabled and the API key is set.
136+
fn posthog_client(app_settings: AppSettings) -> Option<impl Future<Output = posthog_rs::Client>> {
137+
if app_settings.telemetry.app_metrics_enabled {
138+
if let Some(api_key) = option_env!("POSTHOG_API_KEY") {
139+
let options = posthog_rs::ClientOptionsBuilder::default()
140+
.api_key(api_key.to_string())
141+
.api_endpoint("https://eu.i.posthog.com/i/v0/e/".to_string())
142+
.build()
143+
.ok()?;
144+
Some(posthog_rs::client(options))
145+
} else {
146+
None
147+
}
148+
} else {
149+
None
150+
}
105151
}

0 commit comments

Comments
 (0)