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
30dis one argument token, so it reads like the relative dates in filters."30d"andgranularity=30dwork too. - Constraint: a
Granularityparameter kind accepts the three calendar values or1d..399d(the engine's cap). Out-of-range, zero-padded or bare-number spellings get a compile error naming the accepted forms; a signed-30dparses 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 tomonthlyorweekly/monthly; the engine accepts every value there (verified live on gitlab.com). - Schema document: the parameter publishes as
"kind": "Granularity"withvalues,min_daysandmax_days, and the kind is described invalue_kinds. - Sorting on a fixed-day dimension carries the granularity into
orderBylike 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:graphqlOr through the Ruby gem against gitlab.com (see Example Usage below).
Example Usage
cd glql_rb
bundle install
bundle exec rake compileDuo 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 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 = "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"])
RUBYquery 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 ascTest 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 descTest 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 issues
Related to #187 (closed)