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 compile

Sessions 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 desc
Test 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"])
RUBY

Generated 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: creditsUsedSum

Compiles 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.

Edited by Abhinaba Ghosh

Merge request reports

Loading
Loading