#![allow(dead_code)]
use opentelemetry::KeyValue;
use opentelemetry::metrics::{Counter, Gauge, Histogram, MeterProvider};
use opentelemetry_otlp::{Protocol, WithExportConfig};
use opentelemetry_sdk::metrics::SdkMeterProvider;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::path::Path;
use std::sync::OnceLock;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct TelemetryConfig {
pub enabled: bool,
pub endpoint: String,
pub protocol: String,
pub logs: bool,
pub metrics: bool,
pub traces: bool,
pub sample_rate: f64,
#[serde(default)]
pub resource: HashMap<String, String>,
}
impl Default for TelemetryConfig {
fn default() -> Self {
Self {
enabled: true,
endpoint: "https://telemetry.jarvy.dev".to_string(),
protocol: "http".to_string(),
logs: true,
metrics: true,
traces: false,
sample_rate: 1.0,
resource: HashMap::new(),
}
}
}
impl TelemetryConfig {
pub fn narrow_with_project(&mut self, project: &TelemetryConfig) -> Option<String> {
if !project.enabled {
self.enabled = false;
}
self.logs = self.logs && project.logs;
self.metrics = self.metrics && project.metrics;
self.traces = self.traces && project.traces;
self.sample_rate = self.sample_rate.min(project.sample_rate);
let default_endpoint = TelemetryConfig::default().endpoint;
if project.endpoint != default_endpoint && project.endpoint != self.endpoint {
let safe = crate::tools::unsupported::sanitize_for_display(&project.endpoint);
return Some(format!(
"[jarvy] project jarvy.toml requests non-default telemetry endpoint ({}). \
Refusing without explicit consent. Inspect jarvy.toml, then re-run with \
JARVY_OTLP_ENDPOINT set to your chosen endpoint (do not copy the value \
above blindly).",
safe
));
}
None
}
pub fn from_env() -> Self {
let mut config = Self::default();
if let Ok(v) = env::var("JARVY_TELEMETRY") {
config.enabled = matches!(v.as_str(), "1" | "true" | "yes");
}
if let Ok(v) = env::var("JARVY_OTLP_ENDPOINT") {
if !v.trim().is_empty() {
config.endpoint = v;
}
}
if let Ok(v) = env::var("JARVY_OTLP_PROTOCOL") {
config.protocol = v;
}
if let Ok(v) = env::var("JARVY_OTLP_LOGS") {
config.logs = matches!(v.as_str(), "1" | "true" | "yes");
}
if let Ok(v) = env::var("JARVY_OTLP_METRICS") {
config.metrics = matches!(v.as_str(), "1" | "true" | "yes");
}
if let Ok(v) = env::var("JARVY_OTLP_TRACES") {
config.traces = matches!(v.as_str(), "1" | "true" | "yes");
}
if let Ok(v) = env::var("JARVY_OTLP_SAMPLE_RATE") {
if let Ok(rate) = v.parse::<f64>() {
config.sample_rate = rate.clamp(0.0, 1.0);
}
}
if crate::sandbox::is_seamless_auto() && env::var("JARVY_TELEMETRY").is_err() {
config.enabled = false;
}
config
}
pub fn is_enabled(&self) -> bool {
self.enabled && (self.logs || self.metrics || self.traces)
}
pub fn validate_endpoint(&self) -> Result<(), String> {
let url = self.endpoint.trim();
if url.is_empty() {
return Err("telemetry endpoint is empty".to_string());
}
if url.starts_with("https://") {
return Ok(());
}
if url.starts_with("http://") {
let host_with_port = url
.trim_start_matches("http://")
.split('/')
.next()
.unwrap_or("");
let host_only = if let Some(rest) = host_with_port.strip_prefix('[') {
rest.split(']').next().unwrap_or("")
} else {
host_with_port.split(':').next().unwrap_or("")
};
const LOOPBACK: &[&str] = &["localhost", "127.0.0.1", "::1"];
if LOOPBACK.contains(&host_only) {
return Ok(());
}
return Err(format!(
"telemetry endpoint `{url}` uses plain HTTP to a non-loopback host; \
use https:// or set the endpoint to localhost"
));
}
Err(format!(
"telemetry endpoint `{url}` must use http:// (loopback only) or https://"
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Source {
Config,
Mcp,
Cli,
Request,
}
impl std::fmt::Display for Source {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Source::Config => write!(f, "config"),
Source::Mcp => write!(f, "mcp"),
Source::Cli => write!(f, "cli"),
Source::Request => write!(f, "request"),
}
}
}
static TELEMETRY: OnceLock<TelemetryState> = OnceLock::new();
struct TelemetryState {
config: TelemetryConfig,
meter_provider: Option<SdkMeterProvider>,
metrics: Option<Metrics>,
}
struct Metrics {
tool_requests: Counter<u64>,
tool_installs: Counter<u64>,
tool_not_supported: Counter<u64>,
errors: Counter<u64>,
hooks_executions: Counter<u64>,
commands: Counter<u64>,
install_duration: Histogram<f64>,
setup_duration: Histogram<f64>,
hooks_duration: Histogram<f64>,
commands_duration: Histogram<f64>,
setup_inventory_size: Gauge<u64>,
}
pub fn init(config: TelemetryConfig) {
let enabled = config.is_enabled();
let _ = TELEMETRY.set(build_telemetry_state(config));
crate::observability::telemetry_gate::set_enabled(enabled);
}
pub fn init_from_env() {
init(TelemetryConfig::from_env());
}
fn build_telemetry_state(config: TelemetryConfig) -> TelemetryState {
if !config.is_enabled() {
return TelemetryState {
config,
meter_provider: None,
metrics: None,
};
}
if let Err(why) = config.validate_endpoint() {
tracing::error!(
event = "telemetry.endpoint.refused",
reason = %why,
"disabling telemetry: configured endpoint rejected"
);
let mut disabled = config;
disabled.enabled = false;
return TelemetryState {
config: disabled,
meter_provider: None,
metrics: None,
};
}
let (meter_provider, metrics) = if config.metrics {
match build_meter_provider(&config) {
Ok(provider) => {
let meter = provider.meter("jarvy");
let metrics = Metrics {
tool_requests: meter
.u64_counter("jarvy.tool.requests")
.with_description("Number of tool installation requests")
.build(),
tool_installs: meter
.u64_counter("jarvy.tool.installs")
.with_description("Number of tool installations by status")
.build(),
tool_not_supported: meter
.u64_counter("jarvy.tool.unsupported")
.with_description(
"Number of unsupported tool requests \
(one per `tool.unsupported` event, sourced from \
config / mcp / cli / request)",
)
.build(),
errors: meter
.u64_counter("jarvy.errors")
.with_description("Number of errors by type")
.build(),
hooks_executions: meter
.u64_counter("jarvy.hooks.executions")
.with_description("Number of hook executions by type and status")
.build(),
commands: meter
.u64_counter("jarvy.commands")
.with_description("Number of command executions")
.build(),
install_duration: meter
.f64_histogram("jarvy.install.duration")
.with_description("Tool installation duration in seconds")
.with_unit("s")
.build(),
setup_duration: meter
.f64_histogram("jarvy.setup.duration")
.with_description("Total setup duration in seconds")
.with_unit("s")
.build(),
hooks_duration: meter
.f64_histogram("jarvy.hooks.duration")
.with_description("Hook execution duration in seconds")
.with_unit("s")
.build(),
commands_duration: meter
.f64_histogram("jarvy.commands.duration")
.with_description("Command execution duration in seconds")
.with_unit("s")
.build(),
setup_inventory_size: meter
.u64_gauge("jarvy.setup.inventory_size")
.with_description(
"Number of tools in the provisioning inventory (security audit)",
)
.build(),
};
(Some(provider), Some(metrics))
}
Err(e) => {
tracing::warn!("Failed to initialize metrics: {}", e);
(None, None)
}
}
} else {
(None, None)
};
TelemetryState {
config,
meter_provider,
metrics,
}
}
fn build_meter_provider(config: &TelemetryConfig) -> Result<SdkMeterProvider, String> {
let endpoint = crate::analytics::resolve_otlp_endpoint(&config.endpoint, "metrics");
let exporter = opentelemetry_otlp::MetricExporter::builder()
.with_http()
.with_protocol(Protocol::HttpBinary)
.with_endpoint(endpoint.as_str())
.build()
.map_err(|e| format!("Failed to build metric exporter: {}", e))?;
let reader = opentelemetry_sdk::metrics::PeriodicReader::builder(exporter)
.with_interval(Duration::from_secs(60))
.build();
Ok(SdkMeterProvider::builder()
.with_reader(reader)
.with_resource(crate::analytics::build_resource())
.build())
}
pub fn shutdown() {
if let Some(state) = TELEMETRY.get() {
if let Some(ref provider) = state.meter_provider {
let _ = provider.shutdown();
}
}
}
pub fn is_enabled() -> bool {
TELEMETRY
.get()
.map(|s| s.config.is_enabled())
.unwrap_or(false)
}
pub fn config() -> Option<&'static TelemetryConfig> {
TELEMETRY.get().map(|s| &s.config)
}
pub fn tool_requested(tool: &str, version: &str, source: Source) {
if !is_enabled() {
return;
}
tracing::info!(
event = "tool.requested",
tool = %tool,
version = %version,
source = %source,
platform = %env::consts::OS,
);
if let Some(state) = TELEMETRY.get() {
if let Some(ref metrics) = state.metrics {
metrics.tool_requests.add(
1,
&[
KeyValue::new("tool", tool.to_string()),
KeyValue::new("source", source.to_string()),
KeyValue::new("platform", env::consts::OS.to_string()),
],
);
}
}
}
pub fn tool_installed(tool: &str, version: &str, package_manager: &str, duration: Duration) {
if !is_enabled() {
return;
}
let duration_ms = duration.as_millis() as u64;
let category = crate::tools::spec::get_tool_category(tool).unwrap_or("uncategorized");
tracing::info!(
event = "tool.installed",
tool = %tool,
version = %version,
category = %category,
package_manager = %package_manager,
duration_ms = %duration_ms,
platform = %env::consts::OS,
);
if let Some(state) = TELEMETRY.get() {
if let Some(ref metrics) = state.metrics {
let attrs = [
KeyValue::new("tool", tool.to_string()),
KeyValue::new("pm", package_manager.to_string()),
KeyValue::new("platform", env::consts::OS.to_string()),
KeyValue::new("status", "success"),
KeyValue::new("category", category.to_string()),
];
metrics.tool_installs.add(1, &attrs);
metrics
.install_duration
.record(duration.as_secs_f64(), &attrs[..3]);
}
}
}
pub fn tool_already_installed(
tool: &str,
install_path: &str,
detection_method: &str,
source: &str,
prompted_user: bool,
) {
if !is_enabled() {
return;
}
let redacted_path = redact_path(install_path);
tracing::info!(
event = "tool.already_installed",
tool = %tool,
install_path = %redacted_path,
detection_method = %detection_method,
source = %source,
prompted_user = %prompted_user,
platform = %env::consts::OS,
);
if let Some(state) = TELEMETRY.get() {
if let Some(ref metrics) = state.metrics {
metrics.tool_installs.add(
1,
&[
KeyValue::new("tool", tool.to_string()),
KeyValue::new("platform", env::consts::OS.to_string()),
KeyValue::new("status", "already_installed"),
KeyValue::new("detection_method", detection_method.to_string()),
KeyValue::new("source", source.to_string()),
KeyValue::new("prompted_user", prompted_user),
],
);
}
}
}
pub fn tool_failed(tool: &str, version: &str, error: &str) {
tool_failed_with_kind(tool, version, "install_command_failed", error);
}
pub fn tool_failed_with_kind(tool: &str, version: &str, error_kind: &str, error: &str) {
if !is_enabled() {
return;
}
let redacted_error = redact_sensitive(error);
let category = crate::tools::spec::get_tool_category(tool).unwrap_or("uncategorized");
tracing::error!(
event = "tool.failed",
tool = %tool,
version = %version,
category = %category,
error_kind = %error_kind,
error = %redacted_error,
platform = %env::consts::OS,
);
if let Some(state) = TELEMETRY.get() {
if let Some(ref metrics) = state.metrics {
metrics.tool_installs.add(
1,
&[
KeyValue::new("tool", tool.to_string()),
KeyValue::new("platform", env::consts::OS.to_string()),
KeyValue::new("status", "failed"),
KeyValue::new("error_kind", error_kind.to_string()),
KeyValue::new("category", category.to_string()),
],
);
metrics
.errors
.add(1, &[KeyValue::new("error_type", "tool_install")]);
}
}
}
pub fn tool_not_supported(tool: &str, _version: Option<&str>, source: Source) {
if !is_enabled() {
return;
}
if let Some(state) = TELEMETRY.get() {
if let Some(ref metrics) = state.metrics {
metrics.tool_not_supported.add(
1,
&[
KeyValue::new("tool", tool.to_string()),
KeyValue::new("source", source.to_string()),
KeyValue::new("platform", env::consts::OS.to_string()),
],
);
}
}
}
pub fn tool_request_explicit(tool: &str, _suggestions: &[String]) -> bool {
if let Some(state) = TELEMETRY.get() {
if let Some(ref metrics) = state.metrics {
metrics.tool_not_supported.add(
1,
&[
KeyValue::new("tool", tool.to_string()),
KeyValue::new("source", Source::Request.to_string()),
KeyValue::new("platform", env::consts::OS.to_string()),
],
);
return true;
}
}
false
}
#[derive(Debug, Clone, Default)]
pub struct SetupSummary {
pub tools_requested: usize,
pub tools_installed: usize,
pub tools_skipped: usize,
pub tools_failed: usize,
pub hooks_run: usize,
pub duration: Duration,
}
pub fn setup_started(tools_count: usize) {
if !is_enabled() {
return;
}
tracing::info!(
event = "setup.started",
tools_count = %tools_count,
platform = %env::consts::OS,
);
}
pub fn setup_completed(summary: &SetupSummary) {
if !is_enabled() {
return;
}
let duration_ms = summary.duration.as_millis() as u64;
tracing::info!(
event = "setup.completed",
tools_requested = %summary.tools_requested,
tools_installed = %summary.tools_installed,
tools_skipped = %summary.tools_skipped,
tools_failed = %summary.tools_failed,
hooks_run = %summary.hooks_run,
duration_ms = %duration_ms,
);
if let Some(state) = TELEMETRY.get() {
if let Some(ref metrics) = state.metrics {
metrics.setup_duration.record(
summary.duration.as_secs_f64(),
&[KeyValue::new("tools_count", summary.tools_requested as i64)],
);
}
}
}
pub fn setup_inventory(
tools: &[(String, String)],
role: Option<&str>,
config_source: &str,
machine_id: Option<&str>,
) {
if !is_enabled() {
return;
}
let tools_str = tools
.iter()
.map(|(name, version)| format!("{}={}", name, version))
.collect::<Vec<_>>()
.join(",");
tracing::info!(
event = "setup.inventory",
tools = %tools_str,
tools_count = %tools.len(),
role = %role.unwrap_or("none"),
config_source = %redact_path(config_source),
machine_id = %machine_id.unwrap_or("unknown"),
platform = %env::consts::OS,
);
if let Some(state) = TELEMETRY.get() {
if let Some(ref metrics) = state.metrics {
metrics.setup_inventory_size.record(
tools.len() as u64,
&[
KeyValue::new("machine_id", machine_id.unwrap_or("unknown").to_string()),
KeyValue::new("platform", env::consts::OS.to_string()),
],
);
}
}
}
pub fn hook_started(hook_name: &str, hook_type: &str, tool: Option<&str>) {
if !is_enabled() {
return;
}
tracing::info!(
event = "hook.started",
hook_name = %hook_name,
hook_type = %hook_type,
tool = %tool.unwrap_or("global"),
);
}
pub fn hook_completed(hook_name: &str, hook_type: &str, duration: Duration, exit_code: i32) {
if !is_enabled() {
return;
}
let duration_ms = duration.as_millis() as u64;
tracing::info!(
event = "hook.completed",
hook_name = %hook_name,
hook_type = %hook_type,
duration_ms = %duration_ms,
exit_code = %exit_code,
);
if let Some(state) = TELEMETRY.get() {
if let Some(ref metrics) = state.metrics {
let attrs = [
KeyValue::new("hook_type", hook_type.to_string()),
KeyValue::new("status", "success"),
];
metrics.hooks_executions.add(1, &attrs);
metrics
.hooks_duration
.record(duration.as_secs_f64(), &[attrs[0].clone()]);
}
}
}
pub fn hook_failed(hook_name: &str, hook_type: &str, error: &str, error_type: &str) {
if !is_enabled() {
return;
}
let redacted_error = redact_sensitive(error);
tracing::error!(
event = "hook.failed",
hook_name = %hook_name,
hook_type = %hook_type,
error = %redacted_error,
error_type = %error_type,
);
if let Some(state) = TELEMETRY.get() {
if let Some(ref metrics) = state.metrics {
metrics.hooks_executions.add(
1,
&[
KeyValue::new("hook_type", hook_type.to_string()),
KeyValue::new("status", "failed"),
],
);
metrics
.errors
.add(1, &[KeyValue::new("error_type", "hook")]);
}
}
}
pub fn hook_timeout(hook_name: &str, hook_type: &str, timeout_secs: u64) {
if !is_enabled() {
return;
}
tracing::error!(
event = "hook.timeout",
hook_name = %hook_name,
hook_type = %hook_type,
timeout_seconds = %timeout_secs,
);
if let Some(state) = TELEMETRY.get() {
if let Some(ref metrics) = state.metrics {
metrics.hooks_executions.add(
1,
&[
KeyValue::new("hook_type", hook_type.to_string()),
KeyValue::new("status", "timeout"),
],
);
}
}
}
pub fn command_executed(command: &str, duration: Duration, success: bool) {
if !is_enabled() {
return;
}
let duration_ms = duration.as_millis() as u64;
let status = if success { "success" } else { "failed" };
tracing::info!(
event = "command.executed",
command = %command,
duration_ms = %duration_ms,
status = %status,
);
if let Some(state) = TELEMETRY.get() {
if let Some(ref metrics) = state.metrics {
let attrs = [
KeyValue::new("command", command.to_string()),
KeyValue::new("status", status),
];
metrics.commands.add(1, &attrs);
metrics
.commands_duration
.record(duration.as_secs_f64(), &[attrs[0].clone()]);
}
}
}
pub fn doctor_issue_found(category: &str, severity: &str, message: &str) {
if !is_enabled() {
return;
}
tracing::info!(
event = "doctor.issue_found",
category = %category,
severity = %severity,
message = %message,
);
}
pub fn search_executed(query: &str, results_count: usize) {
if !is_enabled() {
return;
}
let had_results = results_count > 0;
let query_len_bucket = match query.chars().count() {
0 => "0",
1..=4 => "1-4",
5..=15 => "5-15",
16..=40 => "16-40",
_ => "40+",
};
tracing::info!(
event = "search.executed",
had_results = %had_results,
results_count = %results_count,
query_len_bucket = %query_len_bucket,
);
}
pub fn validate_result(errors_count: usize, warnings_count: usize) {
if !is_enabled() {
return;
}
tracing::info!(
event = "validate.result",
errors_count = %errors_count,
warnings_count = %warnings_count,
);
}
pub fn export_completed(tools_count: usize, format: &str) {
if !is_enabled() {
return;
}
tracing::info!(
event = "export.completed",
tools_count = %tools_count,
format = %format,
);
}
pub fn diff_executed(to_install: usize, to_update: usize, satisfied: usize, unknown: usize) {
if !is_enabled() {
return;
}
tracing::info!(
event = "diff.executed",
to_install = %to_install,
to_update = %to_update,
satisfied = %satisfied,
unknown = %unknown,
);
}
pub fn upgrade_result(upgraded: usize, failed: usize, skipped: usize) {
if !is_enabled() {
return;
}
tracing::info!(
event = "upgrade.result",
upgraded = %upgraded,
failed = %failed,
skipped = %skipped,
);
}
pub fn doctor_completed(issues_count: usize, tools_count: usize, exit_code: i32) {
if !is_enabled() {
return;
}
tracing::info!(
event = "doctor.completed",
issues_count = %issues_count,
tools_count = %tools_count,
exit_code = %exit_code,
);
}
pub fn config_loaded(
source: &str,
tools_count: usize,
has_hooks: bool,
has_env: bool,
has_services: bool,
) {
if !is_enabled() {
return;
}
tracing::info!(
event = "config.loaded",
source = %source,
tools_count = %tools_count,
has_hooks = %has_hooks,
has_env = %has_env,
has_services = %has_services,
);
}
pub fn config_parse_error(file: &str, error: &str) {
if !is_enabled() {
return;
}
let redacted_file = redact_path(file);
let redacted_error = redact_sensitive(error);
tracing::error!(
event = "config.parse_error",
file = %redacted_file,
error = %redacted_error,
);
if let Some(state) = TELEMETRY.get() {
if let Some(ref metrics) = state.metrics {
metrics
.errors
.add(1, &[KeyValue::new("error_type", "config_parse")]);
}
}
}
pub fn service_operation(backend: &str, action: &str, success: bool) {
if !is_enabled() {
return;
}
let status = if success { "success" } else { "failed" };
tracing::info!(
event = "service.operation",
backend = %backend,
action = %action,
status = %status,
);
}
pub fn package_manager_batch_install(
pm: &str,
packages_count: usize,
succeeded: usize,
failed: usize,
duration: Duration,
) {
if !is_enabled() {
return;
}
let duration_ms = duration.as_millis() as u64;
tracing::info!(
event = "package_manager.batch_install",
pm = %pm,
packages_count = %packages_count,
succeeded = %succeeded,
failed = %failed,
duration_ms = %duration_ms,
);
}
pub fn ci_detected(provider: &str, build_id: Option<&str>, branch: Option<&str>) {
if !is_enabled() {
return;
}
tracing::info!(
event = "ci.detected",
provider = %provider,
build_id = %build_id.unwrap_or("unknown"),
branch = %branch.unwrap_or("unknown"),
);
}
pub fn env_dotenv_generated(vars_count: usize, secrets_count: usize) {
if !is_enabled() {
return;
}
tracing::info!(
event = "env.dotenv_generated",
vars_count = %vars_count,
secrets_count = %secrets_count,
);
}
pub fn env_shell_rc_updated(shell: &str, vars_count: usize) {
if !is_enabled() {
return;
}
tracing::info!(
event = "env.shell_rc_updated",
shell = %shell,
vars_count = %vars_count,
);
}
#[macro_export]
macro_rules! telemetry_span {
($name:expr) => {
tracing::info_span!($name)
};
($name:expr, $($field:tt)*) => {
tracing::info_span!($name, $($field)*)
};
}
pub fn span_setup(tools_count: usize) -> tracing::Span {
tracing::info_span!("jarvy.setup", tools_count = tools_count, platform = %env::consts::OS)
}
pub fn span_version_check(tool: &str) -> tracing::Span {
tracing::info_span!("jarvy.version_check", tool = %tool)
}
pub fn span_install(tool: &str, version: &str) -> tracing::Span {
tracing::info_span!("jarvy.install", tool = %tool, version = %version)
}
pub fn span_hook(hook_name: &str, hook_type: &str) -> tracing::Span {
tracing::info_span!("jarvy.hook", hook_name = %hook_name, hook_type = %hook_type)
}
pub fn span_command(command: &str) -> tracing::Span {
tracing::info_span!("jarvy.command", command = %command)
}
pub fn span_service(backend: &str, action: &str) -> tracing::Span {
tracing::info_span!("jarvy.service", backend = %backend, action = %action)
}
static REDACT_HOME_PATH_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
regex::Regex::new(r"(/home/[^/\s]+|/Users/[^/\s]+|C:\\Users\\[^/\\\s]+)")
.expect("static home-path regex must compile")
});
static REDACT_ENV_VALUE_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
regex::Regex::new(r"(API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)=\S+")
.expect("static env-value regex must compile")
});
static HOME_DIR_STRING: std::sync::LazyLock<Option<String>> =
std::sync::LazyLock::new(|| dirs::home_dir().map(|p| p.to_string_lossy().into_owned()));
fn redact_sensitive(s: &str) -> std::borrow::Cow<'_, str> {
let after_home = REDACT_HOME_PATH_RE.replace_all(s, "[HOME]");
match REDACT_ENV_VALUE_RE.replace_all(&after_home, "$1=[REDACTED]") {
std::borrow::Cow::Owned(owned) => std::borrow::Cow::Owned(owned),
std::borrow::Cow::Borrowed(_) => match after_home {
std::borrow::Cow::Borrowed(_) => std::borrow::Cow::Borrowed(s),
std::borrow::Cow::Owned(owned) => std::borrow::Cow::Owned(owned),
},
}
}
pub fn redact_path(path: &str) -> String {
if let Some(home) = HOME_DIR_STRING.as_deref() {
if !home.is_empty() && path.starts_with(home) {
return path.replacen(home, "~", 1);
}
}
path.to_string()
}
pub fn disclosure_shown(trigger: &str) {
if !is_enabled() {
return;
}
tracing::info!(
event = "telemetry.disclosure_shown",
trigger = %trigger,
platform = %env::consts::OS,
);
}
pub fn undecided_nudge_shown() {
if !is_enabled() {
return;
}
tracing::info!(
event = "telemetry.undecided_nudge_shown",
platform = %env::consts::OS,
);
}
pub fn user_decided(content: &str) -> bool {
let Ok(value) = toml::from_str::<toml::Value>(content) else {
return false;
};
let Some(telemetry) = value.get("telemetry").and_then(|t| t.as_table()) else {
return false;
};
telemetry.contains_key("enabled")
}
pub fn now() -> Instant {
Instant::now()
}
pub fn ms(d: Duration) -> u128 {
d.as_millis()
}
pub fn ai_hook_phase_started(agents: usize, hooks_count: usize, scope: &str, dry_run: bool) {
if !is_enabled() {
return;
}
tracing::info!(
event = "ai_hook.phase_started",
agents = %agents,
hooks_count = %hooks_count,
scope = %scope,
dry_run = %dry_run,
);
}
pub fn ai_hook_phase_completed(
applied: usize,
agents_touched: usize,
refused_local: usize,
refused_remote: usize,
failures: usize,
duration: Duration,
) {
if !is_enabled() {
return;
}
let duration_ms = duration.as_millis() as u64;
tracing::info!(
event = "ai_hook.phase_completed",
applied = %applied,
agents_touched = %agents_touched,
refused_local = %refused_local,
refused_remote = %refused_remote,
failures = %failures,
duration_ms = %duration_ms,
);
}
pub fn ai_hook_agent_applied(agent: &str, applied: usize, warnings: usize, settings_path: &Path) {
if !is_enabled() {
return;
}
let redacted_path = redact_path(&settings_path.to_string_lossy());
tracing::info!(
event = "ai_hook.agent_applied",
agent = %agent,
applied = %applied,
warnings = %warnings,
settings_path = %redacted_path,
);
}
pub fn ai_hook_agent_failed(agent: &str, error_type: &str) {
if !is_enabled() {
return;
}
tracing::warn!(
event = "ai_hook.agent_failed",
agent = %agent,
error_type = %error_type,
);
}
pub fn ai_hook_provisioned(agent: &str, hook_name: &str, library_source: Option<&str>) {
if !is_enabled() {
return;
}
tracing::info!(
event = "ai_hook.provisioned",
agent = %agent,
hook_name = %hook_name,
library_source = %library_source.unwrap_or("custom"),
);
}
pub fn ai_hook_custom_refused_summary(local_count: usize, remote_count: usize) {
if !is_enabled() {
return;
}
tracing::info!(
event = "ai_hook.custom_refused_summary",
local_count = %local_count,
remote_count = %remote_count,
);
}
pub fn ai_hook_check_completed(agents_checked: usize, drifted_agents: usize) {
if !is_enabled() {
return;
}
tracing::info!(
event = "ai_hook.check_completed",
agents_checked = %agents_checked,
drifted_agents = %drifted_agents,
);
}
pub fn ai_hook_windows_auto_translated(agent: &str, hook_name: &str) {
if !is_enabled() {
return;
}
tracing::info!(
event = "ai_hook.windows_auto_translated",
agent = %agent,
hook_name = %hook_name,
);
}
pub fn mcp_register_phase_started(agents: usize, servers_count: usize, scope: &str) {
if !is_enabled() {
return;
}
tracing::info!(
event = "mcp_register.phase_started",
agents = %agents,
servers_count = %servers_count,
scope = %scope,
);
}
pub fn mcp_register_phase_completed(
applied: usize,
agents_touched: usize,
refused_local: usize,
refused_remote: usize,
failures: usize,
duration: Duration,
) {
if !is_enabled() {
return;
}
tracing::info!(
event = "mcp_register.phase_completed",
applied = %applied,
agents_touched = %agents_touched,
refused_local = %refused_local,
refused_remote = %refused_remote,
failures = %failures,
duration_ms = %duration.as_millis(),
);
}
pub fn mcp_register_agent_applied(agent: &str, applied: usize, settings_path: &Path) {
if !is_enabled() {
return;
}
let redacted_path = redact_path(&settings_path.to_string_lossy());
tracing::info!(
event = "mcp_register.agent_applied",
agent = %agent,
applied = %applied,
settings_path = %redacted_path,
);
}
pub fn mcp_register_agent_failed(agent: &str, error_type: &str) {
if !is_enabled() {
return;
}
tracing::warn!(
event = "mcp_register.agent_failed",
agent = %agent,
error_type = %error_type,
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_telemetry_config_default() {
let config = TelemetryConfig::default();
assert!(config.enabled);
assert!(
config.is_enabled(),
"default must be effectively on (enabled + at least one signal)"
);
assert_eq!(config.endpoint, "https://telemetry.jarvy.dev");
assert_eq!(config.protocol, "http");
assert!(config.logs);
assert!(config.metrics);
assert!(!config.traces);
assert_eq!(config.sample_rate, 1.0);
}
#[test]
fn test_telemetry_config_is_enabled() {
let mut config = TelemetryConfig::default();
assert!(config.is_enabled());
config.enabled = false;
assert!(!config.is_enabled());
config.enabled = true;
assert!(config.is_enabled());
config.logs = false;
config.metrics = false;
config.traces = false;
assert!(!config.is_enabled()); }
#[test]
fn validate_endpoint_accepts_https_and_loopback() {
let mut config = TelemetryConfig {
endpoint: "https://otel.corp.com:4318".to_string(),
..TelemetryConfig::default()
};
assert!(config.validate_endpoint().is_ok());
config.endpoint = "http://localhost:4318".to_string();
assert!(config.validate_endpoint().is_ok());
config.endpoint = "http://127.0.0.1:4318".to_string();
assert!(config.validate_endpoint().is_ok());
config.endpoint = "http://[::1]:4318".to_string();
assert!(config.validate_endpoint().is_ok());
}
#[test]
fn validate_endpoint_rejects_plain_http_remote() {
let config = TelemetryConfig {
endpoint: "http://attacker.tld:4318".to_string(),
..TelemetryConfig::default()
};
let err = config.validate_endpoint().unwrap_err();
assert!(err.contains("plain HTTP"), "got {err:?}");
}
#[test]
fn validate_endpoint_rejects_unknown_scheme() {
let mut config = TelemetryConfig {
endpoint: "ftp://otel.corp.com".to_string(),
..TelemetryConfig::default()
};
assert!(config.validate_endpoint().is_err());
config.endpoint = "".to_string();
assert!(config.validate_endpoint().is_err());
}
#[test]
fn test_source_display() {
assert_eq!(Source::Config.to_string(), "config");
assert_eq!(Source::Mcp.to_string(), "mcp");
assert_eq!(Source::Cli.to_string(), "cli");
assert_eq!(Source::Request.to_string(), "request");
}
fn user_telemetry_on() -> TelemetryConfig {
TelemetryConfig {
enabled: true,
endpoint: "https://telemetry.jarvy.dev".to_string(),
protocol: "http".to_string(),
logs: true,
metrics: true,
traces: true,
sample_rate: 1.0,
resource: HashMap::new(),
}
}
fn project_telemetry_default() -> TelemetryConfig {
TelemetryConfig::default()
}
#[test]
fn narrow_with_project_can_disable_when_user_enabled() {
let mut user = user_telemetry_on();
let project = TelemetryConfig {
enabled: false,
..project_telemetry_default()
};
let warning = user.narrow_with_project(&project);
assert!(warning.is_none(), "no warning expected on plain disable");
assert!(!user.enabled, "project must be able to disable");
}
#[test]
fn narrow_with_project_cannot_enable_when_user_disabled() {
let mut user = TelemetryConfig {
enabled: false,
..TelemetryConfig::default()
};
let project = TelemetryConfig {
enabled: true,
..TelemetryConfig::default()
};
let _ = user.narrow_with_project(&project);
assert!(
!user.enabled,
"project enabling must not broaden user opt-out"
);
}
#[test]
fn narrow_with_project_ands_signal_flags() {
let mut user = user_telemetry_on();
let project = TelemetryConfig {
logs: false,
metrics: false,
traces: true, ..project_telemetry_default()
};
let _ = user.narrow_with_project(&project);
assert!(!user.logs, "project can turn logs off");
assert!(!user.metrics, "project can turn metrics off");
assert!(user.traces, "project cannot turn traces on (AND-narrowing)");
}
#[test]
fn narrow_with_project_ands_signal_cannot_broaden() {
let mut user = TelemetryConfig {
metrics: false,
..user_telemetry_on()
};
let project = TelemetryConfig {
metrics: true,
..project_telemetry_default()
};
let _ = user.narrow_with_project(&project);
assert!(!user.metrics, "AND-narrowing must not flip false to true");
}
#[test]
fn narrow_with_project_sample_rate_takes_min() {
let mut user = TelemetryConfig {
sample_rate: 0.5,
..user_telemetry_on()
};
let project = TelemetryConfig {
sample_rate: 0.1,
..project_telemetry_default()
};
let _ = user.narrow_with_project(&project);
assert!(
(user.sample_rate - 0.1).abs() < 1e-9,
"min must apply: {}",
user.sample_rate
);
let mut user2 = TelemetryConfig {
sample_rate: 0.1,
..user_telemetry_on()
};
let project2 = TelemetryConfig {
sample_rate: 0.9,
..project_telemetry_default()
};
let _ = user2.narrow_with_project(&project2);
assert!(
(user2.sample_rate - 0.1).abs() < 1e-9,
"project cannot raise sample_rate"
);
}
#[test]
fn narrow_with_project_refuses_endpoint_override_with_warning() {
let mut user = user_telemetry_on();
let project = TelemetryConfig {
endpoint: "https://attacker.tld/collect".to_string(),
..project_telemetry_default()
};
let warning = user
.narrow_with_project(&project)
.expect("endpoint mismatch must produce a warning");
assert!(
user.endpoint.starts_with("https://telemetry.jarvy.dev"),
"user endpoint must be untouched: {}",
user.endpoint
);
assert!(
warning.contains("non-default telemetry endpoint"),
"warning text contract: {}",
warning
);
assert!(
warning.contains("JARVY_OTLP_ENDPOINT"),
"warning must point at the env-var escape hatch"
);
}
#[test]
fn narrow_with_project_endpoint_warning_sanitizes_attacker_bytes() {
let mut user = user_telemetry_on();
let project = TelemetryConfig {
endpoint: "https://evil.tld\x1b[2J\x1b[Hfake".to_string(),
..project_telemetry_default()
};
let warning = user.narrow_with_project(&project).unwrap();
assert!(
!warning.contains('\x1b'),
"ANSI escape must not survive: {:?}",
warning
);
}
#[test]
fn narrow_with_project_matching_endpoint_no_warning() {
let mut user = TelemetryConfig {
endpoint: "https://example.com/otlp".to_string(),
..user_telemetry_on()
};
let project = TelemetryConfig {
endpoint: "https://example.com/otlp".to_string(),
..project_telemetry_default()
};
let warning = user.narrow_with_project(&project);
assert!(warning.is_none(), "matching endpoint must not warn");
}
#[test]
fn tool_request_explicit_returns_false_when_telemetry_uninit() {
let fired = tool_request_explicit("nonexistent-tool", &[]);
assert!(
!fired,
"uninitialized telemetry must drop the metric and return false"
);
}
#[test]
fn narrow_with_project_default_endpoint_no_warning() {
let mut user = TelemetryConfig {
endpoint: "https://example.com/otlp".to_string(),
..user_telemetry_on()
};
let project = project_telemetry_default(); let warning = user.narrow_with_project(&project);
assert!(warning.is_none(), "default endpoint must not warn");
}
#[test]
fn test_redact_sensitive() {
let input = "Error at /home/user/project: API_KEY=secret123";
let result = redact_sensitive(input);
assert!(result.contains("[HOME]"));
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("user"));
assert!(!result.contains("secret123"));
}
#[test]
fn test_redact_path() {
let home = dirs::home_dir().map(|p| p.to_string_lossy().to_string());
if let Some(home) = home {
let path = format!("{}/some/path", home);
let result = redact_path(&path);
assert!(result.starts_with("~"));
assert!(!result.contains(&home));
}
}
#[test]
fn user_decided_recognizes_explicit_enabled() {
assert!(user_decided("[telemetry]\nenabled = true\n"));
assert!(user_decided("[telemetry]\nenabled = false\n"));
assert!(user_decided("[telemetry]\nenabled=true\n"));
assert!(user_decided(
"[telemetry]\nendpoint = \"https://example\"\nenabled = false\n"
));
}
#[test]
fn user_decided_undecided_when_telemetry_missing() {
assert!(!user_decided(""));
assert!(!user_decided("[settings]\nfingerprint = \"abc\"\n"));
assert!(!user_decided(
"[telemetry]\nendpoint = \"https://example\"\n"
));
}
#[test]
fn user_decided_section_aware_not_string_match() {
assert!(!user_decided(
"[mcp_register]\nenabled = true\n[telemetry]\nendpoint = \"https://example\"\n"
));
assert!(!user_decided("[drift]\nenabled = true\n"));
}
#[test]
fn user_decided_returns_false_on_malformed_toml() {
assert!(!user_decided("not [valid toml"));
}
#[test]
fn test_setup_summary_default() {
let summary = SetupSummary::default();
assert_eq!(summary.tools_requested, 0);
assert_eq!(summary.tools_installed, 0);
assert_eq!(summary.tools_skipped, 0);
assert_eq!(summary.tools_failed, 0);
assert_eq!(summary.hooks_run, 0);
}
}