Skip to content

feat(analytics): group metric definitions by subject, add tags - #2397

Merged
cyberantonz merged 2 commits into
constructorfabric:mainfrom
cyberantonz:fix/2344-metric-subject-tags
Aug 11, 2026
Merged

feat(analytics): group metric definitions by subject, add tags#2397
cyberantonz merged 2 commits into
constructorfabric:mainfrom
cyberantonz:fix/2344-metric-subject-tags

Conversation

@cyberantonz

@cyberantonz cyberantonz commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #2344.

Metric definitions carried no grouping attribute, so a surface listing a whole family could only sort its metrics by name. This adds two authored attributes, both exposed on GET /v1/metric-definitions:

  • subject — the single topic a metric belongs to within its family (meetings, messaging, email, documents, commits, pull_requests, …). This is the partition the issue asked for: exactly one per metric, required for every builtin and pinned by a registry invariant test. It deliberately is not derived from the source — a metric's numerator and denominator inputs may come from different sources, so a source-based grouping is not a partition — and being required means no new metric can silently fall into an "other" bucket.
  • tags — a many-to-many set of cross-cutting filter/search labels (rate, duration, distribution), distinct from the singular grouping key.

Changes

  • Schema: subject column on metric_definitions; new metric_definition_tags table (shape + display-order CHECKs, wired into the startup probe). Two forward migrations.
  • Registry + reconciler: subject on all builtin metrics (18 topics); tags where they cross subjects. Reconciler seeds subject and a replace_tags step. subject is a required MetricSeed field, so a missing one fails the build; new invariant tests pin subject/tag shape and per-metric tag uniqueness.
  • Listing: subject and tags on the wire view; repository::fetch_tags.
  • Contract & docs: regenerated openapi.json and the stand analytics.py schema via their canonical generators (drift-checked); DESIGN storage model + authoring guidance; e2e collector projects both fields.

Custom-metric definitions store NULL/no tags (the column is nullable); wiring subject into the custom-metric authoring flow is intentionally out of scope.

Testing

  • cargo test -p analytics → 414 passed (incl. the two new registry invariants), 0 failed.
  • cargo clippy -p analytics --tests clean.
  • OpenAPI drift-check passes; stand schema regenerated.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Metric definitions now include an optional subject and ordered tags.
    • Built-in metrics are categorized with standardized subjects and tags such as rate, duration, and distribution.
    • Metric listing and collection outputs now expose subject and tag metadata.
  • Bug Fixes
    • Added validation to ensure subjects and tags use the required format and remain unique per metric.
    • Metric metadata is now reliably updated when built-in definitions change.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cyberantonz, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 522b118b-c805-4c4d-a0b6-0430170570f2

📥 Commits

Reviewing files that changed from the base of the PR and between 79d948d and 8e02870.

📒 Files selected for processing (12)
  • docs/components/backend/analytics/openapi.json
  • docs/domain/metrics/specs/DESIGN.md
  • src/backend/services/analytics/src/domain/metric_definitions/builtin.rs
  • src/backend/services/analytics/src/domain/metric_definitions/listing.rs
  • src/backend/services/analytics/src/domain/metric_definitions/registry.yaml
  • src/backend/services/analytics/src/domain/metric_definitions/repository.rs
  • src/backend/services/analytics/src/domain/metric_definitions/seeds.rs
  • src/backend/services/analytics/src/migration/m20260810_000001_metric_definition_subject.rs
  • src/backend/services/analytics/src/migration/m20260810_000002_metric_definition_tags.rs
  • src/backend/services/analytics/src/migration/mod.rs
  • src/ingestion/tests/e2e/lib/collect_metric_definitions.py
  • tests/stand/api/schemas/analytics.py
📝 Walkthrough

Walkthrough

Metric definitions now include nullable subjects and ordered tags. Migrations create the storage schema, registry seeds validate and persist metadata, listings return it, and OpenAPI and test schemas expose the new fields.

Changes

Metric metadata

Layer / File(s) Summary
Storage schema and metadata rules
docs/domain/metrics/specs/DESIGN.md, src/backend/services/analytics/src/migration/*
Added nullable metric subjects and ordered metric-definition tags with uniqueness, format, ordering, foreign-key, and migration registration changes.
Registry definitions and persistence
src/backend/services/analytics/src/domain/metric_definitions/builtin.rs, src/backend/services/analytics/src/domain/metric_definitions/registry.yaml, src/backend/services/analytics/src/domain/metric_definitions/seeds.rs, docs/domain/metrics/specs/DESIGN.md
Builtin seeds now require subjects and accept tags. Registry entries define subjects and tags. Reconciliation upserts subjects and replaces stored tags.
Listing query and tag loading
src/backend/services/analytics/src/domain/metric_definitions/repository.rs, src/backend/services/analytics/src/domain/metric_definitions/listing.rs, src/ingestion/tests/e2e/lib/collect_metric_definitions.py
Listing queries and maps subjects and ordered tags. Ingestion test output serializes both fields.
Public response schemas
docs/components/backend/analytics/openapi.json, tests/stand/api/schemas/analytics.py
MetricDefinitionView exposes nullable subject and required tags fields.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MetricSeed
  participant MetricDatabase
  participant MetricRepository
  participant MetricDefinitionView
  MetricSeed->>MetricDatabase: upsert subject and replace tags
  MetricRepository->>MetricDatabase: fetch subject and ordered tags
  MetricRepository->>MetricDefinitionView: map metadata into listing response
Loading

Possibly related PRs

Suggested reviewers: ktursunov, dzarlax

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary changes: subject-based grouping and tags for analytics metric definitions.
Linked Issues check ✅ Passed The changes add explicit subjects to builtin metric definitions and expose them through the API, satisfying issue #2344 grouping requirements.
Out of Scope Changes check ✅ Passed The migrations, registry updates, API changes, tests, documentation, and collector support directly implement the stated subject and tag objectives.
Docstring Coverage ✅ Passed Docstring coverage is 86.67% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Metric definitions carried no grouping attribute, so a surface listing a
whole family could only sort its metrics by name. Add a singular `subject`
(the one topic a metric belongs to within its family) and a many-to-many
`tags` set (cross-cutting filter labels), both authored in the builtin
registry and exposed on GET /v1/metric-definitions.

`subject` is a partition: exactly one per metric, required for every builtin
and pinned by a registry invariant test, so it cannot be derived from a
source (a metric's inputs may span sources) and no new metric silently falls
into an "other" bucket. `tags` sit alongside for filtering and search.

- schema: `subject` column on metric_definitions; new metric_definition_tags
  table with shape/order CHECKs wired into the startup probe.
- registry: subject on all builtins; tags where they cross subjects
  (rate/duration/distribution). Reconciler seeds both.
- listing: subject and tags on the wire view; repository fetch_tags.
- contract/docs: regenerated openapi.json and stand schema; DESIGN storage
  model and authoring guidance; e2e collector projects both fields.

Closes constructorfabric#2344

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Anton Zelenov <antonz@constructor.tech>
@cyberantonz
cyberantonz force-pushed the fix/2344-metric-subject-tags branch from 7a5c857 to 2905a67 Compare August 10, 2026 12:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/backend/services/analytics/src/migration/m20260810_000001_metric_definition_subject.rs (1)

15-16: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Derive Debug for both migration types.

  • src/backend/services/analytics/src/migration/m20260810_000001_metric_definition_subject.rs#L15-L16: Add Debug to the Migration derives.
  • src/backend/services/analytics/src/migration/m20260810_000002_metric_definition_tags.rs#L26-L27: Add Debug to the Migration derives.

As per coding guidelines, “Derive Debug for types.”

🤖 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/migration/m20260810_000001_metric_definition_subject.rs`
around lines 15 - 16, Add Debug to the derive attributes for the Migration type
in both
src/backend/services/analytics/src/migration/m20260810_000001_metric_definition_subject.rs
lines 15-16 and
src/backend/services/analytics/src/migration/m20260810_000002_metric_definition_tags.rs
lines 26-27, preserving the existing DeriveMigrationName derive.

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 `@src/backend/services/analytics/src/domain/metric_definitions/builtin.rs`:
- Around line 102-109: The builtin metric metadata must use parsed bounded slugs
instead of raw strings. In builtin.rs lines 102-109, introduce and use
MetricSubject and MetricTag boundary types enforcing lowercase snake case and a
maximum of 64 characters; update lines 325-354 to test the length limit while
preserving per-metric tag uniqueness validation. In
docs/domain/metrics/specs/DESIGN.md lines 663-669, document the 64-character
maximum in the metric-authoring guidance.

In `@src/backend/services/analytics/src/domain/metric_definitions/seeds.rs`:
- Around line 219-245: The replace_tags function must perform deletion and all
tag inserts within a single database transaction so failures cannot leave
partial results and concurrent reconcilers cannot interleave operations. Start a
transaction from db, execute the delete and insert loop through the transaction
handle, commit only after every operation succeeds, and roll back or propagate
errors on failure while preserving idempotent, race-safe replacement behavior.

---

Nitpick comments:
In
`@src/backend/services/analytics/src/migration/m20260810_000001_metric_definition_subject.rs`:
- Around line 15-16: Add Debug to the derive attributes for the Migration type
in both
src/backend/services/analytics/src/migration/m20260810_000001_metric_definition_subject.rs
lines 15-16 and
src/backend/services/analytics/src/migration/m20260810_000002_metric_definition_tags.rs
lines 26-27, preserving the existing DeriveMigrationName derive.
🪄 Autofix

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: 3fc1e8f8-2be2-48bd-8f5d-17ccafcd47d8

📥 Commits

Reviewing files that changed from the base of the PR and between 79d948d and 2905a67.

📒 Files selected for processing (12)
  • docs/components/backend/analytics/openapi.json
  • docs/domain/metrics/specs/DESIGN.md
  • src/backend/services/analytics/src/domain/metric_definitions/builtin.rs
  • src/backend/services/analytics/src/domain/metric_definitions/listing.rs
  • src/backend/services/analytics/src/domain/metric_definitions/registry.yaml
  • src/backend/services/analytics/src/domain/metric_definitions/repository.rs
  • src/backend/services/analytics/src/domain/metric_definitions/seeds.rs
  • src/backend/services/analytics/src/migration/m20260810_000001_metric_definition_subject.rs
  • src/backend/services/analytics/src/migration/m20260810_000002_metric_definition_tags.rs
  • src/backend/services/analytics/src/migration/mod.rs
  • src/ingestion/tests/e2e/lib/collect_metric_definitions.py
  • tests/stand/api/schemas/analytics.py

Comment on lines +102 to +109
/// The single topic this metric belongs to within its family, so a surface
/// listing a family can partition it into topics. Required for builtins —
/// exactly one per metric, which is the partition a source key cannot give.
pub subject: String,
/// Cross-cutting labels a surface can filter or search by; many per metric,
/// unlike the singular `subject`.
#[serde(default)]
pub tags: Vec<String>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Parse bounded metadata slugs at the registry boundary.

subject and tags remain raw strings. The current tests accept values longer than the VARCHAR(64) columns. A future registry entry can pass cargo test and then fail or truncate during reconciliation.

  • src/backend/services/analytics/src/domain/metric_definitions/builtin.rs#L102-L109: Replace raw metadata strings with parsed MetricSubject and MetricTag boundary types that enforce lowercase snake case and a 64-character maximum.
  • src/backend/services/analytics/src/domain/metric_definitions/builtin.rs#L325-L354: Test the maximum length and retain per-metric tag uniqueness validation.
  • docs/domain/metrics/specs/DESIGN.md#L663-L669: State the 64-character maximum in the metric-authoring instructions.

As per coding guidelines, “Parse, don't validate: introduce boundary newtypes … and do not carry raw String values through layers.”

📍 Affects 2 files
  • src/backend/services/analytics/src/domain/metric_definitions/builtin.rs#L102-L109 (this comment)
  • src/backend/services/analytics/src/domain/metric_definitions/builtin.rs#L325-L354
  • docs/domain/metrics/specs/DESIGN.md#L663-L669
🤖 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 102 - 109, The builtin metric metadata must use parsed bounded
slugs instead of raw strings. In builtin.rs lines 102-109, introduce and use
MetricSubject and MetricTag boundary types enforcing lowercase snake case and a
maximum of 64 characters; update lines 325-354 to test the length limit while
preserving per-metric tag uniqueness validation. In
docs/domain/metrics/specs/DESIGN.md lines 663-669, document the 64-character
maximum in the metric-authoring guidance.

Source: Coding guidelines

Comment on lines +219 to +245
async fn replace_tags(
db: &DatabaseConnection,
metric_id: Uuid,
tags: &[String],
) -> Result<(), DbErr> {
db.execute(Statement::from_sql_and_values(
db.get_database_backend(),
"DELETE FROM metric_definition_tags WHERE metric_definition_id = ?",
[uuid_value(metric_id)],
))
.await?;

for (idx, tag) in tags.iter().enumerate() {
db.execute(Statement::from_sql_and_values(
db.get_database_backend(),
"INSERT INTO metric_definition_tags \
(id, metric_definition_id, tag, display_order) \
VALUES (?, ?, ?, ?)",
[
uuid_value(Uuid::now_v7()),
uuid_value(metric_id),
Value::from(tag.as_str()),
Value::from(order_value(idx)),
],
))
.await?;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make tag replacement atomic.

The DELETE on Line 224 commits before the insert loop completes. If an insert fails, the metric keeps no tags or only a prefix of its tags. Concurrent reconcilers can also interleave deletes and inserts.

Reconcile tags in one transaction. Upsert desired tags before deleting obsolete tags, or otherwise serialize this replacement.

As per docs/domain/metrics/specs/DESIGN.md, builtin upserts must be idempotent and race-safe across replicas.

🤖 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 219 - 245, The replace_tags function must perform deletion and all tag
inserts within a single database transaction so failures cannot leave partial
results and concurrent reconcilers cannot interleave operations. Start a
transaction from db, execute the delete and insert loop through the transaction
handle, commit only after every operation succeeds, and roll back or propagate
errors on failure while preserving idempotent, race-safe replacement behavior.

…ct/tag length

Addresses review feedback on the subject/tags reconcile:

- Wrap each metric's definition upsert and its input/dimension/tag replacement
  in one transaction. A mid-way failure no longer leaves a metric with a
  partial child set, and a concurrent reconciler on another replica observes
  the whole prior set or the whole new one rather than a delete-in-progress —
  the idempotent, race-safe convergence DESIGN requires. The per-metric write
  helpers now take any connection so they run inside the transaction.
- Bound subject and tag length to the VARCHAR(64) column width in the builtin
  registry invariant tests, so an over-long value fails the build instead of
  erroring or truncating at reconcile time.

Refs constructorfabric#2344

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Anton Zelenov <antonz@constructor.tech>
@cyberantonz
cyberantonz added this pull request to the merge queue Aug 11, 2026
Merged via the queue into constructorfabric:main with commit bfce60c Aug 11, 2026
60 checks passed
@cyberantonz
cyberantonz deleted the fix/2344-metric-subject-tags branch August 11, 2026 04:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Metric definitions carry no subject, so a family's metrics cannot be grouped

2 participants