How does optional anonymous telemetry work?
How does the optional anonymous telemetry implementation works?
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.jsonon Unix,AppData/Local/xmcp/telemetry.jsonon Windows, and a temp directory for CI/Docker (ephemeral). - Opt-out methods:
- Set
XMCP_TELEMETRY_DISABLED=trueenvironment variable (line 68) - Run
npx xmcp telemetry disable(callssetEnabled(false)which writestelemetry.enabled: falseto the config file)
- Set
- First-run notification: The
notify()method (line 117) displays a one-time console message informing the user about telemetry, with a link tohttps://xmcp.dev/telemetryand 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 frompackage.json'sname@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:
| Event | Fields |
|---|---|
build:completed | success, duration, toolsCount, reactToolsCount, promptsCount, resourcesCount, outputSize, transport (http/stdio), adapter (none/express/nextjs), nodeVersion, xmcpVersion |
build:failed | Same 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:
-
Recording:
record()queues events in aSet<Promise>. Events can be deferred (skipping immediate network submission) when a detached flush is imminent. -
Detached flush (
flushDetached()): This is the key non-blocking mechanism:- Events are serialized to
<distDir>/_events.jsonalong with thesessionId - A detached child process is spawned (
detached: true,unref()'d) runningdetached-flush.ts - This allows the main build process to exit immediately without waiting for the HTTP request
- Events are serialized to
-
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. -
HTTP posting (
post-payload.ts): Usesfetchwith:- 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-second timeout via
###5. Integration with the Build
In build-telemetry.ts, two convenience functions wire telemetry into the compiler:
logBuildSuccess()— called after a successful production build (compiler/index.tsline 296)logBuildFailure()— called on webpack errors (compiler/index.tsline 260)
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