feat(telemetry): distinguish MCP tool calls from direct CLI use

What

Adds an invocation_source property to gitlab_cli_command_used so MCP tool calls can be told apart from the same command run directly.

Why

MCP tool calls already emit telemetry. executeGlabCommand spawns the same binary, so every tool call is an ordinary glab mr list process that runs the telemetry hook like any other invocation.

The problem is that a tool call and the same command typed into an agent's terminal produce identical events. Both carry the same command_and_subcommand, and the subprocess inherits the agent's environment, so coding_agent is the same on both. Today they are one undifferentiated bucket, and there is no way to ask "which MCP tools are actually being used".

Since command_and_subcommand already carries the command path, and MCP tool names map onto it one to one (glab_mr_list → mr list), one property is enough to get per-tool counts.

How

  • internal/api/invocation_source.go: GLAB_INVOCATION_SOURCE and DetectInvocationSource(), mirroring the existing DetectCodingAgent(). The value is validated, because anything in the environment is caller-controlled.
  • internal/commands/mcp/serve: tool-call subprocesses get the stamp. It is appended last so a value already present in the client's environment cannot disguise a tool call as a direct invocation, and there's a test for that.
  • cmd/glab/telemetry.go: reported as invocation_source, absent rather than empty for direct invocations, matching how coding_agent behaves.

No new event, no new metric, and nothing new about the user is collected: the command path was already being sent.

Verified end to end

Pointed a local collector at glab and captured the real events. Same command, three ways:

command coding_agent invocation_source
mr list typed directly claude-code… absent
mr list via MCP tool call claude-code… mcp
mcp serve (the session itself) claude-code… absent

That last row is worth keeping: it gives sessions, so sessions-per-tool-call is available as a ratio.

lefthook run pre-push clean. Rebased onto main after !3932 (merged) merged; both conflicts were additive (an import line and two adjacent test functions) and the feature was re-verified end to end afterwards.

Dependency (satisfied)

The matching entry in config/events/gitlab_cli_command_used.yml merged on 2026-09-18 in gitlab!256199 (merged), so this is no longer blocked.

It had to go first because Gitlab::Tracking::EventValidator raises InvalidPropertyError through track_and_raise_for_dev_exception for any undeclared property. In production that reports rather than raises, so events would still have landed, but every MCP tool call would have reported an error.

Note for whoever queries this

Custom properties land in the nested extra field of the gitlab_standard Snowplow context, not as flat warehouse columns. That is already true of coding_agent and command_and_subcommand, so existing glab queries already reach into extra, but invocation_source will not appear as a column without Data team work.

🤖 Generated with Claude Code

Edited by Kai Armstrong

Merge request reports

Loading
Loading