Add exact-match filter for model_used on Duo Workflows analytics
What does this MR do and why?
The Duo Workflows aggregation engine (Analytics::AggregationEngines::DuoWorkflows) already exposes model_used as a dimension, so analytics queries could break credits down by model, but there was no filter on it — a query could not scope to a single model. In GLQL, model = "claude-sonnet-4-5" failed to compile with "model is not a recognized field for Duo workflows".
This adds exact_match :model_used to the engine's filters do block, plus its exact_not_match twin. The twin is required, not optional: ee/spec/models/analytics/aggregation_engines/exclusion_filter_coverage_spec.rb asserts across all eight engines that every exact_match has a matching exact_not_match capped at MAX_EXCLUSION_VALUES.
The GraphQL arguments modelUsed: [String!] and modelUsedNot: [String!] are generated from that declaration by the aggregation engine's GraphQL adapter, so no argument is hand-written. The committed GraphQL reference docs and introspection JSON are regenerated accordingly.
Additional notes:
- No new feature flag: the whole
duoWorkflowsfield is already gated behind the default-offdap_impact_v1flag. model_usedis a non-nullable ClickHouseLowCardinality(String)defaulting to'', so flows whose credits could not be attributed to a model are stored as an empty string, not NULL.modelUsedNot: [""]is therefore how a query narrows to model-attributed flows.model_usedis not part of the table's sort key(traversal_path, created_at, id), so the filter scans within the existing scope and date-range filters rather than pruning granules. Fine at current cardinality.- Why this engine can have a
model_usedfilter while theDuoWorkflowModelCreditsengine added in !257760 (not yet merged) deliberately cannot: that engine expands one flow into one row per model witharrayJoin, and filters apply before the expansion, where the exploded model does not exist yet. Heremodel_usedis a plain physical column with no expansion, so an ordinaryexact_matchworks. - This is the engine half only. The dashboard query is unchanged: GLQL cannot emit these arguments until a matching compiler change ships in the gitlab-org/glql project and its gem and npm packages are bumped here. The engine change has to land first, because glql's CI validates its generated queries against this project's master GraphQL schema.
References
- Issue: glql#236
- The GLQL compiler MR that added the
modeldimension but deliberately left filtering out: glql!511 (merged)
Screenshots or screen recordings
Backend-only change with no UI surface, so screenshots do not apply.
How to set up and validate locally
Requires GDK with ClickHouse, the dap_impact_v1 feature flag enabled, and duo_workflows_workflows_enriched seeded.
-
Enable the flag in the rails console:
Feature.enable(:dap_impact_v1) -
Query in GraphiQL, grouping by model and filtering to one, then confirm the totals shrink versus the same query without the filter:
query { group(fullPath: "your-group") { analytics { duoWorkflows(modelUsed: ["claude-sonnet-4-5"]) { aggregated { nodes { dimensions { modelUsed } totalCount } } } } } } -
Then try
modelUsedNot: [""]and confirm only model-attributed flows come back.
Specs to run:
ee/spec/models/analytics/aggregation_engines/exclusion_filter_coverage_spec.rbee/spec/models/analytics/aggregation_engines/duo_workflows_spec.rbee/spec/requests/api/graphql/analytics/duo_workflows_spec.rb
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.