Add "Top tools used by agents" card to AI Governance dashboard
What
Adds a Top tools used by agents card to the AI Governance dashboard, at group and project scope. It lists the top 10 tools agents invoked over the dashboard timeframe, each with an invocation count.
All data comes from the existing ai_audit_events store (event_name = 'ai_tool_invoked', tool name in details['tool_name']), so nothing new is captured. Implements #618956.
Counts show Duo Agent Platform activity only for now (external agents do not emit tool events yet) and include a synthetic final-response call. Both are noted on the card.
How
- New GraphQL field
aiGovernanceToolCalls(timeframe:)onGroupandProject, returningAiGovernanceToolCallsReport { approximate, nodes { toolName, count } }. Ai::Governance::ToolCallsServicedispatches to ClickHouse (production) or PostgreSQL (self-managed / local); both return the same shape.- Frontend:
tool_calls_card.vue, shown on the dashboard, with an "approximate counts" note when the backend flags it.
Failure rate was dropped for beta: the producer under-captures failures, so the rate read misleadingly low. Counts only.
Database
ClickHouse aggregates server-side (GROUP BY tool_name, ORDER BY calls DESC, tool_name, top 10). PostgreSQL is the self-managed fallback: details is serialized YAML, so it tallies in Ruby, bounded by the timeframe and capped at MAX_EVENTS = 10_000 rows ordered by id. When the cap is hit, approximate: true; the cap-overflow check uses the same id order, so the counted set and the flag stay consistent across refreshes. Namespace scoping uses a UNION ALL of two indexed arms (in_namespace_hierarchy_union) rather than an OR that can't use both indexes.
Worst-case query (group scope, PostgreSQL):
SELECT ai_audit_events.*
FROM (
(SELECT ai_audit_events.* FROM ai_audit_events
WHERE project_id IN (SELECT id FROM projects WHERE namespace_id IN
(SELECT id FROM namespaces WHERE type = 'Group' AND traversal_ids @> ('{<root_group_id>}'))))
UNION ALL
(SELECT ai_audit_events.* FROM ai_audit_events
WHERE namespace_id IN (SELECT id FROM namespaces WHERE type = 'Group' AND traversal_ids @> ('{<root_group_id>}')))
) ai_audit_events
WHERE event_name = 'ai_tool_invoked' AND created_at BETWEEN '<from>' AND '<to>'
ORDER BY id
LIMIT 10000This PostgreSQL query runs only on self-managed instances without ClickHouse. On GitLab.com, ai_tool_invoked events are written to ClickHouse, not Postgres (ProcessAuditEventsWorker#store_audit_events branches on Gitlab::ClickHouse.globally_enabled_for_analytics?), and the card is served by the ClickHouse path there.
Database Lab plan (namespace 9970, gitlab-org): https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/56470/commands/160743 — the planner prunes to the current monthly partitions and keeps the per-partition project_id/namespace_id + created_at indexes available; ~0.2 ms. It returns 0 rows by design: Database Lab clones production Postgres, which holds no ai_tool_invoked rows because on GitLab.com they live in ClickHouse (self-managed instances write them to Postgres, where this query has data). The reviewable part is the shape: partition pruning + a cheap top-level sort/limit, index-driven per arm.
Feature flag
The GraphQL field is behind ai_governance_dashboard (default off; returns null when disabled). The card additionally has its own flag, ai_governance_tool_calls (wip, default off), nested inside ai_governance_dashboard, so we can decide independently whether it ships in beta (same pattern as the MCP server activity card). Both flags must be on for the card to render. The field is additive and nullable, so it is multi-version safe.
