feat: add createdByDuo dimension/filter and acceptanceRate metric for MR analytics

What does this MR do and why?

Adds createdByDuo and acceptanceRate to the MergeRequests analytics source, so the DAP Impact v1 dashboard's Work tab can render its "MR cycle time: median and p75, Duo vs non-Duo" panel.

The GitLab MergeRequests aggregation engine (gitlab-org/gitlab#608191, shipping in 19.5) exposes three things GLQL did not yet know about:

Engine addition GraphQL name Exposed in GLQL as
Dimension createdByDuo: Boolean on MergeRequestsAggregationResponseDimensions dimension + sort field
Filter argument createdByDuo: [Boolean!] on analytics.mergeRequests filter field (= only)
Metric acceptanceRate: Float on MergeRequestsAggregationResponse metric + sort field

Two details worth a reviewer's attention:

  • createdByDuo accepts = and !=. The engine argument is a [Boolean!] allow-list with no negation, but no operator restriction is needed: transform_expression rewrites (_, NotEqual, Bool(b)) to (Equal, !b) before codegen, so createdByDuo != true compiles to createdByDuo: [false]. in is still rejected, since BooleanLike does not admit it.
  • A new bool_list_filter_value helper wraps the scalar into the one-element list the engine wants (createdByDuo: [true]), mirroring the existing string_list_filter_value and lowercase_string_list_filter_value helpers next to it.

AcceptanceRate already existed as a field (CodeSuggestions uses it), so only CreatedByDuo is a new Field variant.

The other two commits are supporting work:

  • test(merge_requests): the new dimension tests asserted dimensions { and the field name as two independent substrings of the compiled query, which held whether or not the field landed inside the block — a name appearing only as a filter argument satisfied both. A new dimensions_block test helper slices the body of the dimensions { … } selection so the assertions pin placement, and the metric test now also pins the negative (acceptanceRate must sit outside the block).
  • refactor(analytics): review feedback — drops the operator restriction described above, and documents bool_list_filter_value's unreachable List arm as defensive, matching how string_list_filter_value documents its Token arm.

Example Usage

Build prerequisite, named once:

cd glql_rb
bundle install
bundle exec rake compile

Duo vs non-Duo cycle time (the dashboard panel)

mode: analytics
query: type = MergeRequest and group = "gitlab-org" and merged > -30d
dimensions: createdByDuo
metrics: timeToMergeQuantile(0.5) as "median", timeToMergeQuantile(0.75) as "p75", acceptanceRate
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   = "type = MergeRequest and group = \"gitlab-org\" and merged > -30d"
context = {
  mode:       "analytics",
  dimensions: "createdByDuo",
  metrics:    "timeToMergeQuantile(0.5) as \"median\", timeToMergeQuantile(0.75) as \"p75\", acceptanceRate"
}

compiled = Glql.compile(query, context)
puts "== Generated GraphQL =="
puts compiled["output"]

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")
# Analytics needs read_cycle_analytics, so a token is required here:
req["PRIVATE-TOKEN"] = ENV.fetch("GITLAB_TOKEN")
req.body = { query: compiled["output"], variables: { limit: 3 } }.to_json
response = JSON.parse(http.request(req).body)

transformed = Glql.transform(response["data"], { mode: "analytics", fields: compiled["fields"] })
puts "\n== Transformed rows =="
puts JSON.pretty_generate(transformed["data"])
RUBY

Generated GraphQL:

query GLQL($before: String, $after: String, $limit: Int) {
  group(fullPath: "gitlab-org") {
    analytics {
      mergeRequests(metricMergedAtFrom: "2026-08-18 23:59") {
        aggregated(before: $before, after: $after, first: $limit) {
          count
          nodes {
            dimensions {
              createdByDuo
            }
            timeToMerge {
              timeToMergeQuantile_quantile_0_d5: quantile(quantile: 0.5)
            }
            timeToMerge {
              timeToMergeQuantile_quantile_0_d75: quantile(quantile: 0.75)
            }
            acceptanceRate
          }
        }
      }
    }
  }
}

Transformed rows — one row per createdByDuo value, with the median / p75 aliases applied by the transformer (they are not in the wire query):

{
  "nodes": [
    {
      "createdByDuo": true,
      "median": 143520000,
      "p75": 402180000,
      "acceptanceRate": 0.9124,
      "timeToMergeQuantile_quantile_0_d5": 143520000,
      "timeToMergeQuantile_quantile_0_d75": 402180000
    },
    {
      "createdByDuo": false,
      "median": 268140000,
      "p75": 720300000,
      "acceptanceRate": 0.8341,
      "timeToMergeQuantile_quantile_0_d5": 268140000,
      "timeToMergeQuantile_quantile_0_d75": 720300000
    }
  ]
}

The GraphQL above is the compiler's real output. The rows are the real transform output over a representative engine response rather than live data: analytics.mergeRequests requires read_cycle_analytics, so an unauthenticated call returns The following sources are not accessible: gitlab-org. That response does confirm the API accepts the query against its schema — the failure is authorization, not an unknown field or argument. Re-run the snippet above with a GITLAB_TOKEN for live numbers.

Filtering to Duo-created merge requests

mode: analytics
query: type = MergeRequest and group = "gitlab-org" and createdByDuo = true
dimensions: createdByDuo
metrics: acceptanceRate
Test via ruby extension

Same snippet as above with:

query   = "type = MergeRequest and group = \"gitlab-org\" and createdByDuo = true"
context = { mode: "analytics", dimensions: "createdByDuo", metrics: "acceptanceRate" }

Generated GraphQL — note the scalar true compiled to the [Boolean!] list the engine expects:

query GLQL($before: String, $after: String, $limit: Int) {
  group(fullPath: "gitlab-org") {
    analytics {
      mergeRequests(createdByDuo: [true]) {
        aggregated(before: $before, after: $after, first: $limit) {
          count
          nodes {
            dimensions {
              createdByDuo
            }
            acceptanceRate
          }
        }
      }
    }
  }
}

How to set up and validate locally

  1. Check out the branch and build:

    git checkout feat/created-by-duo-mr-analytics
    cargo build
  2. Run the suite. The MergeRequests analytics tests carry 11 new cases covering the dimension, the = true / = false filters, the metric, both sort identifiers, the != rejection, acceptanceRate being rejected as a dimension, the end-to-end dashboard query, and the boolean dimension surviving the transform:

    cargo test --test merge_request_analytics_tests
  3. Confirm schema.json is exactly what the generator produces (CI's schema job checks this):

    cargo run --bin generate-schema && npm run lint:prettier:fix
    git diff --stat src/schema/schema.json   # expected: no output
  4. Validate every compiled query in the suite against GitLab's real schema:

    DUMP_GRAPHQL=1 cargo test && npm run test:graphql

    This may report two invalid documents — duplicate $epicId1 / $parentId1 variable definitions. They are pre-existing and unrelated to this MR, and they are not about any particular query: unique_id counters are process-global and compile resets them, so concurrent compiles interfere. cargo test runs tests as threads in one process and can trigger it; re-running with -- --test-threads=1 yields a clean corpus and exit 0, which isolates concurrency as the cause. CI is unaffected because cargo nextest run is process-per-test. Reported in #217 rather than fixed here.

  5. Reproduce the examples above with the Ruby extension:

    cd glql_rb && bundle install && bundle exec rake compile

The user-facing docs table (doc/user/glql/data_sources/merge_request_analytics.md) follows in gitlab-org/gitlab after the release and version bump; series labels for true / false are a frontend concern.

Edited by Chandra Saripaka

Merge request reports

Loading
Loading