Skip to content
Open
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
53 changes: 50 additions & 3 deletions src/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ use std::sync::Mutex;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{Duration, Instant};

const TELEMETRY_ENDPOINT: &str = "https://jcode-telemetry.jeremyhuang55555.workers.dev/v1/event";
// Telemetry endpoint is now resolved at runtime — see telemetry_endpoint().
// Default in this fork is no endpoint, which disables telemetry. Set
// JCODE_TELEMETRY_ENDPOINT or write `endpoint = "..."` to
// `${JCODE_HOME:-~/.jcode}/telemetry.toml` to enable.
const ASYNC_SEND_TIMEOUT: Duration = Duration::from_secs(5);
const BLOCKING_INSTALL_TIMEOUT: Duration = Duration::from_millis(1200);
const BLOCKING_LIFECYCLE_TIMEOUT: Duration = Duration::from_millis(800);
Expand Down Expand Up @@ -265,7 +268,47 @@ pub fn is_enabled() -> bool {
logging::debug("telemetry disabled by no_telemetry marker");
return false;
}
true
// Opt-in by default in this fork: a telemetry endpoint must be configured
// (env var or config file) before any event is sent. See
// `telemetry_endpoint` for the resolution order.
telemetry_endpoint().is_some()
}

#[derive(Debug, serde::Deserialize, Default)]
struct TelemetryConfig {
endpoint: Option<String>,
}

/// Resolve the telemetry endpoint at runtime.
///
/// Resolution order:
/// 1. `JCODE_TELEMETRY_ENDPOINT` environment variable (highest precedence).
/// 2. `endpoint = "..."` in `${JCODE_HOME:-~/.jcode}/telemetry.toml`.
/// 3. `None` — telemetry is disabled (the fork's default).
///
/// Empty / whitespace-only values are treated as `None` so that explicitly
/// setting `JCODE_TELEMETRY_ENDPOINT=""` reliably disables telemetry without
/// needing the separate `JCODE_NO_TELEMETRY` knob.
fn telemetry_endpoint() -> Option<String> {
if let Ok(value) = std::env::var("JCODE_TELEMETRY_ENDPOINT") {
let trimmed = value.trim();
return if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
};
}
let dir = storage::jcode_dir().ok()?;
let path = dir.join("telemetry.toml");
let contents = std::fs::read_to_string(&path).ok()?;
let config: TelemetryConfig = toml::from_str(&contents).unwrap_or_default();
let endpoint = config.endpoint?;
let trimmed = endpoint.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}

fn telemetry_envelope() -> (u32, String, bool, bool, bool) {
Expand Down Expand Up @@ -793,6 +836,10 @@ pub fn record_command_family(command: &str) {
}

fn post_payload(payload: serde_json::Value, timeout: Duration) -> bool {
let endpoint = match telemetry_endpoint() {
Some(value) => value,
None => return false,
};
let client = match reqwest::blocking::Client::builder()
.user_agent(crate::provider::JCODE_USER_AGENT)
.timeout(timeout)
Expand All @@ -804,7 +851,7 @@ fn post_payload(payload: serde_json::Value, timeout: Duration) -> bool {
return false;
}
};
match client.post(TELEMETRY_ENDPOINT).json(&payload).send() {
match client.post(&endpoint).json(&payload).send() {
Ok(response) => match response.error_for_status() {
Ok(_) => true,
Err(err) => {
Expand Down
87 changes: 87 additions & 0 deletions src/telemetry/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,90 @@ fn test_install_marker_tracks_current_telemetry_id() {
crate::env::remove_var("JCODE_HOME");
}
}

// ---------------------------------------------------------------------------
// Regression tests for issue #73 / upstream PR #77 — telemetry endpoint is
// now resolved at runtime and absent by default in this fork.
// ---------------------------------------------------------------------------

#[test]
fn telemetry_endpoint_returns_none_without_config() {
let _lock = lock_test_env();
let prev_env = std::env::var_os("JCODE_TELEMETRY_ENDPOINT");
let prev_home = std::env::var_os("JCODE_HOME");
crate::env::remove_var("JCODE_TELEMETRY_ENDPOINT");
let temp = tempfile::TempDir::new().expect("create temp dir");
crate::env::set_var("JCODE_HOME", temp.path());

assert!(
super::telemetry_endpoint().is_none(),
"no env, no telemetry.toml → telemetry must be disabled by default"
);
assert!(!super::is_enabled());

if let Some(prev) = prev_env {
crate::env::set_var("JCODE_TELEMETRY_ENDPOINT", prev);
}
if let Some(prev) = prev_home {
crate::env::set_var("JCODE_HOME", prev);
} else {
crate::env::remove_var("JCODE_HOME");
}
}

#[test]
fn telemetry_endpoint_picks_up_env_var() {
let _lock = lock_test_env();
let prev_env = std::env::var_os("JCODE_TELEMETRY_ENDPOINT");
crate::env::set_var(
"JCODE_TELEMETRY_ENDPOINT",
"https://example.com/telemetry/v1",
);

assert_eq!(
super::telemetry_endpoint().as_deref(),
Some("https://example.com/telemetry/v1")
);
assert!(super::is_enabled());

// Empty value disables telemetry without the separate JCODE_NO_TELEMETRY knob.
crate::env::set_var("JCODE_TELEMETRY_ENDPOINT", " ");
assert!(super::telemetry_endpoint().is_none());
assert!(!super::is_enabled());

if let Some(prev) = prev_env {
crate::env::set_var("JCODE_TELEMETRY_ENDPOINT", prev);
} else {
crate::env::remove_var("JCODE_TELEMETRY_ENDPOINT");
}
}

#[test]
fn telemetry_endpoint_picks_up_telemetry_toml() {
let _lock = lock_test_env();
let prev_env = std::env::var_os("JCODE_TELEMETRY_ENDPOINT");
let prev_home = std::env::var_os("JCODE_HOME");
crate::env::remove_var("JCODE_TELEMETRY_ENDPOINT");
let temp = tempfile::TempDir::new().expect("create temp dir");
crate::env::set_var("JCODE_HOME", temp.path());
std::fs::write(
temp.path().join("telemetry.toml"),
"endpoint = \"https://collector.example/v1/event\"\n",
)
.expect("write telemetry.toml");

assert_eq!(
super::telemetry_endpoint().as_deref(),
Some("https://collector.example/v1/event")
);
assert!(super::is_enabled());

if let Some(prev) = prev_env {
crate::env::set_var("JCODE_TELEMETRY_ENDPOINT", prev);
}
if let Some(prev) = prev_home {
crate::env::set_var("JCODE_HOME", prev);
} else {
crate::env::remove_var("JCODE_HOME");
}
}