Skip to content

feat(cua-driver-rs): port TelemetryClient PostHog integration (#1528) - #1532

Merged
f-trycua merged 7 commits into
mainfrom
feat/cua-driver-rs-telemetry-1528
May 16, 2026
Merged

feat(cua-driver-rs): port TelemetryClient PostHog integration (#1528)#1532
f-trycua merged 7 commits into
mainfrom
feat/cua-driver-rs-telemetry-1528

Conversation

@f-trycua

@f-trycua f-trycua commented May 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #1528. Ports Swift's TelemetryClient (anonymous usage telemetry to PostHog) to the Rust port cua-driver-rs. Same endpoint, same event names, same payload shape, same opt-out semantics as Swift — so dashboards aggregate cleanly.

What lands

  • crates/cua-driver/src/telemetry.rs — new module with capture(event, properties), capture_install(), is_enabled(), and canonical event-name constants mirrored 1:1 from Swift's TelemetryEvent.
  • HTTP client: ureq v3 (rustls + json features). Fire-and-forget with 3 s timeout; runs on tokio::task::spawn_blocking when a runtime is live, else on a short-lived OS thread.
  • Call-site wiring: every CLI dispatch in main.rs calls emit_entry_telemetry(&command) once after parsing, before doing work. call <tool> reports as cua_driver_api_<tool> so per-tool adoption is visible without ever recording args.
  • Hidden cua-driver telemetry install-event subcommand — used by scripts/install.sh post-install to fire the one-shot cua_driver_install ping. Guarded by ~/.cua-driver-rs/.installation_recorded marker so it only fires once per install.
  • scripts/install.sh — backgrounded post-install call so the install ping never delays the user-facing "installed" log line.
  • PARITY.md — new "Telemetry (PostHog)" section documenting endpoint, event names, payload shape, privacy posture, opt-out env var, HTTP client choice, and intentional divergences from Swift.

Privacy posture

Identical to Swift. Each event sends: driver version, OS name, OS version, CPU arch, CI-environment flag, and a stable per-install UUID. The payload never contains usernames, file paths, command arguments, tool args, screenshot paths, window titles, application names, or coordinates. Asserted by a unit test that serializes the payload and greps for $user/username/home_dir/cwd/argv.

Independence from Swift install (deliberate)

The Rust install ID lives at ~/.cua-driver-rs/.telemetry_id and is opted out via CUA_DRIVER_RS_TELEMETRY_ENABLED=false — both intentionally distinct from Swift's ~/.cua-driver/.telemetry_id + CUA_DRIVER_TELEMETRY_ENABLED. This means:

  • A machine with both ports installed shows up as two distinct adoption events (we can count Rust adoption separately from Swift).
  • A user who opts out of one port stays opted-in for the other unless they set both env vars.
  • The Rust port can ship telemetry changes without invalidating Swift's install UUID (no shared on-disk state).

Documented in PARITY.md under "Independence from Swift install".

Opt-out

CUA_DRIVER_RS_TELEMETRY_ENABLED=false (or 0 / no / off) disables ALL telemetry from the binary. The only path that bypasses the check is capture_install() (one-shot adoption ping fired by install.sh) — every subsequent event respects the flag normally. Verified locally and in unit tests.

Test plan

  • cargo build -p cua-driver --release — clean build green on macOS arm64.
  • cargo test -p cua-driver --release telemetry — 8 unit tests pass (env parsing, opt-out default, CI detection, payload shape, payload-key collision, install-id idempotent persistence, ISO-8601 format, arch mapping).
  • Manual: cua-driver list-tools with CUA_DRIVER_RS_TELEMETRY_ENABLED=false silently skips the POST.
  • Manual: cua-driver list-tools with CUA_DRIVER_RS_TELEMETRY_DEBUG=true prints [telemetry] sending event: cua_driver_list_tools.
  • Manual: cua-driver telemetry install-event returns PostHog HTTP 200 on first call; second call is silent (marker file present).
  • Manual: cua-driver --version exits cleanly without firing telemetry.
  • CI: verify Linux and Windows builds (cross-platform ureq + rustls build).
  • Manual on Linux: cua-driver list-tools with debug — confirm POST works.
  • Manual on Windows: same.

Commits (4)

  1. feat(telemetry): TelemetryClient with PostHog integration + opt-out — module + unit tests, no call-site wiring.
  2. feat(cli): emit telemetry events from mcp/serve/call subcommands — wire capture(...) at dispatch + add telemetry install-event hidden subcommand.
  3. feat(install.sh): emit cua_driver_install event post-install — installer hook.
  4. docs(PARITY.md): document telemetry parity + opt-out — new section.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Implemented anonymous usage telemetry to track adoption and feature usage.
    • Users can opt-out via the CUA_DRIVER_RS_TELEMETRY_ENABLED environment variable.
    • Install events are automatically captured during setup.
    • All telemetry data excludes personally identifiable information, file paths, and user input.
  • Documentation

    • Added comprehensive telemetry implementation documentation.

Review Change Stack

@vercel

vercel Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Ignored Ignored Preview May 16, 2026 10:21pm

Request Review

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d5d5bcf-2798-493f-bb24-89000237bc85

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR ports PostHog telemetry from the Swift cua-driver to Rust, introducing anonymous usage tracking with event names, per-install identity, payload metadata (version/OS/arch/CI), privacy protections (no user data), opt-out control, and one-shot install pings via a new CLI subcommand wired into the main entrypoint and installer.

Changes

PostHog Telemetry Integration

Layer / File(s) Summary
Telemetry module contracts and public API
src/telemetry.rs (lines 1–159)
Defines public event constants (INSTALL, MCP, SERVE, CALL, etc.), is_enabled() toggle driven by CUA_DRIVER_RS_TELEMETRY_ENABLED, and three entry points: capture(event_name, properties) for generic events and capture_install() for one-time install pings that bypass opt-out via a marker file.
Telemetry implementation: identity, payload, delivery, and platform helpers
src/telemetry.rs (lines 160–652)
Persists a per-install UUID to ~/.cua-driver-rs/.telemetry_id with in-process caching and best-effort disk IO. Constructs PostHog payloads with system metadata (version, OS, arch, CI detection) and optional caller properties. Asynchronously sends events via tokio::task::spawn_blocking or detached threads with fire-and-forget HTTP delivery (ureq with timeout) and optional debug logging. Includes platform-specific OS/version detection, CI environment detection, and ISO-8601 timestamp generation. Validates behavior, privacy (no user identifiers/paths), and ID persistence across reads in unit tests.
CLI telemetry subcommand and event naming
src/cli.rs (lines 42–48, 139–149, 1279–1316)
Adds hidden telemetry install-event subcommand via Command::TelemetryInstallEvent variant, extends parse_command() to dispatch telemetry with usage enforcement (exit code 64 on invalid subcommands), and introduces public telemetry_entry_event(cmd) that maps CLI commands to canonical event names (with per-tool suffixes for call <tool>; returns None for install events).
Main entrypoint telemetry instrumentation
src/main.rs (lines 29, 44–58, 68–79, 257–266)
Declares the telemetry module, adds emit_entry_telemetry(cmd) to record per-command events immediately after CLI parsing, and handles TelemetryInstallEvent in both macOS and non-macOS main by firing capture_install(), sleeping 2 seconds, and returning early to skip normal server startup.
Cargo dependencies for telemetry
Cargo.toml (lines 23–27)
Adds uuid workspace dependency (unique identifier generation) and ureq v3 (with json feature) for HTTP client used by telemetry delivery.
Installer integration and documentation
scripts/install.sh (lines 141–157), PARITY.md (lines 1347–1461)
Install script triggers cua-driver telemetry install-event in background (fire-and-forget, output suppressed, disowned) after binary installation. PARITY.md documents PostHog endpoint, event names, payload fields, privacy guarantees (no user identifiers/paths/args/titles), opt-out via CUA_DRIVER_RS_TELEMETRY_ENABLED (with install bypass), Rust-vs-Swift isolation (separate directories/IDs/env vars), and intentional divergences (no config flag, no GUI launch event, env-var-only CI detection).

Sequence Diagram

sequenceDiagram
  participant User as User/Installer
  participant CLI as cli::parse_command
  participant Main as main()
  participant Emit as emit_entry_telemetry()
  participant Capture as telemetry::capture()
  participant BG as Background Task
  participant PostHog as PostHog API
  
  User->>CLI: Run cua-driver (or telemetry install-event)
  CLI->>Main: Return Command
  Main->>Emit: emit_entry_telemetry(cmd)
  Emit->>Capture: capture(event_name, props)
  Capture->>BG: spawn_capture() via tokio/thread
  BG->>BG: Load/persist distinct_id
  BG->>BG: build_payload(metadata + props)
  BG->>PostHog: POST JSON (fire-and-forget)
  PostHog-->>BG: HTTP response (ignored)
  BG-->>Main: Done (debug log only)
  Main->>Main: Dispatch command (or return early for install-event)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A rabbit hops through telemetry fields,
PostHog payload in paws, no secrets revealed—
Install pings echo softly, opted-out requests decline,
Per-tool event names dance, UUID stars align!
Privacy guarded, CI detected with care,
Rust adoption measured fair. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(cua-driver-rs): port TelemetryClient PostHog integration' clearly and specifically describes the main change—porting PostHog telemetry to the Rust cua-driver crate.
Linked Issues check ✅ Passed The PR fully implements all coding objectives from issue #1528: PostHog client with same endpoint, event names, and payload shape as Swift, privacy-preserving metadata, opt-out support, cross-platform support, and CLI integration verified by unit tests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the telemetry porting objective: new telemetry module, CLI wiring, install script integration, and PARITY.md documentation. No unrelated alterations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cua-driver-rs-telemetry-1528

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@libs/cua-driver-rs/crates/cua-driver/src/cli.rs`:
- Around line 1288-1303: telemetry_entry_event currently interpolates raw
user-provided `tool` into telemetry names (in the Command::Call arm using
event::API_PREFIX + tool), which can leak paths/identifiers; to fix,
validate/sanitize `tool` before emitting: in the Command::Call branch call a
sanitizer function (or inline logic) that returns either a normalized safe token
(e.g., lowercased alphanumerics and limited length) or a stable placeholder (or
hashed/obfuscated value) and use that safe token instead of the raw `tool`; if
validation fails, fall back to event::CALL (or event::CALL + ".unknown") to
avoid emitting user identifiers, referencing telemetry_entry_event,
Command::Call, event::API_PREFIX, and event::CALL so you update the correct
match arm.

In `@libs/cua-driver-rs/crates/cua-driver/src/telemetry.rs`:
- Around line 138-157: The current code calls spawn_capture(event::INSTALL, ...)
which is fire-and-forget, then immediately writes the install marker
(std::fs::create_dir_all and std::fs::write to marker_path), so failures of the
async POST will not trigger retries; either move the marker-write to occur only
after a successful POST (make spawn_capture return a Result or provide a
synchronous/awaitable path and write marker on success), or keep the async
behavior but update the comment to state that the marker is written immediately
and failed POSTs will be dropped (no retry); reference spawn_capture,
event::INSTALL, marker_path, home_dir, std::fs::create_dir_all and
std::fs::write when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 248e44cb-c01d-4352-bcc0-7c141e2d3fde

📥 Commits

Reviewing files that changed from the base of the PR and between 63be0af and fb24e5e.

⛔ Files ignored due to path filters (1)
  • libs/cua-driver-rs/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • libs/cua-driver-rs/PARITY.md
  • libs/cua-driver-rs/crates/cua-driver/Cargo.toml
  • libs/cua-driver-rs/crates/cua-driver/src/cli.rs
  • libs/cua-driver-rs/crates/cua-driver/src/main.rs
  • libs/cua-driver-rs/crates/cua-driver/src/telemetry.rs
  • libs/cua-driver-rs/scripts/install.sh

Comment on lines +1288 to +1303
pub fn telemetry_entry_event(cmd: &Command) -> Option<String> {
use crate::telemetry::event;
let name = match cmd {
Command::Mcp => event::MCP.to_owned(),
Command::Serve { .. } => event::SERVE.to_owned(),
Command::Stop { .. } => event::STOP.to_owned(),
Command::Status { .. } => event::STATUS.to_owned(),
Command::ListTools => event::LIST_TOOLS.to_owned(),
Command::Describe(_) => event::DESCRIBE.to_owned(),
// `call <tool>` → per-tool event (no args, just the tool name).
Command::Call { tool, .. } => {
if tool.is_empty() {
event::CALL.to_owned()
} else {
format!("{}{tool}", event::API_PREFIX)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid emitting raw call tool names into telemetry event names.

At Line 1298, the event suffix uses unvalidated user input (tool). Combined with shorthand parsing, this can send paths or other identifying strings (for invalid calls) and breaches the stated no-path/no-identifier telemetry posture.

Proposed fix
         // `call <tool>` → per-tool event (no args, just the tool name).
         Command::Call { tool, .. } => {
-            if tool.is_empty() {
+            let safe_tool = !tool.is_empty()
+                && tool
+                    .chars()
+                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
+            if !safe_tool {
                 event::CALL.to_owned()
             } else {
                 format!("{}{tool}", event::API_PREFIX)
             }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver-rs/crates/cua-driver/src/cli.rs` around lines 1288 - 1303,
telemetry_entry_event currently interpolates raw user-provided `tool` into
telemetry names (in the Command::Call arm using event::API_PREFIX + tool), which
can leak paths/identifiers; to fix, validate/sanitize `tool` before emitting: in
the Command::Call branch call a sanitizer function (or inline logic) that
returns either a normalized safe token (e.g., lowercased alphanumerics and
limited length) or a stable placeholder (or hashed/obfuscated value) and use
that safe token instead of the raw `tool`; if validation fails, fall back to
event::CALL (or event::CALL + ".unknown") to avoid emitting user identifiers,
referencing telemetry_entry_event, Command::Call, event::API_PREFIX, and
event::CALL so you update the correct match arm.

Comment on lines +138 to +157
// Send the install event *before* writing the marker. If the POST
// fails or the process dies, the next run retries. This is fine —
// PostHog dedupes by `distinct_id + event + timestamp` only loosely;
// a duplicate install ping for the same UUID is a minor noise floor,
// far better than silently dropping the only adoption signal.
spawn_capture(event::INSTALL.to_owned(), None, /*bypass_opt_out*/ true);

// Persist marker so subsequent launches skip re-sending. Best-effort:
// any IO error here just means the next launch sends another install
// event — non-fatal.
if let Err(e) = std::fs::create_dir_all(&home_dir) {
debug_log(format_args!("failed to create {}: {e}", home_dir.display()));
return;
}
if let Err(e) = std::fs::write(&marker_path, "1") {
debug_log(format_args!(
"failed to write install marker {}: {e}",
marker_path.display()
));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Marker written before POST completes—failures won't retry.

The comment states "If the POST fails or the process dies, the next run retries," but spawn_capture is fire-and-forget: it returns immediately while the HTTP POST runs asynchronously. The marker file is written synchronously after spawn_capture returns, so the marker will be persisted even if the POST later fails. Subsequent runs will skip the install event.

If retry-on-failure is important, write the marker only after confirming the POST succeeded (requires making this path synchronous or using a callback). If an occasional missed install ping is acceptable (as noted in lines 140-141), update the comment to match the actual behavior.

📝 Suggested comment fix if current behavior is acceptable
-    // Send the install event *before* writing the marker. If the POST
-    // fails or the process dies, the next run retries. This is fine —
-    // PostHog dedupes by `distinct_id + event + timestamp` only loosely;
-    // a duplicate install ping for the same UUID is a minor noise floor,
-    // far better than silently dropping the only adoption signal.
+    // Send the install event asynchronously. The marker is written
+    // immediately after spawning, so a POST failure won't trigger a
+    // retry on next run. This is acceptable — missing a single install
+    // ping is minor noise, and the fire-and-forget model keeps the CLI
+    // responsive.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Send the install event *before* writing the marker. If the POST
// fails or the process dies, the next run retries. This is fine —
// PostHog dedupes by `distinct_id + event + timestamp` only loosely;
// a duplicate install ping for the same UUID is a minor noise floor,
// far better than silently dropping the only adoption signal.
spawn_capture(event::INSTALL.to_owned(), None, /*bypass_opt_out*/ true);
// Persist marker so subsequent launches skip re-sending. Best-effort:
// any IO error here just means the next launch sends another install
// event — non-fatal.
if let Err(e) = std::fs::create_dir_all(&home_dir) {
debug_log(format_args!("failed to create {}: {e}", home_dir.display()));
return;
}
if let Err(e) = std::fs::write(&marker_path, "1") {
debug_log(format_args!(
"failed to write install marker {}: {e}",
marker_path.display()
));
}
// Send the install event asynchronously. The marker is written
// immediately after spawning, so a POST failure won't trigger a
// retry on next run. This is acceptable — missing a single install
// ping is minor noise, and the fire-and-forget model keeps the CLI
// responsive.
spawn_capture(event::INSTALL.to_owned(), None, /*bypass_opt_out*/ true);
// Persist marker so subsequent launches skip re-sending. Best-effort:
// any IO error here just means the next launch sends another install
// event — non-fatal.
if let Err(e) = std::fs::create_dir_all(&home_dir) {
debug_log(format_args!("failed to create {}: {e}", home_dir.display()));
return;
}
if let Err(e) = std::fs::write(&marker_path, "1") {
debug_log(format_args!(
"failed to write install marker {}: {e}",
marker_path.display()
));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver-rs/crates/cua-driver/src/telemetry.rs` around lines 138 -
157, The current code calls spawn_capture(event::INSTALL, ...) which is
fire-and-forget, then immediately writes the install marker
(std::fs::create_dir_all and std::fs::write to marker_path), so failures of the
async POST will not trigger retries; either move the marker-write to occur only
after a successful POST (make spawn_capture return a Result or provide a
synchronous/awaitable path and write marker on success), or keep the async
behavior but update the comment to state that the marker is written immediately
and failed POSTs will be dropped (no retry); reference spawn_capture,
event::INSTALL, marker_path, home_dir, std::fs::create_dir_all and
std::fs::write when making the change.

@f-trycua

Copy link
Copy Markdown
Collaborator Author

Addressed both CodeRabbit findings:

  • 48e7ad8fix(cli): sanitize tool name before building API telemetry event name. Added sanitize_tool_name (lowercase, keep [a-z0-9_], cap 64, fall back to "unknown") so user-provided tool strings can't leak into PostHog event names. Covered by 5 new unit tests (canonical names, punctuation/path strip, non-ASCII, empty/all-stripped, length cap).
  • 242a614fix(telemetry): synchronous install POST; only write marker on HTTP success. Install path now posts synchronously through a capture_install_with_poster seam; the .installation_recorded marker is only written on HTTP 2xx, so a failed POST (network / 5xx / 4xx) means the next launch retries instead of silently dropping the adoption signal. Bypass-opt-out semantics preserved (install still skips is_enabled()). Also drops the now-unused 2s sleep in main.rs that was waiting for the old fire-and-forget thread. Covered by 2 new unit tests (no marker on Err, no marker on non-2xx). Other telemetry paths remain async fire-and-forget.

Verification: cargo build --release clean on the modified files; cargo test -p cua-driver passes (15 unit tests, up from 8 telemetry → 10 telemetry + 5 new cli).

@coderabbitai please re-review.

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me pull the latest changes to verify both fixes.


🏁 Script executed:

#!/bin/bash
# Get the latest diff for the PR to review the fixes
gh pr view 1532 --repo trycua/cua --json headRefName,baseRefName,commits | jq '.headRefName, .baseRefName, .commits[-3:]'

# Fetch cli.rs and telemetry.rs current state
gh api repos/trycua/cua/contents/libs/cua-driver-rs/crates/cua-driver/src/cli.rs \
  -H "Accept: application/vnd.github.v3.raw" \
  --jq '.' 2>/dev/null | head -200 || \
gh api repos/trycua/cua/git/trees/HEAD?recursive=1 2>/dev/null | jq '.tree[] | select(.path | contains("cli.rs"))' | head -20

Length of output: 4269


🏁 Script executed:

#!/bin/bash
# Try to get the file contents via raw GitHub API using the PR branch
PR_DATA=$(gh pr view 1532 --repo trycua/cua --json headRefName,headRepositoryOwner)
HEAD_BRANCH=$(echo "$PR_DATA" | jq -r '.headRefName')
OWNER=$(echo "$PR_DATA" | jq -r '.headRepositoryOwner.login')
echo "Branch: $HEAD_BRANCH, Owner: $OWNER"

# Get cli.rs content
gh api "repos/${OWNER}/cua/contents/libs/cua-driver-rs/crates/cua-driver/src/cli.rs?ref=${HEAD_BRANCH}" \
  -H "Accept: application/vnd.github.v3.raw" 2>/dev/null | head -250

echo "---SEPARATOR---"

# Get telemetry.rs content - just the relevant sections
gh api "repos/${OWNER}/cua/contents/libs/cua-driver-rs/crates/cua-driver/src/telemetry.rs?ref=${HEAD_BRANCH}" \
  -H "Accept: application/vnd.github.v3.raw" 2>/dev/null | head -300

Length of output: 22449


Let me fetch the remaining parts of the files to complete the review.


🏁 Script executed:

#!/bin/bash
HEAD_BRANCH="feat/cua-driver-rs-telemetry-1528"
OWNER="trycua"

# Get cli.rs - from line 250 onward to see sanitize_tool_name
gh api "repos/${OWNER}/cua/contents/libs/cua-driver-rs/crates/cua-driver/src/cli.rs?ref=${HEAD_BRANCH}" \
  -H "Accept: application/vnd.github.v3.raw" 2>/dev/null | tail -n +251

echo "===TELEMETRY_REST==="

# Get the rest of telemetry.rs
gh api "repos/${OWNER}/cua/contents/libs/cua-driver-rs/crates/cua-driver/src/telemetry.rs?ref=${HEAD_BRANCH}" \
  -H "Accept: application/vnd.github.v3.raw" 2>/dev/null | tail -n +301

echo "===MAIN_RS==="
# Get main.rs to verify the 2s sleep removal
gh api "repos/${OWNER}/cua/contents/libs/cua-driver-rs/crates/cua-driver/src/main.rs?ref=${HEAD_BRANCH}" \
  -H "Accept: application/vnd.github.v3.raw" 2>/dev/null

Length of output: 85345


@f-trycua Both fixes look excellent — verified against the full file contents. Here's what I confirmed:

Fix 1 — sanitize_tool_name

  • The chain c.to_ascii_lowercase()is_ascii_alphanumeric() || lc == '_' correctly handles all edge cases: non-ASCII chars pass through to_ascii_lowercase() unchanged, then fail is_ascii_alphanumeric() and are filtered — so "クリック" and "🚀" both collapse to "unknown" as expected.
  • .take(MAX_LEN) on a char iterator caps at exactly 64 characters (not bytes), which is correct for ASCII-only output.
  • The early tool.is_empty() guard in telemetry_entry_event returning event::CALL before hitting sanitize_tool_name is a clean distinction: explicit empty call → generic cua_driver_call; non-empty but all-stripped → cua_driver_api_unknown.
  • All 5 unit tests are solid and cover the full decision surface.

Fix 2 — Synchronous install POST + marker-on-success

  • capture_install_with_poster correctly gates the fs::write exclusively inside the Ok(status) if (200..300).contains(&status) arm. Both Err and non-2xx branches do an early return, leaving the marker absent for retry.
  • The with_isolated_home helper properly restores both HOME and USERPROFILE, and the tests acquire ENV_LOCK — safe for parallel test execution.
  • The 2s sleep is completely gone from both the macOS and non-macOS main branches. No vestige remains.

The implementation is clean and the test suite is thorough.

[approve]

🐇🎉


✏️ Learnings added
Learnt from: f-trycua
URL: https://github.com/trycua/cua/pull/1532

Timestamp: 2026-05-16T22:06:08.368Z
Learning: In the `cua-driver-rs` crate (`libs/cua-driver-rs/crates/cua-driver/src/cli.rs`), user-provided tool names from `Command::Call { tool, .. }` are sanitized via `sanitize_tool_name` before being concatenated onto `event::API_PREFIX` ("cua_driver_api_") to build PostHog telemetry event names. The sanitizer: lowercases, keeps only [a-z0-9_], caps at 64 chars, and falls back to "unknown" on empty result.

Learnt from: f-trycua
URL: https://github.com/trycua/cua/pull/1532

Timestamp: 2026-05-16T22:06:08.368Z
Learning: In `libs/cua-driver-rs/crates/cua-driver/src/telemetry.rs`, `capture_install()` posts synchronously via `capture_install_with_poster(post_to_posthog)`. The `.installation_recorded` marker file is only written on HTTP 2xx; Err or non-2xx leaves the marker absent so the next launch retries. The `capture_install_with_poster<F>` function is a test seam that accepts an injectable HTTP poster closure.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

f-trycua and others added 7 commits May 17, 2026 00:20
Port the Swift `TelemetryClient` from `libs/cua-driver/.../TelemetryClient.swift`
to a new `crates/cua-driver/src/telemetry.rs` module. Same PostHog
ingest endpoint, same event names, same payload shape — so Rust and
Swift telemetry aggregate cleanly on the same dashboards.

Public API:
- `telemetry::capture(event, properties)` — fire-and-forget HTTP POST,
  no-op when opt-out is active.
- `telemetry::capture_install()` — one-shot install ping guarded by a
  marker file, **only** path that bypasses the opt-out check (for
  adoption counting parity with Swift).
- `telemetry::is_enabled()` — single env-var check.
- `telemetry::event::*` constants — canonical event names mirrored 1:1
  from Swift's `TelemetryEvent` enum.

Differences from Swift (deliberate, documented in module docs):
- Install ID at `~/.cua-driver-rs/.telemetry_id` (Swift uses
  `~/.cua-driver/`). Independent so opting out of one port doesn't
  silence the other.
- Opt-out env var is `CUA_DRIVER_RS_TELEMETRY_ENABLED=false` (Swift
  uses `CUA_DRIVER_TELEMETRY_ENABLED`). Same independence rationale.
- `$lib = "cua-driver-rs"` so dashboards split Rust vs Swift adoption.
- No persisted config flag (YAGNI — env var only).

HTTP client: `ureq` v3 with rustls (default features). One transitive
dep tree, no system OpenSSL needed. POST runs on `spawn_blocking` when
a tokio runtime is live, otherwise a short-lived OS thread — covers
both the async MCP server and sync CLI subcommands. 3s timeout, all
errors logged via `tracing::debug!` only.

Tests cover: env-var bool parsing, opt-out default semantics, CI
detection, payload shape (incl. privacy assertion that no usernames /
paths / argv leak into the envelope), default-envelope precedence on
key collision, install-ID idempotent persistence, ISO-8601 format.

No call-site wiring yet — commit 2 adds telemetry emission at the
mcp/serve/call CLI entry points.

Refs #1528.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire `telemetry::capture(...)` at the CLI dispatch boundary so every
invocation emits its entry-event (e.g. `cua_driver_mcp`,
`cua_driver_api_click`) once before any work starts. Mirrors Swift's
`TelemetryClient.shared.record(event: entryEvent)` at the top of
`CuaDriverCommand.main()`.

- New `cli::telemetry_entry_event(&Command) -> Option<String>` maps each
  parsed subcommand to its canonical event name. `call <tool>` reports
  as `cua_driver_api_<tool>` so per-tool adoption is visible without
  ever recording the args. Implicit-call form (`cua-driver <tool>`)
  reuses the same path via `Command::Call`.
- New hidden `cua-driver telemetry install-event` subcommand
  (`Command::TelemetryInstallEvent`) — installer-only entry point that
  fires the one-shot `cua_driver_install` ping via
  `telemetry::capture_install()`. Bypasses opt-out (only path that
  does so); guarded by the `.installation_recorded` marker file so
  repeat invocations are no-ops.

Both macOS and non-macOS `main()` paths now call `emit_entry_telemetry`
right after `parse_command()` and before dispatch — fire-and-forget,
respects the env-var opt-out, never blocks the actual work.

Verified locally on macOS:
- `cua-driver list-tools` with `CUA_DRIVER_RS_TELEMETRY_ENABLED=false`
  silently skips the POST.
- `cua-driver list-tools` with debug enabled prints
  `[telemetry] sending event: cua_driver_list_tools`.
- `cua-driver telemetry install-event` returns PostHog HTTP 200 on
  first call; second call is silent (marker file present).
- `cua-driver --version` still exits cleanly without firing anything
  (handled before parse_command).

Refs #1528.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After dropping the binary into place, fire `cua-driver telemetry
install-event` once in the background. Bypasses the
`CUA_DRIVER_RS_TELEMETRY_ENABLED` opt-out by design so we count
adoption even from users who immediately disable telemetry (every
subsequent event from the binary respects the opt-out normally).

The binary's own `.installation_recorded` marker guards against
re-sends, so re-running `install.sh` (e.g. after `cua-driver update`)
is a no-op for telemetry.

Run in the background with `&` + `disown` so a slow or failed POST
can never delay the install — keeps the user-facing "installed" log
line snappy.

Refs #1528.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New "Telemetry (PostHog)" section. Covers:

- Endpoint + event names (identical to Swift so dashboards aggregate)
- Payload shape table (every key + its source)
- Privacy posture: explicit list of what we DO NOT send, backed by
  a unit-test assertion in build_payload_contains_required_keys.
- Opt-out env var (CUA_DRIVER_RS_TELEMETRY_ENABLED) and the single
  exception (install ping bypasses for adoption counting).
- Independence-from-Swift table: separate marker dir + UUID + env var
  so opting out of one port doesn't silence the other.
- HTTP client choice (ureq v3 + rustls) and timeout/error-handling.
- Intentional divergences (no persisted config flag, no GUI launch
  emission, env-var-only CI detection).

Refs #1528.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-tool `call <tool>` events were concatenating the raw user-provided
tool string onto `cua_driver_api_`, so path-like or non-ASCII tool names
would flow verbatim into PostHog event names (privacy + dashboard pollution).

Add `sanitize_tool_name` that lowercases, keeps only `[a-z0-9_]`, caps at
64 chars, and falls back to `"unknown"` when the input strips to empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uccess

Previously `capture_install()` fired the install event via the async
`spawn_capture` path and immediately wrote the `.installation_recorded`
marker — so a failed POST (network, PostHog outage, non-2xx) silently
dropped the only adoption signal because the marker still prevented
retries on the next launch.

Switch the install path to a synchronous POST via a new internal
`capture_install_with_poster` seam (the seam exists for testability —
public callers use `capture_install`). The marker is only written when
the POST returns HTTP 2xx; any other outcome (Err, 4xx, 5xx) leaves the
marker absent so the next `cua-driver` launch retries.

Other telemetry paths still use the async `spawn_capture` fire-and-forget
flow — only the install one-shot blocks. Bypass-opt-out semantics are
preserved (install path still skips `is_enabled()`).

Drops the now-unused 2s sleep in `main.rs` (the comment claimed it was
waiting for a spawned thread; the POST is sync now).

Adds two unit tests verifying the marker is not written on Err or on
non-2xx responses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cua-driver-rs: missing telemetry (PostHog integration)

1 participant