Skip to content

feat: anonymous opt-out telemetry (PostHog) - #74

Merged
theDakshJaitly merged 2 commits into
mainfrom
feat/telemetry-core
Jun 9, 2026
Merged

feat: anonymous opt-out telemetry (PostHog)#74
theDakshJaitly merged 2 commits into
mainfrom
feat/telemetry-core

Conversation

@theDakshJaitly

Copy link
Copy Markdown
Collaborator

Stacked on #73 (E1 — scaffold identity). Base this review against the E1 branch; once #73 merges to main, GitHub will retarget this PR to main automatically. The telemetry layer needs E1's scaffold_id as its grouping key.

What

A thin, anonymous, opt-out telemetry layer that counts command usage. All PostHog-specific code lives behind src/telemetry/index.ts so the backend can be swapped in one file.

Trust properties

  • Whitelist payload only: machine_id, scaffold_id, command name, mex_version, os, node_version. No args, paths, file contents, repo names, IP, or location (disableGeoip is set so PostHog never derives geo from the request IP).
  • Opt out four ways: DO_NOT_TRACK=1, MEX_TELEMETRY=0, mex config set telemetry off, and a dev-repo guard that hard-disables when run from a clone of mex itself (checked before any disk read).
  • No fingerprint when off: machine_id (random UUID at ~/.mex/telemetry-id, mode 0600) is created only when enabled.
  • PII firewall: scaffold_id is passed as a string only — never the identity object — so scaffold_name/origin/upstream cannot leak.
  • Never breaks a command: capture fires in a preAction hook (so commands that process.exit, like check on drift, are still counted); flush is best-effort and bounded; all errors swallowed.
  • Auditable: mex telemetry inspect prints the exact would-be payload without sending (and without minting the machine-id file); mex telemetry status shows state + active opt-out reason; one-time first-run notice to stderr.

Tests / docs

219 tests pass, typecheck clean. The suite can never emit real events (vitest.config.ts sets MEX_TELEMETRY=0). Adds TELEMETRY.md, a README link, and a CHANGELOG entry.

Note

The embedded phc_... key is PostHog's write-only ingestion key, documented as safe to ship in client code.

Copilot AI review requested due to automatic review settings June 9, 2026 01:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds an anonymous, opt-out telemetry layer backed by PostHog, wired into the CLI via Commander hooks, along with transparency commands (mex telemetry inspect/status) and supporting docs/tests.

Changes:

  • Added PostHog-backed telemetry module with a whitelisted payload, opt-out checks, first-run notice, and best-effort flush.
  • Hooked telemetry into the CLI lifecycle and added mex telemetry + mex config set telemetry on|off commands.
  • Added documentation (TELEMETRY.md), changelog/readme updates, and a comprehensive telemetry test suite (with Vitest env hard-disable by default).

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
vitest.config.ts Forces telemetry off in tests by default via MEX_TELEMETRY=0.
test/telemetry.test.ts Adds unit coverage for opt-out precedence, payload whitelist, dev-repo guard, first-run notice, and read-only behaviors.
TELEMETRY.md Documents data collection, opt-out paths, trust properties, and transparency commands.
src/telemetry/index.ts Implements telemetry enablement checks, payload construction, PostHog client wrapper, capture/flush, and first-run notice.
src/global-config.ts Adds global config + machine-id file handling and dev-repo detection.
src/config.ts Adds read-only readScaffoldId() for inspect/telemetry hooks.
src/cli.ts Wires telemetry into preAction/postAction, adds telemetry and config set commands, and switches to parseAsync().
README.md Adds a telemetry section linking to TELEMETRY.md and opt-out instructions.
package.json Adds posthog-node dependency.
package-lock.json Locks posthog-node and transitive deps.
CHANGELOG.md Records the telemetry feature and related user-facing commands.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/cli.ts
Comment on lines +54 to +66
program.hook("preAction", (_thisCommand, actionCommand) => {
try {
let scaffoldId: string | undefined;
try {
scaffoldId = readScaffoldId(findConfig().scaffoldRoot);
} catch {
// No scaffold (or not in one) — omit scaffold_id.
}
captureCommand(actionCommand.name(), scaffoldId);
} catch {
// Telemetry must never affect command behaviour.
}
});
Comment thread src/telemetry/index.ts Outdated
Comment on lines +205 to +208
const timeoutPromise = new Promise<void>((resolve) =>
setTimeout(resolve, FLUSH_TIMEOUT_MS),
);
await Promise.race([client.flush(), timeoutPromise]);
Comment thread src/global-config.ts Outdated
Comment thread src/global-config.ts
Comment on lines +109 to +111
* Generalized from `setup/index.ts:127-138` — checks the real package name
* `mex-agent` plus legacy names `promexeus` and `mex` for safety.
*/
Comment thread test/telemetry.test.ts Outdated
Comment on lines +371 to +377
it("importing the module does not create files or make network calls", () => {
// The module is already imported — check that no ~/.mex/ was created
// in a fresh temp home before any explicit call
const freshHome = mkdtempSync(join(tmpdir(), "mex-import-"));
expect(existsSync(join(freshHome, ".mex"))).toBe(false);
rmSync(freshHome, { recursive: true, force: true });
});
Comment thread src/telemetry/index.ts
Comment on lines +56 to +62
// 3. Dev-repo guard (no disk read for global config yet)
if (isDevRepo()) {
return { enabled: false, reason: "dev" };
}

// 4. Global config opt-out
try {
Comment thread src/telemetry/index.ts
Comment on lines +36 to +41
* Precedence (first match wins):
* 1. `DO_NOT_TRACK=1` → off
* 2. `MEX_TELEMETRY=0` → off
* 3. Dev repo / `MEX_DEV` → off
* 4. `~/.mex/config.json` `telemetry === "off"` → off
* 5. else → on
theDakshJaitly added a commit that referenced this pull request Jun 9, 2026
- preAction no longer fires for the telemetry/config meta-commands, so
  `mex telemetry inspect` sends no event and never creates the machine-id
  file — it stays a pure audit surface even when telemetry is enabled.
- flush() now clears its race timer in a finally, so a fast flush can't
  leave an 800ms timeout pending and delay process exit.
- Remove unused `constants` import in global-config.ts.
- Fix isDevRepo JSDoc: the bare `mex` package name is intentionally excluded.
- Make the "no I/O at import time" test real — it now re-evaluates the module
  under a fresh HOME and asserts nothing is written, instead of passing
  vacuously.
- TELEMETRY.md: drop the non-interactive auto-disable claim; CI usage is
  counted (no TTY gate), so the doc matches the code.
Add a thin telemetry layer that counts command usage anonymously. All
PostHog-specific code lives behind src/telemetry/index.ts so the backend
can be swapped in one file.

- Whitelist payload only: machine_id, scaffold_id, command name, mex_version,
  os, node_version. No args, paths, file contents, repo names, IP, or geo
  (disableGeoip is set so PostHog never derives location from the IP).
- Opt-out precedence: DO_NOT_TRACK=1, MEX_TELEMETRY=0, `mex config set
  telemetry off`, else on. Dev-repo guard hard-disables when run from a clone
  of mex itself, before any disk read.
- machine_id is a random UUID at ~/.mex/telemetry-id (0600), created only when
  enabled. scaffold_id (from E1) is passed as a string only — never the
  identity object — so scaffold_name/origin/upstream can't leak.
- Fire-and-forget: capture fires in a preAction hook (so process.exit commands
  like `check` on drift are still counted); flush is best-effort and bounded;
  a telemetry failure never blocks or changes a command's exit code.
- Transparency: `mex telemetry inspect` prints the exact would-be payload
  without sending (and without minting the machine-id file); `mex telemetry
  status` shows enabled/disabled + reason; one-time first-run notice to stderr.
- Tests never emit real events (vitest sets MEX_TELEMETRY=0).
- Docs: TELEMETRY.md + README link + CHANGELOG entry.
- preAction no longer fires for the telemetry/config meta-commands, so
  `mex telemetry inspect` sends no event and never creates the machine-id
  file — it stays a pure audit surface even when telemetry is enabled.
- flush() now clears its race timer in a finally, so a fast flush can't
  leave an 800ms timeout pending and delay process exit.
- Remove unused `constants` import in global-config.ts.
- Fix isDevRepo JSDoc: the bare `mex` package name is intentionally excluded.
- Make the "no I/O at import time" test real — it now re-evaluates the module
  under a fresh HOME and asserts nothing is written, instead of passing
  vacuously.
- TELEMETRY.md: drop the non-interactive auto-disable claim; CI usage is
  counted (no TTY gate), so the doc matches the code.
@theDakshJaitly
theDakshJaitly changed the base branch from feat/e1-scaffold-identity to main June 9, 2026 06:27
@theDakshJaitly theDakshJaitly reopened this Jun 9, 2026
@theDakshJaitly
theDakshJaitly merged commit 379dbeb into main Jun 9, 2026
2 checks passed
@theDakshJaitly
theDakshJaitly deleted the feat/telemetry-core branch June 9, 2026 06:31
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.

2 participants