-
Notifications
You must be signed in to change notification settings - Fork 60
Route telemetry context through CLI, SDK, and cloud #1033
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
Changes from all commits
f9f18f6
6e5d793
0707739
c1d79d3
96c78fd
e27ef01
b3cb37d
efa0d8c
4422148
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # Trajectory: Rope telemetry through CLI SDK and hosted backend | ||
|
|
||
| > **Status:** ✅ Completed | ||
| > **Confidence:** 86% | ||
| > **Started:** June 3, 2026 at 05:42 AM | ||
| > **Completed:** June 3, 2026 at 05:58 AM | ||
|
|
||
| --- | ||
|
|
||
| ## Summary | ||
|
|
||
| Routed telemetry context through the CLI, SDK, cloud client, Rust broker, and hosted relaycast engine. Added app/surface/harness common properties, minimal SDK method/workflow events, enabled cloud request identity headers only when telemetry is enabled, and made hosted relaycast prefer Agent Relay anonymous ids for PostHog distinct ids. Verified telemetry, cloud, SDK, CLI, broker, and relaycast engine checks. | ||
|
|
||
| **Approach:** Standard approach | ||
|
|
||
| --- | ||
|
|
||
| ## Key Decisions | ||
|
|
||
| ### Propagated Agent Relay telemetry context through child process env and cloud request headers | ||
|
|
||
| - **Chose:** Propagated Agent Relay telemetry context through child process env and cloud request headers | ||
| - **Reasoning:** CLI owns telemetry preferences and anonymous id creation, so cloud requests only include identity, surface, client version, and harness headers when telemetry is enabled and a PostHog key is configured. | ||
|
|
||
| ### Hosted relaycast telemetry prefers Agent Relay anonymous id | ||
|
|
||
| - **Chose:** Hosted relaycast telemetry prefers Agent Relay anonymous id | ||
| - **Reasoning:** When the hosted backend receives X-Agent-Relay-Anonymous-Id, using it as PostHog distinctId ties CLI, SDK, and server-side events together while preserving workspace_id as an event property. | ||
|
|
||
| ### Kept SDK telemetry minimal | ||
|
|
||
| - **Chose:** Kept SDK telemetry minimal | ||
| - **Reasoning:** SDK events capture stable method/workflow names, success, duration, and error class only, avoiding user content, paths, payloads, tokens, URLs, and argument values. | ||
|
|
||
| --- | ||
|
|
||
| ## Chapters | ||
|
|
||
| ### 1. Work | ||
|
|
||
| _Agent: default_ | ||
|
|
||
| - Propagated Agent Relay telemetry context through child process env and cloud request headers: Propagated Agent Relay telemetry context through child process env and cloud request headers | ||
| - Hosted relaycast telemetry prefers Agent Relay anonymous id: Hosted relaycast telemetry prefers Agent Relay anonymous id | ||
| - Kept SDK telemetry minimal: Kept SDK telemetry minimal | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| { | ||
| "id": "traj_6kef2z1redhb", | ||
| "version": 1, | ||
| "task": { | ||
| "title": "Rope telemetry through CLI SDK and hosted backend" | ||
| }, | ||
| "status": "completed", | ||
| "startedAt": "2026-06-03T09:42:04.363Z", | ||
| "completedAt": "2026-06-03T09:58:39.713Z", | ||
| "agents": [ | ||
| { | ||
| "name": "default", | ||
| "role": "lead", | ||
| "joinedAt": "2026-06-03T09:55:45.392Z" | ||
| } | ||
| ], | ||
| "chapters": [ | ||
| { | ||
| "id": "chap_5vw63ytf4i4y", | ||
| "title": "Work", | ||
| "agentName": "default", | ||
| "startedAt": "2026-06-03T09:55:45.392Z", | ||
| "endedAt": "2026-06-03T09:58:39.713Z", | ||
| "events": [ | ||
| { | ||
| "ts": 1780480545393, | ||
| "type": "decision", | ||
| "content": "Propagated Agent Relay telemetry context through child process env and cloud request headers: Propagated Agent Relay telemetry context through child process env and cloud request headers", | ||
| "raw": { | ||
| "question": "Propagated Agent Relay telemetry context through child process env and cloud request headers", | ||
| "chosen": "Propagated Agent Relay telemetry context through child process env and cloud request headers", | ||
| "alternatives": [], | ||
| "reasoning": "CLI owns telemetry preferences and anonymous id creation, so cloud requests only include identity, surface, client version, and harness headers when telemetry is enabled and a PostHog key is configured." | ||
| }, | ||
| "significance": "high" | ||
| }, | ||
| { | ||
| "ts": 1780480545552, | ||
| "type": "decision", | ||
| "content": "Hosted relaycast telemetry prefers Agent Relay anonymous id: Hosted relaycast telemetry prefers Agent Relay anonymous id", | ||
| "raw": { | ||
| "question": "Hosted relaycast telemetry prefers Agent Relay anonymous id", | ||
| "chosen": "Hosted relaycast telemetry prefers Agent Relay anonymous id", | ||
| "alternatives": [], | ||
| "reasoning": "When the hosted backend receives X-Agent-Relay-Anonymous-Id, using it as PostHog distinctId ties CLI, SDK, and server-side events together while preserving workspace_id as an event property." | ||
| }, | ||
| "significance": "high" | ||
| }, | ||
| { | ||
| "ts": 1780480643216, | ||
| "type": "decision", | ||
| "content": "Kept SDK telemetry minimal: Kept SDK telemetry minimal", | ||
| "raw": { | ||
| "question": "Kept SDK telemetry minimal", | ||
| "chosen": "Kept SDK telemetry minimal", | ||
| "alternatives": [], | ||
| "reasoning": "SDK events capture stable method/workflow names, success, duration, and error class only, avoiding user content, paths, payloads, tokens, URLs, and argument values." | ||
| }, | ||
| "significance": "high" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "retrospective": { | ||
| "summary": "Routed telemetry context through the CLI, SDK, cloud client, Rust broker, and hosted relaycast engine. Added app/surface/harness common properties, minimal SDK method/workflow events, enabled cloud request identity headers only when telemetry is enabled, and made hosted relaycast prefer Agent Relay anonymous ids for PostHog distinct ids. Verified telemetry, cloud, SDK, CLI, broker, and relaycast engine checks.", | ||
| "approach": "Standard approach", | ||
| "confidence": 0.86 | ||
| }, | ||
| "commits": [], | ||
| "filesChanged": [], | ||
| "projectId": "AgentWorkforce/relay", | ||
| "tags": [], | ||
| "_trace": { | ||
| "startRef": "30d18e9ef73995f6c3ac4044f59d9261d17875f8", | ||
| "endRef": "30d18e9ef73995f6c3ac4044f59d9261d17875f8" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,8 @@ use tokio::sync::mpsc; | |
| /// binaries report to the production PostHog project. | ||
| const POSTHOG_API_KEY: Option<&str> = option_env!("AGENT_RELAY_POSTHOG_KEY"); | ||
| const POSTHOG_HOST: &str = "https://us.i.posthog.com"; | ||
| const UNKNOWN_ORCHESTRATOR_HARNESS: &str = "unknown"; | ||
| const ORCHESTRATOR_HARNESS_ENV: &str = "AGENT_RELAY_ORCHESTRATOR_HARNESS"; | ||
|
|
||
| /// Returns the configured PostHog key iff it's non-empty. Empty strings are | ||
| /// treated the same as "unset" so an accidentally-blank secret doesn't trip | ||
|
|
@@ -297,6 +299,144 @@ fn env_nonempty(key: &str) -> Option<String> { | |
| }) | ||
| } | ||
|
|
||
| fn sanitize_orchestrator_harness(raw: &str) -> Option<String> { | ||
| let trimmed = raw.trim(); | ||
| if trimmed.is_empty() { | ||
| return None; | ||
| } | ||
| if !trimmed.chars().all(|ch| { | ||
| ch.is_ascii_alphanumeric() | ||
| || matches!( | ||
| ch, | ||
| ' ' | '.' | '_' | '-' | '/' | '(' | ')' | ':' | '=' | ';' | ',' | '+' | ||
| ) | ||
| }) { | ||
| return None; | ||
| } | ||
| Some(trimmed.chars().take(120).collect::<String>().to_lowercase()) | ||
| } | ||
|
|
||
| fn infer_harness_from_command(command: &str) -> Option<&'static str> { | ||
| let lower = command.to_lowercase(); | ||
| let normalized = lower.replace('\\', "/"); | ||
| let base = normalized | ||
| .rsplit('/') | ||
| .next() | ||
| .unwrap_or(normalized.as_str()) | ||
| .trim_end_matches(".exe"); | ||
| let base = base | ||
| .strip_suffix(".cmd") | ||
| .or_else(|| base.strip_suffix(".bat")) | ||
| .unwrap_or(base); | ||
|
|
||
| if base == "claude" || lower.contains("claude-code") { | ||
| return Some("claude-code"); | ||
| } | ||
| if base == "codex" || normalized.contains("/codex") { | ||
| return Some("codex"); | ||
| } | ||
| if base == "cursor" || base == "cursor-agent" || lower.contains("cursor") { | ||
| return Some("cursor"); | ||
| } | ||
| if base == "gemini" || base == "gemini-cli" || lower.contains("gemini-cli") { | ||
| return Some("gemini-cli"); | ||
| } | ||
| if base == "aider" || lower.contains("aider") { | ||
| return Some("aider"); | ||
| } | ||
| if base == "opencode" || lower.contains("opencode") { | ||
| return Some("opencode"); | ||
| } | ||
| if base == "goose" || lower.contains("goose") { | ||
| return Some("goose"); | ||
| } | ||
| if base == "droid" || lower.contains("droid") { | ||
| return Some("droid"); | ||
| } | ||
| if base == "amp" || normalized.contains("/amp") { | ||
| return Some("amp"); | ||
| } | ||
| if lower.contains("copilot") { | ||
| return Some("github-copilot"); | ||
| } | ||
| if base == "zed" || lower.contains("zed") { | ||
| return Some("zed"); | ||
|
Comment on lines
+362
to
+363
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: The broker's harness inference also classifies any command containing Severity Level: Major
|
||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| #[cfg(unix)] | ||
| fn lookup_process_info(pid: i32) -> Option<(i32, String)> { | ||
| if pid <= 0 { | ||
| return None; | ||
| } | ||
| let output = std::process::Command::new("ps") | ||
| .args(["-o", "ppid=", "-o", "comm=", "-p", &pid.to_string()]) | ||
| .output() | ||
| .ok()?; | ||
| if !output.status.success() { | ||
| return None; | ||
| } | ||
| let stdout = String::from_utf8(output.stdout).ok()?; | ||
| let mut parts = stdout.split_whitespace(); | ||
| let ppid = parts.next()?.parse::<i32>().ok()?; | ||
| let command = parts.collect::<Vec<_>>().join(" "); | ||
| if command.is_empty() { | ||
| None | ||
| } else { | ||
| Some((ppid, command)) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(unix)] | ||
| fn detect_process_orchestrator_harness() -> Option<String> { | ||
| use std::collections::HashSet; | ||
|
|
||
| let mut pid = nix::unistd::getppid().as_raw(); | ||
| let mut seen = HashSet::new(); | ||
|
|
||
| for _ in 0..8 { | ||
| if pid <= 0 || !seen.insert(pid) { | ||
| break; | ||
| } | ||
| let Some((ppid, command)) = lookup_process_info(pid) else { | ||
| break; | ||
| }; | ||
| if let Some(harness) = infer_harness_from_command(&command) { | ||
| return Some(harness.to_string()); | ||
| } | ||
| if ppid == pid { | ||
| break; | ||
| } | ||
| pid = ppid; | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| #[cfg(not(unix))] | ||
| fn detect_process_orchestrator_harness() -> Option<String> { | ||
| None | ||
| } | ||
|
|
||
| fn detect_orchestrator_harness() -> String { | ||
| for key in [ | ||
|
Comment on lines
+423
to
+424
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the CLI, We should check if telemetry is disabled via environment variables ( fn detect_orchestrator_harness() -> String {
if let Ok(val) = std::env::var("AGENT_RELAY_TELEMETRY_DISABLED").or_else(|_| std::env::var("DO_NOT_TRACK")) {
if val == "1" || val.to_lowercase() == "true" {
return UNKNOWN_ORCHESTRATOR_HARNESS.to_string();
}
}
for key in [ |
||
| ORCHESTRATOR_HARNESS_ENV, | ||
| "RELAYCAST_HARNESS", | ||
| "X_RELAYCAST_HARNESS", | ||
| ] { | ||
| if let Ok(value) = std::env::var(key) { | ||
| if let Some(harness) = sanitize_orchestrator_harness(&value) { | ||
| return harness; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| detect_process_orchestrator_harness() | ||
| .unwrap_or_else(|| UNKNOWN_ORCHESTRATOR_HARNESS.to_string()) | ||
| } | ||
|
|
||
| /// Best-effort OS release string for telemetry tagging. Shells out to | ||
| /// `uname -r` on unix (broker is unix-only anyway); returns `None` on | ||
| /// failure so we just omit the property rather than risking a crash. | ||
|
|
@@ -344,6 +484,8 @@ pub struct TelemetryClient { | |
| /// OS release string (best-effort via `uname -r`, empty on failure / | ||
| /// platforms where that isn't meaningful). | ||
| os_version: Option<String>, | ||
| /// Harness or agent CLI that appears to be driving Agent Relay. | ||
| orchestrator_harness: String, | ||
| } | ||
|
|
||
| #[derive(Debug, Serialize)] | ||
|
|
@@ -372,6 +514,7 @@ impl TelemetryClient { | |
| cli_version: None, | ||
| sdk_version: None, | ||
| os_version: None, | ||
| orchestrator_harness: UNKNOWN_ORCHESTRATOR_HARNESS.to_string(), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -419,6 +562,7 @@ impl TelemetryClient { | |
| cli_version: env_nonempty("AGENT_RELAY_CLI_VERSION"), | ||
| sdk_version: env_nonempty("AGENT_RELAY_SDK_VERSION"), | ||
| os_version: detect_os_version(), | ||
| orchestrator_harness: detect_orchestrator_harness(), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -444,6 +588,12 @@ impl TelemetryClient { | |
| // as a back-compat alias that mirrors `broker_version` here. | ||
| if let Some(obj) = props.as_object_mut() { | ||
| let broker_version = crate::util::version::broker_version(); | ||
| obj.insert("app".to_string(), json!("broker")); | ||
| obj.insert("surface".to_string(), json!("broker")); | ||
| obj.insert( | ||
| "orchestrator_harness".to_string(), | ||
| json!(self.orchestrator_harness.as_str()), | ||
| ); | ||
| obj.insert("agent_relay_version".to_string(), json!(broker_version)); | ||
| obj.insert("broker_version".to_string(), json!(broker_version)); | ||
| if let Some(ref v) = self.cli_version { | ||
|
|
@@ -605,6 +755,7 @@ mod tests { | |
| cli_version: None, | ||
| sdk_version: None, | ||
| os_version: None, | ||
| orchestrator_harness: UNKNOWN_ORCHESTRATOR_HARNESS.to_string(), | ||
| }; | ||
| assert!(!client.is_enabled()); | ||
| client.track(TelemetryEvent::BrokerStart); | ||
|
|
@@ -688,6 +839,33 @@ mod tests { | |
| std::env::remove_var("AGENT_RELAY_TEST_TELEMETRY_SET"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn sanitize_orchestrator_harness_normalizes_safe_values() { | ||
| assert_eq!( | ||
| sanitize_orchestrator_harness(" Codex CLI "), | ||
| Some("codex cli".to_string()) | ||
| ); | ||
| assert_eq!(sanitize_orchestrator_harness("bad\nvalue"), None); | ||
| assert_eq!(sanitize_orchestrator_harness(""), None); | ||
| } | ||
|
|
||
| #[test] | ||
| fn infer_harness_from_command_recognizes_known_parents() { | ||
| assert_eq!( | ||
| infer_harness_from_command("/usr/local/bin/codex"), | ||
| Some("codex") | ||
| ); | ||
| assert_eq!( | ||
| infer_harness_from_command("/Applications/Cursor.app/Contents/MacOS/Cursor"), | ||
| Some("cursor") | ||
| ); | ||
| assert_eq!( | ||
| infer_harness_from_command(r"C:\Users\will\AppData\Roaming\npm\gemini.cmd"), | ||
| Some("gemini-cli") | ||
| ); | ||
| assert_eq!(infer_harness_from_command("/usr/bin/zsh"), None); | ||
| } | ||
|
|
||
| #[test] | ||
| fn prefs_default_is_enabled() { | ||
| let prefs = TelemetryPrefs::default(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: Typo in heading: "Rope" should be "Route" to match the PR title and all other references in the document.
Prompt for AI agents