How does optional anonymous telemetry work?

HEAD
dd55c47add/fix deployment page

How does the optional anonymous telemetry implementation works?

Avatar of anthropic/claude-opus-4.6
Claude Opus 4.6
commented

Now I have a comprehensive picture. Here's how the optional anonymous telemetry implementation works:


##Anonymous Telemetry Architecture

The telemetry system lives in packages/xmcp/src/telemetry/ and is designed with strong privacy guarantees, opt-out mechanisms, and non-blocking behavior. Here's how each piece fits together:

###1. Opt-in by Default, Easy Opt-out

storage.ts manages the user's telemetry preference via a JSON config file:

  • Config location: ~/.config/xmcp/telemetry.json on Unix, AppData/Local/xmcp/telemetry.json on Windows, and a temp directory for CI/Docker (ephemeral).
  • Opt-out methods:
    • Set XMCP_TELEMETRY_DISABLED=true environment variable (line 68)
    • Run npx xmcp telemetry disable (calls setEnabled(false) which writes telemetry.enabled: false to the config file)
  • First-run notification: The notify() method (line 117) displays a one-time console message informing the user about telemetry, with a link to https://xmcp.dev/telemetry and opt-out instructions. It records the notification date so it only shows once.

###2. Anonymity Guarantees

Several mechanisms ensure no personal data is ever sent:

  • Anonymous ID (line 147): A random 32-byte hex string, generated per installation. Not tied to any user identity.
  • One-way hashing with salt (line 197): A cryptographic salt (random 16-byte hex) is stored locally and never sent to the server. All identifiers (like project IDs) are hashed with SHA-256 using this salt before transmission, making dictionary attacks impossible.
  • Project ID (project-id.ts): Derived from package.json's name@version, then one-way hashed before sending — so the actual project name is never transmitted.

###3. What Data is Collected

Defined in definitions.ts, only two event types are tracked:

EventFields
build:completedsuccess, duration, toolsCount, reactToolsCount, promptsCount, resourcesCount, outputSize, transport (http/stdio), adapter (none/express/nextjs), nodeVersion, xmcpVersion
build:failedSame as above, plus errorPhase (config/compile/webpack/transpile), errorType

System metadata (metadata.ts) includes: OS platform, arch, CPU cores/model/speed, total memory, and whether the environment is Docker/WSL/CI. No IP addresses, file paths, source code, or user-identifiable information is included.

###4. How Events Are Sent

The TelemetryTracker singleton orchestrates everything:

  1. Recording: record() queues events in a Set<Promise>. Events can be deferred (skipping immediate network submission) when a detached flush is imminent.

  2. Detached flush (flushDetached()): This is the key non-blocking mechanism:

    • Events are serialized to <distDir>/_events.json along with the sessionId
    • A detached child process is spawned (detached: true, unref()'d) running detached-flush.ts
    • This allows the main build process to exit immediately without waiting for the HTTP request
  3. The detached process reads the events file, constructs the payload, POSTs to https://telemetry.xmcp.dev/api/telemetry/events, and cleans up the file on success. If it fails, the file remains for a potential future pickup.

  4. HTTP posting (post-payload.ts): Uses fetch with:

    • 5-second timeout via AbortSignal.timeout(5000)
    • 1 retry with 500ms minimum backoff (via async-retry)
    • Error swallowing by default — telemetry never breaks the build

###5. Integration with the Build

In build-telemetry.ts, two convenience functions wire telemetry into the compiler:

Both use record(event, true) (deferred) followed by flushDetached("build") to ensure the build process isn't delayed.

###6. Debug Mode

Setting XMCP_DEBUG_TELEMETRY=true (debug.ts) enables verbose logging of telemetry state and payloads, useful for verifying exactly what's being sent.

###Summary Flow

Build completes/fails
  → logBuildSuccess() / logBuildFailure()
    → record(event, deferred=true)  — queues in memory
    → flushDetached("build")
      → writes _events.json to disk
      → spawns detached child process (non-blocking)
        → detached-flush.ts reads events, checks isDisabled
        → constructs payload with anonymousId, projectHash (salted SHA-256), metadata
        → POSTs to https://telemetry.xmcp.dev/api/telemetry/events
        → cleans up _events.json

The design closely mirrors Next.js's telemetry approach: privacy-first, non-blocking, with clear opt-out mechanisms and no personally identifiable information.


END OF POST

How does optional anonymous telemetry work? — basementstudio/xmcp