feat: unified metrics system — typed registry, generic result runtime, AI metrics - #1656
Conversation
|
Warning Review limit reached
Next review available in: 57 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. 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 Run ID: 📒 Files selected for processing (9)
📝 WalkthroughWalkthroughAdds a typed metrics registry and metric-results runtime in the analytics backend, plus dbt and silver/gold ingestion changes that emit the metric observations, labels, and cohorts those metrics consume. ChangesMetrics Engine Backend
Ingestion Labels and Metric Observations
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 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 |
Metrics are defined once in a typed registry (sources, measures, dimensions, definitions, input-role mappings) and served by one generic runtime: request validation with caps, per-view ClickHouse query compilation for sum and ratio computations (period, timeseries, peer, breakdown views), densification, and row-cap enforcement. Definitions convert to Rust discriminated unions before compilation; only executable computations reach the compiler. Builtin definitions live in a code registry converged by a startup reconciler (disable-not-delete); migrations own schema only. A background validator probes observation sources per definition and degrades unavailable definitions without erroring quiet metrics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
dbt-owned gold views emit the source measure observation contract for the metrics runtime: insight.ai_metric_observations (measure streams from the AI dev and assistant classes) and insight.metric_entity_cohorts_current (person cohort membership for peer comparison), with schema tests and a cohort-uniqueness check. The AI class contracts gain connector-declared semantics: tool_label / surface_label display labels and conversation_count (populated only by sources that report conversations). Class rows carry an activity invariant — a row exists only for real activity, enforced per staging model and guarded by data-quality tests — so gold derives active days from row existence and contains no vendor-specific columns, tool names, or label mappings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
The metrics system contract moves to docs/domain/metrics/specs/DESIGN.md with an Adding a Metric guide covering the three authoring cases (existing measure, new measure, new source) and the rules that hold for every case. Root AGENTS.md routes metric work to the spec; the legacy ad-hoc gold-view + catalog-seed path is frozen for new metrics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
036d1bd to
20a48fc
Compare
Every measure branch in the gold observation models is a call to a shape macro — sum_measure for aggregated numerics (optional contract- dimension filter) and presence_measure for row-existence markers. The observation contract columns and the null-preserving aggregation idiom live in one place, and authoring a measure is a one-call branch. Macros map to computation shapes, not metrics; a new macro is added only when a new computation kind becomes executable. Filter predicates may reference only class-contract dimension values, per the authoring guide. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
Path references, the cargo package name in the authoring guide, the config env-var prefix, and dbt artifact descriptions now use the analytics service naming. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
Metric definitions carry two text tiers: description (short qualifier) and explanation (full meaning and scope, shown in info surfaces). Both ride the metric result response. Builtin AI metrics get explanations covering what counts and which tool families are in scope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
…fabric/insight into feat/unified-metrics
Seed vocabulary fields use the domain enums instead of strings: format, direction, and input roles reuse the definition-side types with paired as_db accessors; source kind, measure value type, entity type, and cohort key get authoring enums; the computation and its parameters collapse into one data-carrying SeedComputation so invalid combinations (ratio without scale) cannot be expressed. The DB-check-mirroring test is replaced by the type system; round-trip tests pin the as_db/from_db pairs. Regenerates the analytics OpenAPI document for the /v1/metric-results route and the explanation response field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
…ntime SchemaStatus and SourceKind enums replace string comparisons in definition loading and the schema validator; status writes carry typed values end to end. Definitions referencing stored custom-SQL sources now classify as unavailable (client error) rather than corrupt configuration, matching the custom metric gate. The dimension alias contract between the query compiler and the response builder moves to one shared constructor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
A custom-SQL input row after a corrupt row downgraded the definition from corrupt (config error) to unavailable, silencing the loud failure the precedence lattice promises. The downgrade now respects corrupt precedence; regression test added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
…atio - collapse MetricDefinition to base + ComputationSpec; drop non-executable computation kinds, input roles, and their registry storage columns - flatten the metric result DTO; the computation tag carries only executable fields, pinned by a wire-shape test - emit observation rows only when a value exists (HAVING in sum_measure) and drop gold defensive dimension fallbacks in favor of class-contract guarantees enforced by silver schema tests - align design spec, schema docs, and authoring guide with the closed vocabulary and document the extension path Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
# Conflicts: # AGENTS.md # src/backend/services/analytics/src/migration/mod.rs
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (5)
src/backend/services/analytics/src/domain/metric_definitions/mod.rs (1)
8-14: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueInconsistent re-export surface.
MetricInputRole,MetricComputation,SourceKind,MetricBase, andMetricInputare all public types indefinition.rsbut aren't re-exported here, whileCohortSource,ComputationSpec,MetricDefinition,MetricDirection,MetricFormat, andObservationSourceare. Consumers outside this module (e.g. the metric-results compiler/builder that need to match onMetricInputRole) will have to reach intodefinition::directly instead of the module root, which is workable but inconsistent.🤖 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/mod.rs` around lines 8 - 14, The module root re-export list is incomplete and leaves several public definition types only reachable through definition::, creating an inconsistent public API. Update the pub use block in metric_definitions::mod to also re-export MetricInputRole, MetricComputation, SourceKind, MetricBase, and MetricInput alongside the existing types, so consumers like the metric-results compiler/builder can import everything from the module root consistently.src/ingestion/scripts/create-bronze-placeholders.sh (1)
420-434: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffConsider extracting the placeholder-reconciliation guard into a helper.
The
count(placeholder) → if == "1" reconcile else skipidiom is now duplicated acrossclass_ai_dev_usage,class_ai_assistant_usage,class_people, andmtr_git_person_weekly. A small helper (e.g.reconcile_if_placeholder <db> <table> <<'SQL' … SQL) would DRY this up and keep the count-query/comment filter consistent as the list grows. Not required for this PR.🤖 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/ingestion/scripts/create-bronze-placeholders.sh` around lines 420 - 434, The placeholder-reconciliation logic is duplicated across multiple table blocks, using the same count/comment guard followed by conditional schema updates. Consider extracting this pattern into a helper in create-bronze-placeholders.sh, such as a reconcile_if_placeholder function that takes the database/table and runs the SQL only when the table matches the placeholder marker. Reuse that helper for class_ai_dev_usage, class_ai_assistant_usage, class_people, and mtr_git_person_weekly to keep the guard/query logic consistent and easier to extend.src/backend/services/analytics/src/api/metric_results.rs (1)
36-51: 🧹 Nitpick | 🔵 TrivialErrors don't short-circuit concurrent view execution.
stream::iter(tasks).map(...).buffer_unordered(QUERY_CONCURRENCY).collect::<Vec<_>>().awaitwaits for every task to finish (success or failure) before the firstErris surfaced viaresult?at line 49. With up to 50 metrics × 4 views, a request that fails validation-adjacent execution early still lets the remaining ClickHouse queries run to completion under the hood (throttled to 4 concurrent). Bounded impact givenQUERY_CONCURRENCY = 4and existing row caps, but worth consideringtry_for_each_concurrent-style early bailout if this becomes a hot failure path.🤖 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/api/metric_results.rs` around lines 36 - 51, Concurrent view execution currently buffers all task results before checking for errors in the metric_results flow, so failures in execute_task do not stop later ClickHouse work early. Update the task execution path in the metric results handler to short-circuit on the first error, using an early-bailing concurrent pattern instead of collecting into a Vec first, and keep the existing result placement logic in views_by_metric and the execute_task call site aligned with that change.src/backend/services/analytics/src/domain/metric_results/validation.rs (1)
325-350: 🧹 Nitpick | 🔵 TrivialBreakdown views have no upfront row-limit projection.
Unlike Period/Peer/Timeseries,
ValidatedMetricView::Breakdown { .. } => {}contributes nothing to the projected count, so oversized breakdown requests aren't rejected until after the ClickHouseGROUP BY entity_id, dim_*query (compiler.rs) runs to (near) completion andenforce_row_limitcatches it post-hoc. This is workable given today's low-cardinality AI dimensions (tool/surface), but as more dimensions are registered this becomes a way to force expensive aggregations before rejection. Worth a bound (e.g., cap on entity_ids × requested-dimension count for breakdown views, or an upfrontCOUNT(DISTINCT ...)probe) if breakdown dimensions grow beyond a handful of low-cardinality values.🤖 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_results/validation.rs` around lines 325 - 350, The projected row-limit check in validate_projected_row_limit currently ignores ValidatedMetricView::Breakdown { .. }, so oversized breakdown requests can slip through until after the ClickHouse query runs. Update validate_projected_row_limit to account for Breakdown views by adding a conservative upfront bound based on req.entity_ids and the requested breakdown dimensions, or by introducing a lightweight precheck before compiler.rs executes the GROUP BY path. Keep the fix localized to validate_projected_row_limit and the Breakdown match arm so row-limit enforcement happens before expensive aggregation work.src/backend/services/analytics/src/gear.rs (1)
139-159: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated single-tenant warehouse-id parsing logic.
The
trim+is_emptyfilter chain here duplicateswarehouse_tenant_id()inapi/metric_results.rs. Consider extracting a shared helper (e.g.MetricResultsConfig::effective_single_tenant_warehouse_id(&self) -> Option<&str>) so the startup guard and the runtime read path can never diverge on what counts as "set".♻️ Proposed shared helper
+impl MetricResultsConfig { + pub fn effective_single_tenant_warehouse_id(&self) -> Option<&str> { + self.single_tenant_warehouse_id + .as_deref() + .map(str::trim) + .filter(|id| !id.is_empty()) + } +}Then both
gear.rsandapi/metric_results.rscallcfg.metric_results.effective_single_tenant_warehouse_id().🤖 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/gear.rs` around lines 139 - 159, The single-tenant warehouse-id parsing logic is duplicated between the startup guard and the runtime metric-results read path, which can cause them to drift. Extract the trimming/non-empty check into a shared helper on MetricResultsConfig, such as effective_single_tenant_warehouse_id(), and have both gear.rs and api/metric_results.rs use it. Update the existing warehouse_tenant handling in gear.rs to call the helper so the “set” semantics stay identical everywhere.
🤖 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 `@src/backend/services/analytics/src/domain/metric_results/builder.rs`:
- Around line 110-126: build_peer_view currently returns only the rows from
ClickHouse, so it can drop requested entity_ids and break positional alignment
with the request. Update build_peer_view to take the request context like
build_period_view does, then densify against req.entity_ids by producing one
PeerValueDto per requested id, inserting a default entry with zero/nil values
when no row exists. Thread req through the execute_task call site in
api/metric_results.rs and use the existing build_peer_view and PeerValueDto
symbols to keep peer results aligned with the requested ids.
In `@src/backend/services/analytics/src/domain/metric_results/validation.rs`:
- Around line 207-221: The Peer metric view path in validate_metric_view_request
only normalizes an explicit cohort_key and never verifies it matches
def.base.peer_cohort_key. Update the MetricViewRequest::Peer branch to compare
any provided cohort_key against the metric’s declared peer cohort (similar to
the allowed_dimension validation elsewhere), and return a
MetricError::invalid_argument with a field violation when it does not match.
Keep the existing defaulting behavior when cohort_key is omitted, but reject
unsupported explicit values in
validate_metric_view_request/ValidatedMetricView::Peer.
In `@src/ingestion/silver/ai/class_ai_assistant_usage.sql`:
- Line 8: The incremental class_ai_assistant_usage model will still leave
tool_label and surface_label NULL for existing rows, so the new not_null tests
will fail. Update the dbt flow around class_ai_assistant_usage and its upstream
staging models to backfill these fields for historical data, either by
full-refreshing the affected models or by adding a backfill step before
enforcing the new constraints. Ensure the fix covers the incremental logic in
class_ai_assistant_usage and any staging models that populate these labels.
In `@src/ingestion/silver/ai/class_ai_dev_usage.sql`:
- Line 8: The `append_new_columns` change in `class_ai_dev_usage` leaves
existing `tool_label` values NULL for historical rows, so the `not_null`
contract still fails. Update the `class_ai_dev_usage` model and its upstream
`silver:class_ai_dev_usage` builders to backfill `tool_label` for existing
records before enforcing the constraint, and ensure the same fix is applied
wherever this column is introduced so the historical data is rebuilt with
non-NULL values.
In `@src/ingestion/silver/ai/schema.yml`:
- Around line 63-69: Add a backfill/full-refresh step before enabling the new
not_null test on tool_label in the schema for the silver AI models. Since the
incremental models use on_schema_change='append_new_columns', update the
relevant incremental model(s) to populate historical rows with tool_label (and
surface_label where applicable) or document/trigger a coordinated full refresh
so existing records are repaired before the tests run.
---
Nitpick comments:
In `@src/backend/services/analytics/src/api/metric_results.rs`:
- Around line 36-51: Concurrent view execution currently buffers all task
results before checking for errors in the metric_results flow, so failures in
execute_task do not stop later ClickHouse work early. Update the task execution
path in the metric results handler to short-circuit on the first error, using an
early-bailing concurrent pattern instead of collecting into a Vec first, and
keep the existing result placement logic in views_by_metric and the execute_task
call site aligned with that change.
In `@src/backend/services/analytics/src/domain/metric_definitions/mod.rs`:
- Around line 8-14: The module root re-export list is incomplete and leaves
several public definition types only reachable through definition::, creating an
inconsistent public API. Update the pub use block in metric_definitions::mod to
also re-export MetricInputRole, MetricComputation, SourceKind, MetricBase, and
MetricInput alongside the existing types, so consumers like the metric-results
compiler/builder can import everything from the module root consistently.
In `@src/backend/services/analytics/src/domain/metric_results/validation.rs`:
- Around line 325-350: The projected row-limit check in
validate_projected_row_limit currently ignores ValidatedMetricView::Breakdown {
.. }, so oversized breakdown requests can slip through until after the
ClickHouse query runs. Update validate_projected_row_limit to account for
Breakdown views by adding a conservative upfront bound based on req.entity_ids
and the requested breakdown dimensions, or by introducing a lightweight precheck
before compiler.rs executes the GROUP BY path. Keep the fix localized to
validate_projected_row_limit and the Breakdown match arm so row-limit
enforcement happens before expensive aggregation work.
In `@src/backend/services/analytics/src/gear.rs`:
- Around line 139-159: The single-tenant warehouse-id parsing logic is
duplicated between the startup guard and the runtime metric-results read path,
which can cause them to drift. Extract the trimming/non-empty check into a
shared helper on MetricResultsConfig, such as
effective_single_tenant_warehouse_id(), and have both gear.rs and
api/metric_results.rs use it. Update the existing warehouse_tenant handling in
gear.rs to call the helper so the “set” semantics stay identical everywhere.
In `@src/ingestion/scripts/create-bronze-placeholders.sh`:
- Around line 420-434: The placeholder-reconciliation logic is duplicated across
multiple table blocks, using the same count/comment guard followed by
conditional schema updates. Consider extracting this pattern into a helper in
create-bronze-placeholders.sh, such as a reconcile_if_placeholder function that
takes the database/table and runs the SQL only when the table matches the
placeholder marker. Reuse that helper for class_ai_dev_usage,
class_ai_assistant_usage, class_people, and mtr_git_person_weekly to keep the
guard/query logic consistent and easier to extend.
🪄 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
Run ID: 5f1cd9c7-0c73-4474-9106-9386cc0f134f
📒 Files selected for processing (46)
AGENTS.mddocs/components/backend/analytics/openapi.jsondocs/domain/README.mddocs/domain/metrics/README.mddocs/domain/metrics/specs/DESIGN.mdsrc/backend/services/analytics/src/api/metric_results.rssrc/backend/services/analytics/src/api/mod.rssrc/backend/services/analytics/src/config.rssrc/backend/services/analytics/src/domain/metric_definitions/README.mdsrc/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/error_code.rssrc/backend/services/analytics/src/domain/metric_definitions/mod.rssrc/backend/services/analytics/src/domain/metric_definitions/repository.rssrc/backend/services/analytics/src/domain/metric_definitions/seeds.rssrc/backend/services/analytics/src/domain/metric_definitions/validator.rssrc/backend/services/analytics/src/domain/metric_results/builder.rssrc/backend/services/analytics/src/domain/metric_results/compiler.rssrc/backend/services/analytics/src/domain/metric_results/dto.rssrc/backend/services/analytics/src/domain/metric_results/mod.rssrc/backend/services/analytics/src/domain/metric_results/validation.rssrc/backend/services/analytics/src/domain/metric_results/view.rssrc/backend/services/analytics/src/domain/mod.rssrc/backend/services/analytics/src/gear.rssrc/backend/services/analytics/src/migration/m20260625_000001_metric_definitions.rssrc/backend/services/analytics/src/migration/mod.rssrc/ingestion/connectors/ai/chatgpt-team/dbt/chatgpt_team__ai_assistant_usage.sqlsrc/ingestion/connectors/ai/chatgpt-team/dbt/chatgpt_team__ai_dev_usage.sqlsrc/ingestion/connectors/ai/claude-admin/dbt/claude_admin__ai_dev_usage.sqlsrc/ingestion/connectors/ai/claude-enterprise/dbt/claude_enterprise__ai_assistant_usage.sqlsrc/ingestion/connectors/ai/claude-enterprise/dbt/claude_enterprise__ai_dev_usage.sqlsrc/ingestion/connectors/ai/claude-team/dbt/claude_team__ai_dev_usage.sqlsrc/ingestion/connectors/ai/cursor/dbt/cursor__ai_dev_usage.sqlsrc/ingestion/connectors/ai/github-copilot/dbt/copilot__ai_dev_usage.sqlsrc/ingestion/dbt/dbt_project.ymlsrc/ingestion/dbt/macros/metric_observation_measures.sqlsrc/ingestion/dbt/tests/ai/assert_ai_assistant_usage_rows_active.sqlsrc/ingestion/dbt/tests/ai/assert_ai_dev_usage_rows_active.sqlsrc/ingestion/dbt/tests/gold/assert_metric_entity_cohorts_unique.sqlsrc/ingestion/gold/ai_metric_observations.sqlsrc/ingestion/gold/metric_entity_cohorts_current.sqlsrc/ingestion/gold/schema.ymlsrc/ingestion/scripts/create-bronze-placeholders.shsrc/ingestion/silver/ai/class_ai_assistant_usage.sqlsrc/ingestion/silver/ai/class_ai_dev_usage.sqlsrc/ingestion/silver/ai/schema.yml
Warehouse tenant isolation is not implemented platform-wide (the legacy query engine skips it), and the control-plane tenant id has no defined mapping to the warehouse tenant_id strings stamped at ingestion — a predicate on an unmapped identifier reads as isolation without providing it. Remove the tenant_id predicate from compiled queries and the single_tenant_warehouse_id config bridge with its boot guard. The observation and cohort contracts keep the tenant_id column, so enabling isolation later is a one-place compiler change once the platform defines the identifier mapping. Posture documented in the design spec. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
Quartiles over a handful of people are noise presented as signal, and a two-person pool discloses the colleague's value through the median. The peer view now returns null percentiles and min/max when fewer than 5 members contribute, while n keeps reporting the true pool size. Enforced in the compiled query so every consumer inherits the floor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
The peer query zero-filled every cohort member without observations, asserting a value the data never determined: absence of rows cannot be distinguished from lack of source coverage (no seat, no account), so the fabricated zeros ranked unmeasured people and pinned percentiles to zero under partial adoption. Cohort membership now scopes who counts as a peer; only members with observed values contribute to percentiles, for every computation alike. Targets without observations report a null target_value. Sources where covered-but-inactive genuinely means zero can emit explicit zero observations instead — coverage knowledge lives in the connector, not the runtime. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
…l fast on view errors - reject explicit peer cohort_key values not declared by the metric instead of silently compiling a query that matches nothing - consume view query results as they complete so the first failure cancels in-flight and queued ClickHouse work - backfill connector-declared label columns on the AI class tables: rows ingested before the columns existed read them as empty strings, which Gold now consumes verbatim; frozen label constants repair history idempotently, and a data-quality test locks the non-empty contract Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/backend/services/analytics/src/domain/metric_results/compiler.rs (1)
308-313: 🚀 Performance & Scalability | 🔵 TrivialCohort join fan-out may be costly for large peer cohorts.
LEFT JOIN peers ON peers.cohort_id = targets.cohort_idmaterializes atargets × peerscross-product per cohort before theGROUP BY. For a target set within a large org-unit cohort (thousands of observed members), this expands to a large intermediate before aggregation. Correctness is fine; this is a scaling note. Consider pre-aggregating cohort percentiles once percohort_idand joining the target rows to the aggregated result, so peer stats are computed once per cohort rather than once per (target, peer) pair.🤖 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_results/compiler.rs` around lines 308 - 313, The cohort percentile query in compiler.rs is doing a costly target-to-peer fan-out by joining peers directly to targets before aggregation. Update the SQL-building logic around the query that uses targets, peers, and GROUP BY targets.entity_id, target_values.value so peer statistics are computed once per cohort_id first, then join that pre-aggregated cohort result back to the target rows. Use the existing metric query compiler path in compiler.rs to keep correctness while reducing the intermediate cross-product.
🤖 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.
Nitpick comments:
In `@src/backend/services/analytics/src/domain/metric_results/compiler.rs`:
- Around line 308-313: The cohort percentile query in compiler.rs is doing a
costly target-to-peer fan-out by joining peers directly to targets before
aggregation. Update the SQL-building logic around the query that uses targets,
peers, and GROUP BY targets.entity_id, target_values.value so peer statistics
are computed once per cohort_id first, then join that pre-aggregated cohort
result back to the target rows. Use the existing metric query compiler path in
compiler.rs to keep correctness while reducing the intermediate cross-product.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 506df536-9cb5-4d93-bacc-5e83ab7b07c0
📒 Files selected for processing (6)
docs/domain/metrics/specs/DESIGN.mdsrc/backend/services/analytics/src/api/metric_results.rssrc/backend/services/analytics/src/domain/metric_results/compiler.rssrc/backend/services/analytics/src/domain/metric_results/validation.rssrc/ingestion/dbt/tests/ai/assert_ai_class_labels_nonempty.sqlsrc/ingestion/scripts/migrations/20260707000000_ai_class_label_backfill.sql
✅ Files skipped from review due to trivial changes (1)
- docs/domain/metrics/specs/DESIGN.md
🚧 Files skipped from review as they are similar to previous changes (2)
- src/backend/services/analytics/src/api/metric_results.rs
- src/backend/services/analytics/src/domain/metric_results/validation.rs
# Conflicts: # src/backend/services/analytics/src/migration/mod.rs
…+ CI lanes
Reworks the api-endpoint coverage gate and extends the api/ contract suite,
test-only (zero src/backend diff — the committed OpenAPI spec stays the
.standard_errors boilerplate and its inaccuracies are filed as bugs, not fixed
here):
- Operation-level gate (lib/api_coverage.py): the gate blocks only when a
documented operation is exercised by NO test (a new endpoint) or a SKIP_LIST
entry rots. Per-status-code coverage is REPORTED as a percentage — an
endpoints x registered-codes matrix (observed / declared-but-unobserved /
excluded) — not enforced. coverable(op) = declared - {>=500} -
UNIVERSAL_BOILERPLATE{401,429} - BLOCKED[op]; BLOCKED absorbs the boilerplate
over-declaration, and a now-observed excluded code is a non-blocking advisory.
- POST /v1/metric-results (api/test_metric_results.py): the unified-metric
compute endpoint added by the feat/unified-metrics merge (constructorfabric#1656) is covered on
its deterministic error paths — 400 (empty / bad-period / unknown-key, which
is a 400 via `unavailable`, not 404) and 415. Its 200 needs seeded observation
data and reports as a coverage gap. All 21 spec operations exercised.
- CI (e2e-bronze-to-api.yml): the suite runs as two lanes, renamed to `api` and
`metrics`, each feeding its own coverage gate, aggregated by an umbrella
`Run E2E suite` job that supplies the `main` branch-protection required check.
Contract suite (api/): per-(path,method,status) cases incl. 415 wrong
content-type and 400 path-parse; constructorfabric#1663/constructorfabric#1664 pinned as strict xfails with their
codes BLOCKED; constructorfabric#1670 (off-schema body should be canonical 400) pinned as strict
xfails.
Docs: README updated to the operation-level model; the bronze-to-api-e2e
PRD/DESIGN/DECOMPOSITION specs have the (never-shipped) temporal skip-list
feature removed. Bugs filed: constructorfabric/insight constructorfabric#1663 constructorfabric#1664 constructorfabric#1669 constructorfabric#1670.
Verified: endpoint gate PASS 21/21 (97.9% registered-code coverage) on the
merged ledger; cfs validate-toc clean.
Signed-off-by: Konstantin Tursunov <Konstantin.Tursunov@constructor.tech>
What
Introduces the unified metrics system: metrics are authored, requested, and rendered through one structured path instead of ad-hoc per-metric gold views and catalog seeds. The AI metrics are the first family on it.
Architecture
insight.ai_metric_observations): tenant, entity, date,measure_key, value, dimension tuples. Observations belong to measures, not metrics.metric_sources→metric_source_measures/metric_source_dimensions→metric_definitions+ input-role mappings. Product definitions are tenant-overridable; disabled/schema-error rows degrade to 400, never 500.builtin.rs) converged at startup (disable-not-delete); migrations own schema only. Warm environments converge to the registry state on every deploy.POST /v1/metric-resultsvalidates (request caps, per-entity-type id normalization, dimension grants), compiles one ClickHouse query per metric view (sum/ratio× period/timeseries/peer/breakdown), densifies, and enforces all-or-nothing row caps. No metric-key-specific branches. The computation vocabulary is closed and fully executable — nothing is stored before it executes; extending it is one coordinated change (spec variant, compiler arm, DB enum, response tag) documented in the design spec.ok/error/uncheckedper emitted measures and dimension coverage; transient probe failures never overwrite established status; quiet (filtered) measures downgrade tounchecked, nevererror.Vendor-neutral gold
Gold models read only class-contract fields. The AI class contracts gain connector-declared semantics:
tool_label/surface_label(each connector owns its display name),conversation_count(populated only by sources that actually report conversations), and an activity invariant — a class row exists only for real activity, enforced by each staging model's emission filter and guarded by data-quality tests. Gold derives active days from row existence and contains no vendor columns, tool names, or label maps.AI metrics on the system
ai.accepted_lines,ai.removed_lines,ai.active_days,ai.cost,ai.accepted_edit_actions,ai.tool_acceptance_rate,ai.assistant_messages,ai.assistant_actions,ai.dev_conversations,ai.chat_assistant_conversations— withtool/surfacedimensions and org-unit peer comparison.ai.active_daysandai.costspan dev and assistant tools.Authoring standard
docs/domain/metrics/specs/DESIGN.mdis the system contract and includes an Adding a Metric guide (three cases: existing measure / new measure / new source) plus the rules that hold for every case. RootAGENTS.mdroutes all metric work there. The legacy path (ad-hocinsight.*gold views +metric_catalogseed migrations) is frozen for new metrics.Validation
cargo test -p analytics: 397 passed — 60+ new tests for this system (registry invariants, compiler SQL shapes and parameter order, bucket enumeration vs ClickHouse week/month semantics, densification/zero-fill, definition precedence and fallback, request caps, DDL CHECK↔enum sync); the pre-existing suite stays green.dbt parseclean; gold models resolve toinsight.*with lineage to the AI silver classes; contract-column parity verified across all staging models in both class tags.Follow-ups
/v1/metric-results(metric collections), then removal of the legacy AI gold views and catalog seeds.🤖 Generated with Claude Code
Summary by CodeRabbit
POST /v1/metric-resultsanalytics endpoint to compute metric results across period, timeseries, peer, and breakdown views.tool_label,surface_label) andconversation_countend-to-end.