feat(duo_workflows): add model dimension
What does this MR do and why?
Adds a model dimension to the duoWorkflows source, rendered as model: modelUsed and sortable, mirroring how flowType aliases workflowDefinition. The DuoWorkflows aggregation engine already exposes modelUsed; the compiler just had no field for it, so credits could not be broken down by model.
Filter support (model in (...)) is deliberately left out: the engine does not expose a modelUsed filter argument yet, so the compiler side follows once that schema exists.
References
Needed for the DAP Impact Spend tab: https://gitlab.com/groups/gitlab-org/-/work_items/23470
Example Usage
Build prerequisite:
cd glql_rb
bundle install
bundle exec rake compileSessions by model and flow type
mode: analytics
query: type = DuoWorkflow and created >= "2026-08-18" and created <= "2026-09-16"
dimensions: model, flowType
metrics: totalCount
sort: totalCount descTest via ruby extension
cd glql_rb # after the build prerequisite above
bundle exec ruby -Ilib - <<'RUBY'
require "gitlab_query_language"
require "json"
require "net/http"
query = %q(type = DuoWorkflow and created >= "2026-08-18" and created <= "2026-09-16")
context = { mode: "analytics", dimensions: "model, flowType", metrics: "totalCount", sort: "totalCount desc", group: "gitlab-org" }
# 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. Analytics data is private, so a token is required:
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, reusing the compiled fields.
result = Glql.transform(response["data"], { fields: compiled["fields"], source: compiled["source"], mode: compiled["mode"] })
puts "== Rows =="
puts JSON.pretty_generate(result["data"])
RUBYGenerated GraphQL:
query GLQL($before: String, $after: String, $limit: Int) {
group(fullPath: "gitlab-org") {
analytics {
duoWorkflows(createdAtFrom: "2026-08-18 00:00", createdAtTo: "2026-09-16 23:59") {
aggregated(before: $before, after: $after, first: $limit, orderBy: [{direction: DESC, identifier: "totalCount"}]) {
count
nodes {
dimensions {
model: modelUsed
flowType: workflowDefinition
}
totalCount
}
}
}
}
}
}Transformed rows (gitlab.com, limit: 3 of 31 buckets):
{
"count": 31,
"nodes": [
{ "totalCount": 56783, "model": "", "flowType": "developer/v1" },
{ "totalCount": 24735, "model": "", "flowType": "chat" },
{ "totalCount": 19759, "model": "", "flowType": "ai_catalog_agent" }
]
}Every bucket returns model: "" on gitlab.com today because model_used is not populated there yet (ingestion is behind duo_workflows_session_credits_ingestion) — see the discussion below.
Credits by model, by capability (DAP Impact Spend tab)
mode: analytics
query: type = DuoWorkflow and created >= "2026-08-16" and created <= "2026-09-15"
dimensions: model, flowType
metrics: creditsUsedSumCompiles to the same shape with creditsUsed { creditsUsedSum: sum } as the metric selection. Verified end to end in GDK against seeded duo_workflows_workflows_enriched data; gitlab.com's deployed engine predates the creditsUsed measurement ("Metrics at least one metric is required"), so run this one against master/GDK.
Schema document regenerated; DUMP_GRAPHQL=1 cargo test && npm run test:graphql passes against the current master schema.