feat: accept fixed-day Xd granularities on every date dimension

What does this MR do and why?

Every granularity parameter on an analytics date dimension accepts a fixed number of days alongside daily/weekly/monthly: created(30d), merged("30d"), timestamp(granularity=399d). The value is passed to the engine's date_bucket as a string, which buckets rows into fixed N-day windows counted back from the Unix epoch. This is the first half of #187 (closed): the building block for "this period vs. last period" comparisons in the DAP Impact Dashboard. The optional origin anchor follows in a separate MR.

  • Parser: a bare 30d is one argument token, so it reads like the relative dates in filters. "30d" and granularity=30d work too.
  • Constraint: a Granularity parameter kind accepts the three calendar values or 1d..399d (the engine's cap). Out-of-range, zero-padded or bare-number spellings get a compile error naming the accepted forms; a signed -30d parses and is refused by the same check.
  • One shared set: all date dimensions on all analytics sources accept the same values. Code suggestions (timestamp) and agent platform sessions (created) no longer restrict to monthly or weekly/monthly; the engine accepts every value there (verified live on gitlab.com).
  • Schema document: the parameter publishes as "kind": "Granularity" with values, min_days and max_days, and the kind is described in value_kinds.
  • Sorting on a fixed-day dimension carries the granularity into orderBy like the calendar values do. Labels are unchanged: a parameterised dimension is labelled by its field name.

Fixed-day granularities need GitLab 19.5 (gitlab!254135 (merged) and gitlab!254973 (merged)). Older instances reject the value with "Invalid value(s) for parameter granularity". User docs in gitlab-org/gitlab will follow with the package bump.

How to set up and validate locally

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

Or through the Ruby gem against gitlab.com (see Example Usage below).

Example Usage

cd glql_rb
bundle install
bundle exec rake compile

Duo workflow sessions in 30-day buckets

mode: analytics
query: type = duoworkflow and group = "gitlab-org" and created > -90d
dimensions: created(30d)
metrics: totalCount, usersCount
sort: created 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   = "type = duoworkflow and group = \"gitlab-org\" and created > -90d"
context = { mode: "analytics", dimensions: "created(30d)", metrics: "totalCount, usersCount", sort: "created desc" }

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")
req["PRIVATE-TOKEN"] = ENV.fetch("GITLAB_TOKEN")
req.body = { query: compiled["output"], variables: { limit: 4 } }.to_json
response = JSON.parse(http.request(req).body)

transformed = Glql.transform(response["data"], { mode: context[:mode], fields: compiled["fields"] })
puts "\n== Transformed rows =="
puts JSON.pretty_generate(transformed["data"])
RUBY
query GLQL($before: String, $after: String, $limit: Int) {
  group(fullPath: "gitlab-org") {
    analytics {
      duoWorkflows(createdAtFrom: "2026-06-20 23:59") {
        aggregated(before: $before, after: $after, first: $limit, orderBy: [{direction: DESC, identifier: "createdAt", parameters: {granularity: "30d"}}]) {
          count
          pageInfo { startCursor endCursor hasNextPage hasPreviousPage }
          nodes {
            dimensions {
              createdAt(granularity: "30d")
            }
            totalCount
            usersCount
          }
        }
      }
    }
  }
}
{
  "count": 4,
  "nodes": [
    { "usersCount": 961,  "totalCount": 58092,  "created": "2026-09-04" },
    { "usersCount": 1150, "totalCount": 131359, "created": "2026-08-05" },
    { "usersCount": 1200, "totalCount": 121368, "created": "2026-07-06" },
    { "usersCount": 1034, "totalCount": 62043,  "created": "2026-06-06" }
  ]
}

Merged MRs per 30-day window, aliased

mode: analytics
query: type = mergerequest and group = "gitlab-org" and mergedAt > -60d
dimensions: merged(30d) as "Period"
metrics: totalCount
sort: merged asc
Test via ruby extension

Same snippet as above with this query and context. Generated selection and rows:

mergeRequests(metricMergedAtFrom: "2026-07-20 23:59") {
  aggregated(before: $before, after: $after, first: $limit, orderBy: [{direction: ASC, identifier: "metricMergedAt", parameters: {granularity: "30d"}}]) {
    nodes {
      dimensions {
        mergedAt_granularity_30d: metricMergedAt(granularity: "30d")
      }
      totalCount
    }
  }
}
{
  "count": 3,
  "nodes": [
    { "totalCount": 4298,  "Period": "2026-07-06" },
    { "totalCount": 11456, "Period": "2026-08-05" },
    { "totalCount": 6537,  "Period": "2026-09-04" }
  ]
}

Code suggestions per day (previously monthly only)

mode: analytics
query: type = codesuggestion and group = "gitlab-org" and timestamp > -14d
dimensions: timestamp(daily)
metrics: totalCount
sort: timestamp desc
Test via ruby extension

Same snippet with this query and context. Rows returned by gitlab.com:

{
  "count": 13,
  "nodes": [
    { "totalCount": 108,  "timestamp": "2026-09-18" },
    { "totalCount": 1123, "timestamp": "2026-09-17" },
    { "totalCount": 702,  "timestamp": "2026-09-16" },
    { "totalCount": 313,  "timestamp": "2026-09-15" }
  ]
}

Related to #187 (closed)

Edited by Daniele Rossetti

Merge request reports

Loading
Loading