feat(analytics): add anonymised usage telemetry via @elastic/ebt#29
Conversation
Wires the MCP App into Elastic's V3 analytics shipper so the team can see which views and tools are exercised in the wild. Emits two closed- schema events: - mcp_tool_called (server-side, every tracked tool handler) - view_rendered (client-side, once per top-level view mount) Shipping mirrors the user's Kibana telemetry opt-in fetched once at startup from GET /api/telemetry/v2/config on the default cluster, and is fail-closed: events queue in-memory and drop when optIn is false, null, or the fetch errors. Each event is enriched with cluster/license context plus mcp_app_version; cluster_name is intentionally omitted to keep the feed anonymised. See docs/telemetry.md for the full event catalog, opt-out story, and codebase map.
Clarify telemetry routing in logs and tighten the shared analytics bootstrap so the PR documents the production default and staging override.
…Modules() The sample-data test uses vi.resetModules() to isolate module-scoped state. After reset, a fresh dynamic import transitively loads @elastic/ebt as a CJS module, which vitest's ESM context cannot parse. Mocking create-analytics-client.js (the entry point that imports ebt) stops the load; the mock survives vi.resetModules() because vi.mock() registrations are hoisted and not cleared by the module cache reset.
|
Question: The context block shipped with every event includes Can we get explicit product/privacy sign-off that collecting this is acceptable? @davethegut | Field | Source | Notes |
|-------------------|-------------------------------------|------------------------------------------------|
| `cluster_uuid` | Elasticsearch `GET /` | Required by the V3 shipper; events do not ship without it |
| `cluster_version` | Elasticsearch `GET /` `version.number` | Stack version |
| `license_id` | Elasticsearch `GET /_license` | Optional |
| `license_status` | Elasticsearch `GET /_license` | Optional |
| `license_type` | Elasticsearch `GET /_license` | Optional |
| `mcp_app_version` | `package.json` `version` | Version of the MCP App that emitted the event | |
- Fix McpAppProvider name mismatch in attack-discovery (was "attack-discovery-triage", must match bootstrap viewId "attack-discovery") - Add noopAnalyticsClient to production code; make createServer analytics dep optional with noop default - Remove dead sync path in registerTrackedAppTool — always wrap in Promise.resolve() - Add initial-load fallback useEffect to all five views so the first connected+idle render triggers a data load - Remove bufferedEvents mutation inside adaptLogger; replace with simple per-event logReportedEvent calls gated on optedIn flag - Log a warning instead of silently swallowing errors in the report-analytics-event catch block - Add test coverage for noop default in createServer and warn logging in registerAnalyticsTools
|
@KDKHD - Smoke-tested this against staging from Claude Desktop — opt-in resolution, batching, and shipping all work cleanly, zero errors. One thing worth flagging: Otherwise LGTM. |
|
Also, @KDKHD, noticed this weird bug when invoking a threat hunt? Dk if it is just my machine? Clipboard-20260522-181337-405.mp4 |
davethegut
left a comment
There was a problem hiding this comment.
left a comment, but still approving this PR :)
…to feature/user-analytics2
Memoize the derived bootstrap hook result so views do not replay startup effects on unrelated rerenders.
|
@davethegut The UI bug and the repeated telemetry events bug were related and have been fixed now. |
Summary
Adds usage telemetry for the Elastic Security MCP app so we can understand which views and tools are being exercised.
Telemetry uses the Kibana telemetry opt-in setting from the default Kibana cluster. On startup, the app reads
GET /api/telemetry/v2/configfrom that default cluster and only ships events when Kibana telemetry opt-in is enabled. If opt-in is disabled, unset, or the config fetch fails, the app stays opted out.By default, telemetry is sent to production (
telemetry.elastic.co). To test against staging, start the MCP server with:claude_desktop_config.json
{ "mcpServers": { "elastic-security": { "command": "node", "args": [ "/Users/<BUILD PATH>/dist/main.js", "--stdio" ], "env": { "MCP_APP_TELEMETRY_ENV": "staging", "CLUSTERS_JSON": "[{\"name\":\"primary\",\"elasticsearchUrl\":\"https://my-deployment-782f0f.es.us-central1.gcp.cloud.es.io\",\"kibanaUrl\":\"https://my-deployment-782f0f.kb.us-central1.gcp.cloud.es.io\",\"elasticsearchApiKey\":\"<YOUR API KEY>\"}]" } } }, }That routes events to
telemetry-staging.elastic.coinstead.Test plan
npm run test:run -- src/elastic/analytics/create-analytics-client.test.ts src/elastic/service/telemetryService.test.tsnpm run typecheck