Expose Ai::UsageEvent to GraphQL AiUsageData
What does this MR do and why?
This MR exposes the Ai::UsageEvent model to GraphQL under the AiUsageData field.
Within the AiUsageData field it exposes the following properties:
- ID
- Timestamp
- User
- Event
For now, this endpoint is meant to use PostgreSQL only. Eventually it will support both PSQL and Clickhouse.
References
GraphQL
GraphQL query
{
group(fullPath: "gitlab-org") {
aiUsageData {
all {
nodes {
id
timestamp
user {
id
username
}
event
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
}
GraphQL output
{
"data": {
"group": {
"aiUsageData": {
"all": {
"nodes": [
{
"id": "gid://gitlab/Ai::UsageEvent/2",
"timestamp": "2025-06-30T11:21:56Z",
"user": {
"id": "gid://gitlab/User/1",
"username": "root"
},
"event": "TROUBLESHOOT_JOB"
},
{
"id": "gid://gitlab/Ai::UsageEvent/1",
"timestamp": "2025-07-01T11:21:51Z",
"user": {
"id": "gid://gitlab/User/1",
"username": "root"
},
"event": "TROUBLESHOOT_JOB"
}
],
"pageInfo": {
"endCursor": "eyJpZCI6IjEifQ",
"hasNextPage": false
}
}
}
}
},
"correlationId": "01JZAVZDHHVWSPWZBJZYSB2GA0"
}
PSQL Query performance
Query ran
Calling Ai::UsageEventsFinder.new(user, resource: namespace):
SELECT
*
FROM ( WITH RECURSIVE "array_cte" AS MATERIALIZED (
SELECT
"namespaces"."id"
FROM
"namespaces"
WHERE (
traversal_ids @> (
'{9970}'
)
)
),
"recursive_keyset_cte" AS (
(
SELECT
NULL::ai_usage_events AS records,
array_cte_id_array,
ai_usage_events_timestamp_array,
ai_usage_events_id_array,
0::bigint AS count
FROM (
SELECT
ARRAY_AGG("array_cte"."id") AS array_cte_id_array,
ARRAY_AGG("ai_usage_events"."timestamp") AS ai_usage_events_timestamp_array,
ARRAY_AGG("ai_usage_events"."id") AS ai_usage_events_id_array
FROM (
SELECT
"array_cte"."id"
FROM
array_cte) array_cte
LEFT JOIN LATERAL (
SELECT
"ai_usage_events"."timestamp" AS timestamp,
"ai_usage_events"."id" AS id
FROM
"ai_usage_events"
WHERE
"ai_usage_events"."timestamp" <= '2025-07-15 19:53:19.629219'
AND "ai_usage_events"."namespace_id" = "array_cte"."id"
ORDER BY
"ai_usage_events"."timestamp" DESC,
"ai_usage_events"."id" DESC
LIMIT 1) ai_usage_events ON TRUE
WHERE
"ai_usage_events"."timestamp" IS NOT NULL
AND "ai_usage_events"."id" IS NOT NULL) array_scope_lateral_query
LIMIT 1)
UNION ALL (
SELECT
(
SELECT
ai_usage_events
FROM
"ai_usage_events"
WHERE
"ai_usage_events"."id" = recursive_keyset_cte.ai_usage_events_id_array[position]
LIMIT 1),
array_cte_id_array,
recursive_keyset_cte.ai_usage_events_timestamp_array[:position_query.position - 1] || next_cursor_values.timestamp || recursive_keyset_cte.ai_usage_events_timestamp_array[position_query.position + 1:],
recursive_keyset_cte.ai_usage_events_id_array[:position_query.position - 1] || next_cursor_values.id || recursive_keyset_cte.ai_usage_events_id_array[position_query.position + 1:],
recursive_keyset_cte.count + 1
FROM
recursive_keyset_cte,
LATERAL (
SELECT
timestamp,
id,
position
FROM
UNNEST(ai_usage_events_timestamp_array, ai_usage_events_id_array)
WITH ORDINALITY AS u (timestamp, id, position)
WHERE
timestamp IS NOT NULL
AND id IS NOT NULL
ORDER BY
1 DESC,
2 DESC
LIMIT 1) AS position_query,
LATERAL (
SELECT
"record"."timestamp",
"record"."id"
FROM (
VALUES (NULL, NULL)) AS nulls
LEFT JOIN (
SELECT
"ai_usage_events"."timestamp" AS timestamp, "ai_usage_events"."id" AS id
FROM
"ai_usage_events"
WHERE
"ai_usage_events"."timestamp" <= '2025-07-15 19:53:19.629219'
AND "ai_usage_events"."namespace_id" = recursive_keyset_cte.array_cte_id_array[position]
AND (("ai_usage_events"."timestamp",
"ai_usage_events"."id") < (recursive_keyset_cte.ai_usage_events_timestamp_array[position],
recursive_keyset_cte.ai_usage_events_id_array[position]))
ORDER BY
"ai_usage_events"."timestamp" DESC,
"ai_usage_events"."id" DESC
LIMIT 1) record ON TRUE
LIMIT 1) AS next_cursor_values))
SELECT
(records).*
FROM
"recursive_keyset_cte" AS "ai_usage_events"
WHERE (count <> 0)) ai_usage_events
LIMIT 100
Query plan
https://console.postgres.ai/gitlab/gitlab-production-main/sessions/41528/commands/127573
How to set up and validate locally
Note: If you haven't set up Duo enterprise locally, you can apply this patch to override the check:
Index: ee/app/models/ee/user.rb
===================================================================
diff --git a/ee/app/models/ee/user.rb b/ee/app/models/ee/user.rb
--- a/ee/app/models/ee/user.rb (revision Staged)
+++ b/ee/app/models/ee/user.rb (date 1751637798349)
@@ -528,6 +528,8 @@
end
def assigned_to_duo_enterprise?(container)
+ return true
+
namespace = ::Gitlab::Saas.feature_available?(:gitlab_duo_saas_only) ? container.root_ancestor : nil
GitlabSubscriptions::AddOnPurchase
- In rails console enable the feature flag
:unified_ai_events_graphqlvia http://gdk.test:3000/rails/features - Visit the GraphQL explorer: http://gdk.test:3000/-/graphql-explorer
- Enter the GraphQL query above and confirm the output
- If you don't have any usage events, you should be able to populate by running
FILTER=ai_usage_stats bundle exec rake db:seed_fu
MR 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.
Related to #549782 (closed)
Edited by Robert Hunt


