Skip to content

Add opt-in telemetry reporting#4

Open
rishiy15 wants to merge 1 commit into
mainfrom
add-telemetry-reporting
Open

Add opt-in telemetry reporting#4
rishiy15 wants to merge 1 commit into
mainfrom
add-telemetry-reporting

Conversation

@rishiy15

Copy link
Copy Markdown
Collaborator

Summary

Adds opt-in, no-PII telemetry to the plugin so 1Password can measure plugin adoption and the prevalence of common validation outcomes (allow/deny, missing vs. disabled mounts). Events are written as JSON lines to a file that the 1Password desktop app ingests and forwards to
1Password's telemetry pipeline. This brings the cursor-plugin to parity with the equivalent instrumentation in the agent-hooks repo so the two can be unioned on a single dashboard.

This is the producer half only — it writes event files. Ingestion/forwarding is handled by the 1Password desktop app; this PR has no network calls of its own.

What & why

The hook already knows, per invocation, whether it allowed or denied, why it denied, how it discovered env files, and how long it took. Capturing that (bucketed, de-identified) lets us answer "are people actually using the plugin, and where does validation commonly fail?" without
any code in the hot path that could affect a hook decision.

Changes

scripts/lib/telemetry.sh (new, sourced helper)

  • Pure functions, no side effects on source, idempotent source guard, fails silently throughout — telemetry can never alter a hook decision.
  • bucket_duration_ms — raw ms → coarse bucket (ms_0_to_50 … ms_5000_plus) to bound cardinality and avoid timing fingerprints.
  • current_time_ms — portable epoch-ms with a fallback chain (EPOCHREALTIME → date +%s%3N → perl → python3 → date +%s).
  • telemetry_consent_enabled — true only if the signal file ~/.config/1Password/telemetry-enabled exists.
  • write_telemetry_event — consent gate → mkdir -p → 1 MB file-size cap → append one JSON line.
  • write_execution_event / write_install_event — build the two event payloads.
  • emit_plugin_marketplace_install_event_once — fires the install event once per (client, hook_name, hook_version), deduped by a sentinel file (so an upgrade emits a fresh install event).

scripts/validate-mounted-env-files (+95)

  • Sources the lib fail-open (|| true if absent).
  • Reads hook_version from .cursor-plugin/plugin.json via pure-bash extraction (no jq dependency).
  • Tracks resolved_mode (default/configured) and mount_count, and derives a three-bucket deny_reason (file_missing, file_disabled, file_missing_and_disabled).
  • Emission is off the hot path: all values are derived before output_response, and the actual writes run after it in a detached background subshell (( … ) >/dev/null 2>&1 &), so the IDE gets its decision immediately and never blocks on telemetry.

Events emitted

Both under vendor com.1password.app:

  • agent_hook_execution (per invocation): hook_name, hook_version, client (cursor), event_type (before_shell_execution), decision, deny_reason, duration_bucket, mode, mount_count.
  • agent_hook_install (once per version, via sentinel): client, hook_name, hook_version, install_method (plugin_marketplace).

Privacy & consent

  • Opt-in only. Nothing is written unless ~/.config/1Password/telemetry-enabled exists; the desktop app creates/removes it from the in-app Data Usage preference. The app makes a final authoritative consent check before anything leaves the device.
  • No PII. No paths, file contents, environment names, or workspace paths — only the hook name/version, client, decision, bucketed duration, mode, mount count, and a coarse deny reason.
  • Fail-open. Telemetry runs in a detached subshell after the decision is returned; any failure (missing lib, full disk, permission denied) is swallowed.

Testing

Automated (library behavior)

Sandboxed exercise of scripts/lib/telemetry.sh under a throwaway $HOME (no writes to your real ~/.config/1Password): consent gating, JSON validity (incl. escaped quotes), null serialization for empty mode/mount_count, bucket_duration_ms boundaries, install-once dedup +
version-bump, and the 1 MB cap. All pass.

Manual end-to-end in Cursor

▎ The beforeShellExecution hook only fires for agent-run shell commands — commands the Cursor agent executes, not ones you type into the terminal yourself.

Prerequisites

  1. Enable the telemetry consent signal

Events are only written when the signal file exists. Either flip the in-app setting (Settings → Manage Account → Data Usage → enable product telemetry), or create it directly for a producer-only test:
touch ~/.config/1Password/telemetry-enabled

  1. Register this checkout as a local hook

Create a .cursor/hooks.json in the workspace you'll test in, pointing the command at the absolute path of this checkout's script (so you exercise your working tree, not the installed marketplace copy):

mkdir -p .cursor
cat > .cursor/hooks.json <<'EOF'
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{ "command": "/ABSOLUTE/PATH/TO/cursor-plugin/scripts/validate-mounted-env-files" }
]
}
}
EOF
Replace /ABSOLUTE/PATH/TO/cursor-plugin with your clone path. (User-level ~/.cursor/hooks.json works too; project-level is easiest to scope to one workspace.) If the marketplace 1Password plugin is installed, disable it (Settings → Plugins) so it doesn't also fire.

  1. Reload Cursor so it picks up the hook: Cmd+Shift+P → Developer: Reload Window.

  2. Trigger the allow path — ask the agent to run a shell command, e.g. "run echo hi in the terminal". Then confirm:

  • the hook ran (timestamped line appears):
    tail -5 /tmp/1password-cursor-hooks.log
  • the event was written (cat immediately — the desktop app drains it every ~60s):
    cat ~/.config/1Password/data/hook-events/events.jsonl
    Expect an agent_hook_execution line with "decision":"allow", the correct hook_version (from .cursor-plugin/plugin.json), client":"cursor", plus a one-time agent_hook_install line on the first run after install.
  1. Trigger the deny path — point a mount at a missing file so validation denies. Easiest via TOML in the test workspace:
    mkdir -p .1password
    echo 'mount_paths = ["'"$PWD"'/.env.missing"]' > .1password/environments.toml
    Re-trigger an agent command; expect a "decision":"deny" event with "deny_reason":"file_missing" and "mode":"configured". (Disable a mount in the app, or combine both, to see file_disabled / file_missing_and_disabled.)

  2. Verify fail-open — confirm telemetry can't break the hook:
    mv scripts/lib/telemetry.sh /tmp/telemetry.sh.bak # simulate missing lib
    Re-trigger; the hook must still return its allow/deny decision normally (no telemetry written). Restore afterward:
    mv /tmp/telemetry.sh.bak scripts/lib/telemetry.sh

  3. Confirm ingestion (if running a desktop-app build with hook-event ingestion): after a trigger, events.jsonl is drained (deleted) within ~60s and the event appears in your telemetry backend (e.g. Snowplow). Without that build, step 4's cat confirms the producer side.

Cleanup: remove .cursor/hooks.json, .1password/environments.toml, and (if you created it for testing) ~/.config/1Password/telemetry-enabled.

@rishiy15 rishiy15 marked this pull request as ready for review June 22, 2026 16:53
@rishiy15 rishiy15 requested review from JillRegan and bertrmz June 22, 2026 16:53
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.

1 participant