Skip to content

feat(telemetry): add PostHog event tracking to publish and run commands #1607

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jul 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/cargo-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ jobs:
- name: Build
run: |
cargo build --release -p codemod --target ${{ matrix.target }}
env:
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}

- name: Chmod binary
run: |
Expand Down
27 changes: 27 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"crates/state",
"crates/codemod-sandbox",
"crates/codemod-sandbox/build",
"crates/telemetry",
"xtask"
]
resolver = "2"
Expand All @@ -29,6 +30,7 @@ butterflow-state = { path = "crates/state" }
butterflow-runners = { path = "crates/runners" }
butterflow-scheduler = { path = "crates/scheduler" }
codemod-sandbox = { path = "crates/codemod-sandbox" }
codemod-telemetry = { path = "crates/telemetry" }

anyhow = "1.0"
ast-grep-language = "0.38.6"
Expand Down
4 changes: 3 additions & 1 deletion crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ butterflow-core = { workspace = true }
butterflow-state = { workspace = true }
butterflow-runners = { workspace = true }
codemod-sandbox = { workspace = true }
codemod-telemetry = { workspace = true }
tokio = { workspace = true }
clap = { version = "4.5", features = ["derive"] }
flate2 = { workspace = true }
Expand Down Expand Up @@ -57,7 +58,8 @@ libtest-mimic = "0.8"
similar = "2.0"
ast-grep-language.workspace = true
tabled = "0.20.0"

posthog-rs = "0.3.7"
async-trait.workspace = true

[features]
default = []
Expand Down
20 changes: 19 additions & 1 deletion crates/cli/src/commands/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ use log::{debug, info, warn};
use reqwest;
use serde::{Deserialize, Serialize};
use serde_yaml;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
use walkdir::WalkDir;

use crate::auth::TokenStorage;
use codemod_telemetry::send_event::{BaseEvent, TelemetrySender};

#[derive(Args, Debug)]
pub struct Command {
Expand Down Expand Up @@ -117,7 +119,7 @@ struct PublishedPackage {
published_at: String,
}

pub async fn handler(args: &Command) -> Result<()> {
pub async fn handler(args: &Command, telemetry: &dyn TelemetrySender) -> Result<()> {
let package_path = args
.path
.as_ref()
Expand Down Expand Up @@ -196,6 +198,22 @@ pub async fn handler(args: &Command) -> Result<()> {
return Err(anyhow!("Failed to publish package"));
}

let cli_version = env!("CARGO_PKG_VERSION");

let _ = telemetry
.send_event(
BaseEvent {
kind: "codemodPublished".to_string(),
properties: HashMap::from([
("codemodName".to_string(), manifest.name.clone()),
("version".to_string(), manifest.version.clone()),
("cliVersion".to_string(), cli_version.to_string()),
]),
},
None,
)
.await;

println!("✅ Package published successfully!");
println!("📦 {}", format_package_name(&response.package));
println!("🏷️ Version: {}", response.package.version);
Expand Down
72 changes: 66 additions & 6 deletions crates/cli/src/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ use anyhow::Result;
use butterflow_core::utils::get_cache_dir;
use clap::Args;
use log::info;
use rand::Rng;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command as ProcessCommand;
use tokio::sync::Mutex;

use crate::auth_provider::CliAuthProvider;
use crate::dirty_git_check;
use crate::workflow_runner::{run_workflow, WorkflowRunConfig};
use butterflow_core::engine::Engine;
use butterflow_core::engine::{Engine, GLOBAL_STATS};
use butterflow_core::registry::{RegistryClient, RegistryConfig, RegistryError};
use codemod_sandbox::sandbox::engine::ExecutionStats;
use codemod_telemetry::send_event::{BaseEvent, TelemetrySender};

#[derive(Args, Debug)]
pub struct Command {
Expand Down Expand Up @@ -42,7 +47,11 @@ pub struct Command {
allow_dirty: bool,
}

pub async fn handler(engine: &Engine, args: &Command) -> Result<()> {
pub async fn handler(
engine: &Engine,
args: &Command,
telemetry: &dyn TelemetrySender,
) -> Result<()> {
// Create auth provider
let auth_provider = CliAuthProvider::new()?;

Expand Down Expand Up @@ -92,14 +101,60 @@ pub async fn handler(engine: &Engine, args: &Command) -> Result<()> {
);

// Execute the codemod
execute_codemod(
let stats = execute_codemod(
engine,
&resolved_package.package_dir,
&args.path,
&args.args,
args.dry_run,
)
.await?;
.await;

let cli_version = env!("CARGO_PKG_VERSION");

if let Err(e) = stats {
let _ = telemetry
.send_event(
BaseEvent {
kind: "failedToExecuteCommand".to_string(),
properties: HashMap::from([
("codemodName".to_string(), args.package.clone()),
("cliVersion".to_string(), cli_version.to_string()),
(
"commandName".to_string(),
"codemod.executeCodemod".to_string(),
),
]),
},
None,
)
.await;
return Err(e);
}

let stats = stats.unwrap();

let cli_version = env!("CARGO_PKG_VERSION");
let execution_id: [u8; 20] = rand::thread_rng().gen();
let execution_id = base64::Engine::encode(
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
execution_id,
);

let _ = telemetry
.send_event(
BaseEvent {
kind: "codemodExecuted".to_string(),
properties: HashMap::from([
("codemodName".to_string(), args.package.clone()),
("executionId".to_string(), execution_id.clone()),
("fileCount".to_string(), stats.files_modified.to_string()),
("cliVersion".to_string(), cli_version.to_string()),
]),
},
None,
)
.await;

Ok(())
}
Expand Down Expand Up @@ -139,7 +194,7 @@ async fn execute_codemod(
target_path: &Path,
additional_args: &[String],
dry_run: bool,
) -> Result<()> {
) -> Result<ExecutionStats> {
let workflow_path = package_dir.join("workflow.yaml");

info!(
Expand Down Expand Up @@ -176,6 +231,11 @@ async fn execute_codemod(

// Run workflow using the extracted workflow runner
run_workflow(engine, config).await?;
let stats = GLOBAL_STATS
.get_or_init(|| Mutex::new(ExecutionStats::default()))
.lock()
.await
.clone();

Ok(())
Ok(stats)
}
44 changes: 38 additions & 6 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use log::info;

mod ascii_art;
mod auth;
mod auth_provider;
Expand All @@ -10,6 +9,12 @@ mod dirty_git_check;
mod engine;
mod workflow_runner;
use ascii_art::print_ascii_art;
use codemod_telemetry::{
send_event::{PostHogSender, TelemetrySender, TelemetrySenderOptions},
send_null::NullSender,
};

use crate::auth::TokenStorage;

#[derive(Parser)]
#[command(name = "codemod")]
Expand Down Expand Up @@ -140,6 +145,7 @@ fn is_package_name(arg: &str) -> bool {
async fn handle_implicit_run_command(
engine: &butterflow_core::engine::Engine,
trailing_args: Vec<String>,
telemetry_sender: &dyn TelemetrySender,
) -> Result<bool> {
if trailing_args.is_empty() {
return Ok(false);
Expand All @@ -158,7 +164,7 @@ async fn handle_implicit_run_command(
match Cli::try_parse_from(&full_args) {
Ok(new_cli) => {
if let Some(Commands::Run(run_args)) = new_cli.command {
commands::run::handler(engine, &run_args).await?;
commands::run::handler(engine, &run_args, telemetry_sender).await?;
Ok(true)
} else {
Ok(false)
Expand All @@ -178,7 +184,7 @@ async fn handle_implicit_run_command(
#[tokio::main]
async fn main() -> Result<()> {
// Initialize logger
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
env_logger::init_from_env(env_logger::Env::default().default_filter_or("error"));

// Parse command line arguments
let cli = Cli::parse();
Expand All @@ -193,6 +199,30 @@ async fn main() -> Result<()> {
// Create engine
let engine = engine::create_engine()?;

let telemetry_sender: Box<dyn codemod_telemetry::send_event::TelemetrySender> =
if std::env::var("DISABLE_ANALYTICS") == Ok("true".to_string())
|| std::env::var("DISABLE_ANALYTICS") == Ok("1".to_string())
{
Box::new(NullSender {})
} else {
let storage = TokenStorage::new()?;
let config = storage.load_config()?;

let auth = storage.get_auth_for_registry(&config.default_registry)?;

let distinct_id = auth
.map(|auth| auth.user.id)
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());

Box::new(
PostHogSender::new(TelemetrySenderOptions {
distinct_id,
cloud_role: "CLI".to_string(),
})
.await,
)
};

// Handle command or implicit run
match &cli.command {
Some(Commands::Workflow(args)) => match &args.command {
Expand Down Expand Up @@ -236,13 +266,13 @@ async fn main() -> Result<()> {
commands::whoami::handler(args).await?;
}
Some(Commands::Publish(args)) => {
commands::publish::handler(args).await?;
commands::publish::handler(args, telemetry_sender.as_ref()).await?;
}
Some(Commands::Search(args)) => {
commands::search::handler(args).await?;
}
Some(Commands::Run(args)) => {
commands::run::handler(&engine, args).await?;
commands::run::handler(&engine, args, telemetry_sender.as_ref()).await?;
}
Some(Commands::Unpublish(args)) => {
commands::unpublish::handler(args).await?;
Expand All @@ -252,7 +282,9 @@ async fn main() -> Result<()> {
}
None => {
// Try to parse as implicit run command
if !handle_implicit_run_command(&engine, cli.trailing_args).await? {
if !handle_implicit_run_command(&engine, cli.trailing_args, telemetry_sender.as_ref())
.await?
{
// No valid subcommand or package name provided, show help
print_ascii_art();
eprintln!("No command provided. Use --help for usage information.");
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy