feat(metrics): declarative YAML metric registry (#1974) - #2165
Conversation
|
Warning Review limit reached
Next review available in: 55 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe analytics service now loads builtin sources and metrics from an embedded ChangesDeclarative metric registry
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant registry as Embedded registry.yaml
participant analytics as Analytics metric definitions
participant database as Service database
registry->>analytics: Provides sources and metrics
analytics->>analytics: Deserializes and validates definitions
analytics->>database: Reconciles builtin sources, measures, and metrics
analytics->>database: Disables missing builtin measures
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/backend/services/analytics/src/domain/metric_definitions/seeds.rs (1)
245-255: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winGuard the empty-measures case, which the registry migration makes reachable.
If a source declares
measures: [],measure_keysis empty,placeholdersis an empty string, and the SQL becomesmeasure_key NOT IN (). MariaDB rejects that as a syntax error, so startup reconciliation fails.Before this PR the measure lists were Rust array literals. Now they come from
registry.yaml, wheremeasures: []is valid YAML and no registry test forbids it.disable_missingat lines 275-280 already guards its own empty case, so this inline loop is the only unguarded site.Add the guard, and add a registry invariant test that requires at least one measure per source.
🐛 Proposed fix
for builtin_source in builtin_sources() { let source_id = fetch_source_id(db, &builtin_source.source.key).await?; let measure_keys = builtin_source .measures .iter() .map(String::as_str) .collect::<Vec<_>>(); - let placeholders = vec!["?"; measure_keys.len()].join(", "); - let sql = format!( - "UPDATE metric_source_measures SET is_enabled = FALSE \ - WHERE source_id = ? AND is_enabled = TRUE \ - AND measure_key NOT IN ({placeholders})" - ); + let base_sql = "UPDATE metric_source_measures SET is_enabled = FALSE \ + WHERE source_id = ? AND is_enabled = TRUE"; + let sql = if measure_keys.is_empty() { + base_sql.to_owned() + } else { + let placeholders = vec!["?"; measure_keys.len()].join(", "); + format!("{base_sql} AND measure_key NOT IN ({placeholders})") + };In
builtin.rstests:#[test] fn every_source_declares_at_least_one_measure() { for builtin_source in builtin_sources() { assert!( !builtin_source.measures.is_empty(), "source must declare at least one measure: {:?}", builtin_source.source.key ); } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/services/analytics/src/domain/metric_definitions/seeds.rs` around lines 245 - 255, Guard the inline reconciliation SQL around measure_keys/placeholders so empty measure lists do not generate NOT IN (); skip the update when builtin_source.measures is empty, while preserving the existing behavior for non-empty lists. In the builtin.rs test module, add every_source_declares_at_least_one_measure to assert each builtin source has at least one measure and identify the source key in failures.
🧹 Nitpick comments (2)
src/backend/services/analytics/src/domain/metric_definitions/builtin.rs (2)
152-154: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMove
REGISTRY_YAMLto the module top with the other constants.The coding guidelines require constants at the module top, grouped.
REGISTRY_YAMLand theREGISTRYstatic sit between the type definitions and the accessors.As per coding guidelines: "Define constants at module top, group them, and use unit-suffixed names such as
_BYTES,_SECS, and_DAYS."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/services/analytics/src/domain/metric_definitions/builtin.rs` around lines 152 - 154, Move the REGISTRY_YAML constant to the module’s top-level constants group, alongside the other constants, while leaving REGISTRY and the accessor implementations unchanged.Source: Coding guidelines
199-206: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRegistry shape tests assert without per-case messages. The uniqueness and snake_case asserts report only a line number on failure, so a duplicate or malformed key in a 59-metric registry is hard to locate. The coding guidelines require per-case assertion messages.
src/backend/services/analytics/src/domain/metric_definitions/builtin.rs#L199-L206: bind the key to a local and add messages such as"source key must be snake_case: {key:?}"and"duplicate source key: {key:?}".src/backend/services/analytics/src/domain/metric_definitions/builtin.rs#L223-L244: add the same messages to the measure and dimension asserts inmeasure_and_dimension_keys_are_unique_per_source, and to the uniqueness assert inmetric_keys_are_unique_and_shaped.As per coding guidelines: "Make tests read as specifications: use table-driven loops with per-case assertion messages such as
"should reject: {input:?}"".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/services/analytics/src/domain/metric_definitions/builtin.rs` around lines 199 - 206, The registry shape tests need per-case assertion messages identifying the failing key. In src/backend/services/analytics/src/domain/metric_definitions/builtin.rs lines 199-206, bind each source key locally and include it in the snake_case and duplicate-key assertions; in lines 223-244, add equivalent key-specific messages to the measure and dimension assertions in measure_and_dimension_keys_are_unique_per_source and the uniqueness assertion in metric_keys_are_unique_and_shaped.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/domain/metrics/specs/DESIGN.md`:
- Around line 363-366: Update the documentation around builtin_metrics() to
narrow the build-failure claim so it excludes undetected mistyped optional key
names, or add deny_unknown_fields to the relevant seed structs and retain the
claim only once unknown keys are rejected. Align the wording with the behavior
covered by the builtin.rs registry tests and the consolidated
deny_unknown_fields guidance.
In `@src/backend/services/analytics/src/domain/metric_definitions/builtin.rs`:
- Around line 83-85: Move evidence granularity ownership into the registry: in
src/backend/services/analytics/src/domain/metric_definitions/builtin.rs lines
83-85, replace BuiltinSource.measures entries with a MeasureSeed containing key
and required evidence_granularity, derive deserialization with snake_case
handling for EvidenceGranularity, and remove the hardcoded match fallback in
BuiltinSource::evidence_granularity. In docs/domain/metrics/specs/DESIGN.md
lines 636-637, retain step 5 as registry-driven; in lines 657-662, make the same
case 3 step 2 correction and clarify that Rust changes are still required even
though backend enum or table-name changes are not.
- Around line 110-150: Reject unknown YAML keys during registry deserialization
by adding deny-unknown-field validation to MetricSeed, InputSeed, Registry,
SourceSeed, and BuiltinSource in
src/backend/services/analytics/src/domain/metric_definitions/builtin.rs:65-81
and 110-150, and to ValueTransform in
src/backend/services/analytics/src/domain/metric_definitions/definition.rs:118-119
alongside its default attribute. The claim in
docs/domain/metrics/specs/DESIGN.md:363-366 requires no direct change once
validation is enabled.
In `@src/backend/services/analytics/src/domain/metric_definitions/registry.yaml`:
- Line 507: Update the user-facing label values for all collab metric
definitions in the registry so they use sentence casing, including the listed
Messages, Channel, Files, Meeting, email, and sharing labels. Preserve each
existing short_label value unchanged.
---
Outside diff comments:
In `@src/backend/services/analytics/src/domain/metric_definitions/seeds.rs`:
- Around line 245-255: Guard the inline reconciliation SQL around
measure_keys/placeholders so empty measure lists do not generate NOT IN (); skip
the update when builtin_source.measures is empty, while preserving the existing
behavior for non-empty lists. In the builtin.rs test module, add
every_source_declares_at_least_one_measure to assert each builtin source has at
least one measure and identify the source key in failures.
---
Nitpick comments:
In `@src/backend/services/analytics/src/domain/metric_definitions/builtin.rs`:
- Around line 152-154: Move the REGISTRY_YAML constant to the module’s top-level
constants group, alongside the other constants, while leaving REGISTRY and the
accessor implementations unchanged.
- Around line 199-206: The registry shape tests need per-case assertion messages
identifying the failing key. In
src/backend/services/analytics/src/domain/metric_definitions/builtin.rs lines
199-206, bind each source key locally and include it in the snake_case and
duplicate-key assertions; in lines 223-244, add equivalent key-specific messages
to the measure and dimension assertions in
measure_and_dimension_keys_are_unique_per_source and the uniqueness assertion in
metric_keys_are_unique_and_shaped.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 84192fad-d1f9-4ea9-884c-b4155779b35f
📥 Commits
Reviewing files that changed from the base of the PR and between 98bd0c5 and 25f86469ac7232b6a4f33381648d020b4b975d77.
⛔ Files ignored due to path filters (1)
src/backend/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (8)
docs/domain/metrics/specs/DESIGN.mddocs/domain/presentation-layer/specs/DESIGN.mddocs/domain/presentation-layer/specs/PRD.mdsrc/backend/services/analytics/Cargo.tomlsrc/backend/services/analytics/src/domain/metric_definitions/builtin.rssrc/backend/services/analytics/src/domain/metric_definitions/definition.rssrc/backend/services/analytics/src/domain/metric_definitions/registry.yamlsrc/backend/services/analytics/src/domain/metric_definitions/seeds.rs
5ae278a to
b3c1336
Compare
Collapse the code-literal builtin metric seed into one declarative registry.yaml (a `sources` list and a `metrics` list), the single source of truth for the sanctioned metric_definitions seed. The registry is embedded at build time and deserialized once into the seed types; the startup reconciler reads it through builtin_sources()/builtin_metrics(), so reconcile semantics are unchanged. Registry invariants are pinned by the builtin tests, which parse the same embedded registry, so a malformed or drifted registry fails the build. The FE already renders from the metric_definitions catalog API and live peer percentiles, holding no per-metric thresholds, so no FE change is needed. The orphaned, frozen legacy metric_catalog/metric_threshold subsystem has no live consumer and is left untouched; its retirement is tracked separately. Updates the governing metrics DESIGN (builtin.rs -> registry.yaml) and extends the presentation PRD/DESIGN with the registry FR + component. Closes constructorfabric#1974 Part of constructorfabric#1803 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Anton Zelenov <antonz@constructor.tech>
…own keys
- Move evidence_granularity into the registry: each measure is now
`{ key, evidence_granularity }` instead of a bare key resolved by a
hardcoded (source, measure) match. Same DB values, but the registry is
now the full source of truth for the seed and a measure can no longer be
silently mislabeled `source_summary`. Deletes the match.
- Add `#[serde(deny_unknown_fields)]` to every registry struct so a
mistyped key fails deserialization (caught by the registry tests) rather
than being silently dropped — making the "malformed registry fails the
build" guarantee real.
- Guard the empty-measures case in the disable-missing reconcile so a
`measures: []` source cannot emit `NOT IN ()`; pin it with a
`every_source_declares_at_least_one_measure` invariant test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Anton Zelenov <antonz@constructor.tech>
b3c1336 to
d4ed4b5
Compare
docker-compose.yml defined AUTH_MODE twice in the seed service's environment (a merge artifact), so `docker compose config` failed with "mapping key AUTH_MODE already defined" and the E2E/ui-journeys stand could not start. Drop the duplicate; the earlier definition (with its comment) stays. Latent on main (release commits skip the stand lane); it surfaces on any PR that runs the compose stand. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Anton Zelenov <antonz@constructor.tech>
test_the_team_view_lists_every_report_the_roster_declares required every roster member to have every listed metric recorded, but "not recorded" is a legitimate honest-NULL (a member who closed tasks but fixed no bugs). Against the deterministic seed one member has no bugs_fixed, so the assertion failed on main's own merge commit and every PR that runs the stand. Relax to the test's actual intent — no member silently dropped, the view shows real data: assert each member's row is visible, renders a cell for every column (recorded or an honest "not recorded"), and carries at least one recorded value; keep Page edits explicitly not-recorded. Adds metric_cell / any_recorded_metric_cell locators. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Anton Zelenov <antonz@constructor.tech>
297803d
Closes #1974. Part of #1803 (Phase B).
What
One declarative registry (
src/backend/services/analytics/src/domain/metric_definitions/registry.yaml) is now the single source of truth for the sanctioned metric-definition seed — asourceslist and ametricslist. It replaces the code-literalBUILTIN_SOURCES/BUILTIN_METRICSarrays inbuiltin.rs.The registry was generated from the const arrays, so it is byte-for-byte parity (5 sources, 59 metrics). It is embedded at build time (
include_str!) and deserialized once viaOnceLockinto the seed types, exposed asbuiltin_sources()/builtin_metrics(). The startup reconciler reads it through those accessors, so reconcile semantics (idempotent additive upserts + disable-missing) are unchanged. The registry invariant tests parse the same embedded file, so a malformed or drifted registry fails the build.Aligns with the adopted semantic-layer design
This PR is the first Phase-1 step of the semantic-layer target architecture adopted in #2184 (
docs/domain/semantic-layer/specs/— PRDcpt-semantic-layer-fr-definitions-as-data, DESIGNcpt-semantic-layer-component-definition-store): metric definitions authored as data, replacing Rust constants.Per that design, the registry's current observation-relation shape (
source_ref, per-measureevidence_granularity, reconcile intometric_source_measures) is intentionally transitional — it is rewritten to the dataset/measure/metric domain model at the compiler-first cutover (target Phases 2–3). So this PR deliberately does not reshape the schema; it moves authoring to data in the existing shape, which is the low-risk first step the design prescribes. Migration cost stays near zero because builtin rows are seed-reconciled.Scope
metric_definitionscatalog API and live peer percentiles and holds no per-metric thresholds, so the "FE thresholds" collapse was already done.metric_catalog/metric_threshold: untouched. It is an orphaned, frozen subsystem (no live consumer —/v1/catalog/get_metricsis not called by the FE), so it is not folded into the new registry. Its retirement is tracked separately ([pres] Retire the orphaned legacy metric_catalog/metric_threshold subsystem #2167).Docs
builtin.rs->registry.yaml) in "Builtin Seed Reconciliation" and "Adding a Metric".cpt-presentation-fr-metric-registry+cpt-presentation-component-metric-registry.Verification
cargo test -p analytics: 561 passed, 0 failed (registry invariant tests validate the YAML-loaded data, incl.every_source_declares_at_least_one_measure).cargo clippy -p analytics: clean.cfsper-artifact validation of the presentation PRD/DESIGN: green; zero new whole-registry errors.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Reliability
Documentation