Skip to content

feat(analytics): add anonymised usage telemetry via @elastic/ebt#29

Merged
KDKHD merged 6 commits into
mainfrom
feature/user-analytics2
May 26, 2026
Merged

feat(analytics): add anonymised usage telemetry via @elastic/ebt#29
KDKHD merged 6 commits into
mainfrom
feature/user-analytics2

Conversation

@KDKHD

@KDKHD KDKHD commented May 13, 2026

Copy link
Copy Markdown
Member

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/config from 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:

MCP_APP_TELEMETRY_ENV=staging

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.co instead.

Test plan

  • npm run test:run -- src/elastic/analytics/create-analytics-client.test.ts src/elastic/service/telemetryService.test.ts
  • npm run typecheck

KDKHD added 3 commits May 13, 2026 15:49
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.
@KDKHD

KDKHD commented May 20, 2026

Copy link
Copy Markdown
Member Author

Question: license_id in telemetry context

The context block shipped with every event includes license_id (the Elasticsearch license UUID). For Elastic Cloud deployments this UUID is linkable to a specific customer in Elastic's internal systems — it's not user PII, but it is org-identifying information.

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 KDKHD requested a review from davethegut May 20, 2026 12:35
@KDKHD KDKHD marked this pull request as ready for review May 21, 2026 09:07
@davethegut

davethegut commented May 22, 2026

Copy link
Copy Markdown
Collaborator

@KDKHD - Smoke-tested this against staging from Claude Desktop — opt-in resolution, batching, and shipping all work cleanly, zero errors.

One thing worth flagging: execute-esql fired ~7,000 times in a single threat-hunt session because the Monaco editor live-validates on every keystroke. Each one emits a mcp_tool_called event. report-analytics-event is already excluded for the same reason — might be worth adding execute-esql to that list (or only emitting on explicit Run) before this starts drowning the dashboards.

Otherwise LGTM.

@davethegut

Copy link
Copy Markdown
Collaborator

Also, @KDKHD, noticed this weird bug when invoking a threat hunt? Dk if it is just my machine?

Clipboard-20260522-181337-405.mp4

davethegut
davethegut previously approved these changes May 22, 2026

@davethegut davethegut left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

left a comment, but still approving this PR :)

KDKHD added 2 commits May 26, 2026 11:49
Memoize the derived bootstrap hook result so views do not replay startup effects on unrelated rerenders.
@KDKHD

KDKHD commented May 26, 2026

Copy link
Copy Markdown
Member Author

@davethegut The UI bug and the repeated telemetry events bug were related and have been fixed now.

@davethegut davethegut left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Lgtm

@KDKHD KDKHD merged commit c8b71e0 into main May 26, 2026
1 check passed
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