mirror of
https://github.com/railwayapp/cli
synced 2026-04-21 14:07:23 +00:00
Add per-command CLI telemetry (#791)
* Add per-command telemetry via cliEventTrack mutation Sends command name, subcommand, duration, success/error, OS, arch, CLI version, and CI flag to the Railway API after each invocation. Silently skipped when unauthenticated, on network failure, or when the user opts out via DO_NOT_TRACK=1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Migrate telemetry to typed graphql_client mutation Pull updated schema with cliEventTrack mutation and replace the raw JSON POST with typed codegen via graphql_client, matching the rest of the codebase. Also adds DNS_RECORD_TYPE_TXT from the schema update. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add one-time telemetry notice on first run Shows a notice to stderr informing users about CLI usage data collection, with DO_NOT_TRACK=1 opt-out. Persisted to ~/.railway/notices.json so it only displays once. Suppressed in CI/non-TTY and when telemetry is already disabled. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Support RAILWAY_NO_TELEMETRY env var for opting out of telemetry Adds RAILWAY_NO_TELEMETRY=1 as a Railway-specific alternative to DO_NOT_TRACK=1 for disabling CLI telemetry. Updates the notice text to mention both variables. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix telemetry docs link to /cli/telemetry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix cargo fmt --all formatting in telemetry notice Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
85f6e0061b
commit
330ac9ed67
6 changed files with 2956 additions and 18 deletions
|
|
@ -9,6 +9,14 @@ use crate::controllers;
|
|||
pub type DateTime = DateTimeType<Utc>;
|
||||
type EnvironmentConfig = controllers::config::EnvironmentConfig;
|
||||
|
||||
#[derive(GraphQLQuery)]
|
||||
#[graphql(
|
||||
schema_path = "src/gql/schema.json",
|
||||
query_path = "src/gql/mutations/strings/CliEventTrack.graphql",
|
||||
response_derives = "Debug, Serialize, Clone"
|
||||
)]
|
||||
pub struct CliEventTrack;
|
||||
|
||||
#[derive(GraphQLQuery)]
|
||||
#[graphql(
|
||||
schema_path = "src/gql/schema.json",
|
||||
|
|
@ -259,6 +267,7 @@ impl std::fmt::Display for custom_domain_create::DNSRecordType {
|
|||
Self::DNS_RECORD_TYPE_CNAME => write!(f, "CNAME"),
|
||||
Self::DNS_RECORD_TYPE_A => write!(f, "A"),
|
||||
Self::DNS_RECORD_TYPE_NS => write!(f, "NS"),
|
||||
Self::DNS_RECORD_TYPE_TXT => write!(f, "TXT"),
|
||||
Self::DNS_RECORD_TYPE_UNSPECIFIED => write!(f, "UNSPECIFIED"),
|
||||
Self::UNRECOGNIZED => write!(f, "UNRECOGNIZED"),
|
||||
Self::Other(s) => write!(f, "{s}"),
|
||||
|
|
|
|||
3
src/gql/mutations/strings/CliEventTrack.graphql
Normal file
3
src/gql/mutations/strings/CliEventTrack.graphql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mutation CliEventTrack($input: CliEventTrackInput!) {
|
||||
cliEventTrack(input: $input)
|
||||
}
|
||||
2823
src/gql/schema.json
generated
2823
src/gql/schema.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -49,9 +49,27 @@ macro_rules! commands {
|
|||
match matches.subcommand() {
|
||||
$(
|
||||
Some((stringify!([<$module:snake>]), sub_matches)) => {
|
||||
let args = <$module::Args as ::clap::FromArgMatches>::from_arg_matches(sub_matches)
|
||||
.map_err(anyhow::Error::from)?;
|
||||
$module::command(args).await?;
|
||||
let subcommand_name = sub_matches.subcommand_name().map(|s| s.to_string());
|
||||
let args = <$module::Args as ::clap::FromArgMatches>::from_arg_matches(sub_matches)
|
||||
.map_err(anyhow::Error::from)?;
|
||||
let start = ::std::time::Instant::now();
|
||||
let result = $module::command(args).await;
|
||||
let duration = start.elapsed();
|
||||
$crate::telemetry::send($crate::telemetry::CliTrackEvent {
|
||||
command: stringify!([<$module:snake>]).to_string(),
|
||||
sub_command: subcommand_name,
|
||||
success: result.is_ok(),
|
||||
error_message: result.as_ref().err().map(|e| {
|
||||
let msg = format!("{e}");
|
||||
if msg.len() > 256 { msg[..256].to_string() } else { msg }
|
||||
}),
|
||||
duration_ms: duration.as_millis() as u64,
|
||||
cli_version: env!("CARGO_PKG_VERSION"),
|
||||
os: ::std::env::consts::OS,
|
||||
arch: ::std::env::consts::ARCH,
|
||||
is_ci: $crate::config::Configs::env_is_ci(),
|
||||
}).await;
|
||||
result?;
|
||||
},
|
||||
)*
|
||||
_ => {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ mod workspace;
|
|||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
mod telemetry;
|
||||
|
||||
// Generates the commands based on the modules in the commands directory
|
||||
// Specify the modules you want to include in the commands_enum! macro
|
||||
|
|
@ -99,6 +100,8 @@ async fn handle_update_task(
|
|||
async fn main() -> Result<()> {
|
||||
let args = build_args().try_get_matches();
|
||||
let check_updates_handle = if std::io::stdout().is_terminal() {
|
||||
telemetry::show_notice_if_needed();
|
||||
|
||||
let update = UpdateCheck::read().unwrap_or_default();
|
||||
|
||||
if let Some(latest_version) = update.latest_version {
|
||||
|
|
|
|||
112
src/telemetry.rs
Normal file
112
src/telemetry.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
use colored::Colorize;
|
||||
|
||||
use crate::client::{GQLClient, post_graphql};
|
||||
use crate::config::Configs;
|
||||
use crate::gql::mutations::{self, cli_event_track};
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Notices {
|
||||
telemetry_notice_shown: bool,
|
||||
}
|
||||
|
||||
impl Notices {
|
||||
fn path() -> Option<std::path::PathBuf> {
|
||||
dirs::home_dir().map(|h| h.join(".railway/notices.json"))
|
||||
}
|
||||
|
||||
fn read() -> Self {
|
||||
Self::path()
|
||||
.and_then(|p| std::fs::read_to_string(p).ok())
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn write(&self) {
|
||||
if let Some(path) = Self::path() {
|
||||
let _ = serde_json::to_string(self)
|
||||
.ok()
|
||||
.map(|contents| std::fs::write(path, contents));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_notice_if_needed() {
|
||||
if is_telemetry_disabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let notices = Notices::read();
|
||||
if notices.telemetry_notice_shown {
|
||||
return;
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"{}\nYou can opt out by setting RAILWAY_NO_TELEMETRY=1 or DO_NOT_TRACK=1 in your environment.\n{}",
|
||||
"Railway now collects CLI usage data to improve the developer experience.".bold(),
|
||||
format!("Learn more: {}", "https://docs.railway.com/cli/telemetry").dimmed(),
|
||||
);
|
||||
|
||||
Notices {
|
||||
telemetry_notice_shown: true,
|
||||
}
|
||||
.write();
|
||||
}
|
||||
|
||||
pub struct CliTrackEvent {
|
||||
pub command: String,
|
||||
pub sub_command: Option<String>,
|
||||
pub duration_ms: u64,
|
||||
pub success: bool,
|
||||
pub error_message: Option<String>,
|
||||
pub os: &'static str,
|
||||
pub arch: &'static str,
|
||||
pub cli_version: &'static str,
|
||||
pub is_ci: bool,
|
||||
}
|
||||
|
||||
fn env_var_is_truthy(name: &str) -> bool {
|
||||
std::env::var(name)
|
||||
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_telemetry_disabled() -> bool {
|
||||
env_var_is_truthy("DO_NOT_TRACK") || env_var_is_truthy("RAILWAY_NO_TELEMETRY")
|
||||
}
|
||||
|
||||
pub async fn send(event: CliTrackEvent) {
|
||||
if is_telemetry_disabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let configs = match Configs::new() {
|
||||
Ok(c) => c,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let client = match GQLClient::new_authorized(&configs) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let vars = cli_event_track::Variables {
|
||||
input: cli_event_track::CliEventTrackInput {
|
||||
command: event.command,
|
||||
sub_command: event.sub_command,
|
||||
duration_ms: event.duration_ms as i64,
|
||||
success: event.success,
|
||||
error_message: event.error_message,
|
||||
os: event.os.to_string(),
|
||||
arch: event.arch.to_string(),
|
||||
cli_version: event.cli_version.to_string(),
|
||||
is_ci: event.is_ci,
|
||||
},
|
||||
};
|
||||
|
||||
let _ = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(3),
|
||||
post_graphql::<mutations::CliEventTrack, _>(&client, configs.get_backboard(), vars),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Loading…
Reference in a new issue