Read external agent metrics from ai_governance_sessions in ClickHouse
What does this MR do and why?
The AI Governance dashboard reads aiGovernanceMetrics (sessions and agents KPIs with trends, top users, top projects, connected agents). Today the ClickHouse metrics only read Duo workflows (siphon_duo_workflows_workflows). External agents such as Claude Code are moving to ai_governance_sessions, so they would show zero.
Behind the ai_governance_sessions_api feature flag (off by default), and when ClickHouse is enabled for analytics:
agentClass: EXTERNALreads external sessions fromsiphon_ai_governance_sessions(sourcesclaude_code_compliance_apiandclaude_code_glab). Session start time is the time dimension.flow_typeis used to exclude chat.INTERNAL_DAPis unchanged (Duo workflows with no agent type).ALLcombines Duo workflows and external sessions in one SQLUNION ALL. Session rows with sourcegitlab_duoare always skipped. Every Duo workflow is also synced to the sessions table, so this avoids double counting.- Session rows are deduplicated like the existing session distribution service (latest version per row, deleted rows dropped). They are scoped to the group or project hierarchy.
- Connected agents activity (session count and last session per agent type) also reads external sessions from ClickHouse, in a new
Ai::Governance::ClickHouseConnectedAgentActivityService.
With the flag off, or ClickHouse disabled, behaviour is unchanged. The PostgreSQL path is a follow-up. It needs agent_identity_id on the Postgres table from !257557 (merged).
The flag YAML is also added in !257545 (merged). This MR has an identical copy so it can merge on its own.
Known limitation: with the flag on, external rows still in the old workflows table are not counted. There are none on GitLab.com today.
No GraphQL schema, frontend, or migration changes.
References
- Resolves part of https://gitlab.com/gitlab-org/gitlab/-/work_items/630712
- Epic: https://gitlab.com/groups/gitlab-org/-/work_items/21540
- Tracking issue: https://gitlab.com/gitlab-org/gitlab/-/work_items/630862
- Follows the pattern of !258249 (merged)
Screenshots or screen recordings
Not applicable. Backend only.
How to set up and validate locally
-
Make sure ClickHouse is enabled for analytics in GDK. In a Rails console run
Feature.enable(:ai_governance_sessions_api). -
Create an external session in the console. Wait a few seconds for Siphon (it copies PostgreSQL rows to ClickHouse).
project = Project.first Ai::Governance::Session.create!(namespace: project.project_namespace, project: project, user: User.first, source: :claude_code_glab, external_xid: SecureRandom.uuid, agent_type: 'claude-code', session_started_at: Time.current) -
In GraphiQL at
/-/graphql-explorerrun:query { group(fullPath: "gitlab-org") { aiGovernanceMetrics(agentClass: EXTERNAL, timeframe: LAST_30_DAYS) { sessions { count } agents { count } } } }The external session is counted. Run it again with
agentClass: ALL. Duo sessions are counted once. -
Disable the flag and run the query again.
EXTERNALfalls back to the previous behaviour.
Query plan
This section shows the query plan for the new ClickHouse query in Ai::Governance::ClickHouseConnectedAgentActivityService, which reads siphon_ai_governance_sessions. I ran it on a local GDK ClickHouse (version 26.8) with EXPLAIN indexes = 1, for the group with the most sessions (traversal path 1/1000000/100/). The primary key (traversal_path, session_started_at, id) limits the read to that group's path range through binary search: 1 of 2 parts and 1 of 2 granules were read, taking 4.8 ms, reading 493 rows and about 29 KB. Local data is tiny (495 rows total), so this shows that the index is used, not what the query costs at GitLab.com scale. The inner query has no lower time bound on purpose; see the reasoning.
Raw SQL
SELECT
agent_type,
countIf(session_started_at >= toDateTime64('2026-09-01 00:00:00.000000', 6, 'UTC')
AND session_started_at < toDateTime64('2026-09-30 12:00:00.000000', 6, 'UTC')) AS session_count,
max(session_started_at) AS last_session_at
FROM (
SELECT
id,
session_started_at,
argMax(source, _siphon_replicated_at) AS source,
argMax(agent_type, _siphon_replicated_at) AS agent_type,
argMax(agent_identity_id, _siphon_replicated_at) AS agent_identity_id,
argMax(_siphon_deleted, _siphon_replicated_at) AS deleted
FROM siphon_ai_governance_sessions
WHERE startsWith(traversal_path, '1/1000000/100/')
GROUP BY traversal_path, session_started_at, id
)
WHERE deleted = false
AND source != 0
AND agent_identity_id IS NOT NULL
AND agent_type IN ['claude-code', 'opencode']
GROUP BY agent_typeEXPLAIN indexes = 1
Output: argMax(agent_type, _siphon_replicated_at), countIf(session_started_at >= '2026-09-01 00:00:00.000000' AND session_started_at < '2026-09-30 12:00:00.000000'), max(session_started_at)
Aggregating
│ Keys: argMax(agent_type, _siphon_replicated_at)
│ Aggregates: countIf(session_started_at >= '2026-09-01 00:00:00.000000' AND session_started_at < '2026-09-30 12:00:00.000000'), max(session_started_at)
│ Skip merging: 0
└──Filter ((WHERE + (Change column names to column identifiers + (Project names + Projection))))
│ Filter column: argMax(_siphon_deleted, _siphon_replicated_at) = 0 AND argMax(source, _siphon_replicated_at) != 0 AND argMax(agent_identity_id, _siphon_replicated_at) IS NOT NULL AND argMax(agent_type, _siphon_replicated_at) IN ('claude-code', 'opencode')
└──Aggregating
│ Keys: traversal_path, session_started_at, id
│ Aggregates: argMax(source, _siphon_replicated_at), argMax(agent_type, _siphon_replicated_at), argMax(agent_identity_id, _siphon_replicated_at), argMax(_siphon_deleted, _siphon_replicated_at)
│ Skip merging: 0
└──ReadFromMergeTree (gitlab_clickhouse_development.siphon_ai_governance_sessions)
Read type: Default
Parts: 1 | Granules: 1
Output: traversal_path, session_started_at, source, _siphon_replicated_at, argMax(agent_type, _siphon_replicated_at), agent_identity_id, _siphon_deleted, id
Prewhere filter
Prewhere filter column: startsWith(traversal_path, '1/1000000/100/')
Indexes:
PrimaryKey
Keys:
traversal_path
Condition: (traversal_path in ['1/1000000/100/', '1/1000000/1000'))
Parts: 1/2
Granules: 1/2
Search Algorithm: binary search
Ranges: 1MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.