Loading
feat(analytics): emit pageInfo on aggregated connections
What does this MR do and why?
Expose pagination info in analytics sources.
This also fixes a live bug in analytics list/table displays: the Load more button already rendered (its hasNextPage check in resolver.vue only needs data.count, which aggregated already returned), but loadMore reads its cursor from this.data.pageInfo?.endCursor. With no pageInfo selected, it sent no cursor and appended page 1 again as duplicate rows.
Example Usage
Build the Ruby extension once:
cd glql_rb
bundle install
bundle exec rake compileDuo Workflow sessions per flow type, paged three rows at a time
mode: analytics
query: type = DuoWorkflow and group = "gitlab-org" and created > -30d
dimensions: flowType
metrics: totalCount
sort: totalCount descTest via ruby extension
Analytics data needs a token: export GITLAB_TOKEN before running.
cd glql_rb # after the build prerequisite above
bundle exec ruby -Ilib - <<'RUBY'
require "gitlab_query_language"
require "json"
require "net/http"
query = 'type = DuoWorkflow and group = "gitlab-org" and created > -30d'
context = { mode: "analytics", dimensions: "flowType", metrics: "totalCount", sort: "totalCount desc" }
# 1. Compile GLQL -> GraphQL (also returns the typed `fields` the transform needs).
compiled = Glql.compile(query, context)
puts "== Generated GraphQL =="
puts compiled["output"]
# 2. Execute against GitLab. `limit: 3` leaves more pages behind; set `after:`
# to the previous run's `pageInfo.endCursor` to fetch the next page.
uri = URI("https://gitlab.com/api/graphql")
http = Net::HTTP.new(uri.host, uri.port); http.use_ssl = true
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req["PRIVATE-TOKEN"] = ENV.fetch("GITLAB_TOKEN")
req.body = { query: compiled["output"], variables: { limit: 3 } }.to_json
response = JSON.parse(http.request(req).body)
# 3. Transform the response back into flat rows. Pass response["data"] (the
# GraphQL `data` object, not the whole envelope) and reuse the compiled
# `fields`, which carry the resolved parameters/aliases.
transformed = Glql.transform(response["data"], { mode: context[:mode], fields: compiled["fields"] })
puts "\n== Transformed rows =="
puts JSON.pretty_generate(transformed["data"])
RUBYGenerated GraphQL (trimmed to the aggregated selection):
...
duoWorkflows(createdAtFrom: "2026-08-17 23:59") {
aggregated(before: $before, after: $after, first: $limit, orderBy: [{direction: DESC, identifier: "totalCount"}]) {
count
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
nodes {
dimensions {
flowType: workflowDefinition
}
totalCount
}
}
}
...Transformed rows, run 1 with variables: { limit: 3 } (real output from gitlab.com):
{
"count": 31,
"pageInfo": {
"startCursor": "MA",
"endCursor": "Mg",
"hasNextPage": true,
"hasPreviousPage": false
},
"nodes": [
{ "totalCount": 56497, "flowType": "developer/v1" },
{ "totalCount": 24701, "flowType": "chat" },
{ "totalCount": 19735, "flowType": "ai_catalog_agent" }
]
}Run 2 with variables: { limit: 3, after: "Mg" } - the next page comes back:
{
"count": 31,
"pageInfo": {
"startCursor": "Mw",
"endCursor": "NQ",
"hasNextPage": true,
"hasPreviousPage": true
},
"nodes": [
{ "totalCount": 16039, "flowType": "code_review/v1" },
{ "totalCount": 821, "flowType": "fix_pipeline/v1" },
{ "totalCount": 761, "flowType": "software_development" }
]
}How to set up and validate locally
cargo test- all green. The new codegen test alone:cargo test --test code_suggestion_tests test_aggregated_selects_page_info(the transform-sidepageInfopassthrough assertion now lives intest_transform_basic_analytics_response).sh scripts/fetch_gitlab_graphql_schema.sh && npm ci && DUMP_GRAPHQL=1 cargo test -- --test-threads=1 && npm run test:graphql- everyaggregateddocument in the dump validates withpageInfoagainst the master schema.--test-threads=1sidesteps a pre-existing, unrelated race in the subquery variable counter under threadedcargo test; CI usescargo nextestand is not affected.- Run the Example Usage snippet with
limit: 3:pageInfo.hasNextPageistrueandendCursoris set. Re-run withafter:set to that cursor: the next three rows come back withhasPreviousPage: true.
Edited by Duo Developer