Skip to content

feat(metrics): task delivery on unified metrics - #1794

Merged
aleksdotbar merged 14 commits into
mainfrom
feat/unified-metrics-tasks
Jul 17, 2026
Merged

feat(metrics): task delivery on unified metrics#1794
aleksdotbar merged 14 commits into
mainfrom
feat/unified-metrics-tasks

Conversation

@aleksdotbar

@aleksdotbar aleksdotbar commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Ports the task-delivery metric family to /v1/metric-results, the last
IC family still on the legacy bullet path.

  • Gold task_metric_observations view reproduces per-issue state,
    status intervals, close/reopen (14d), worklog and estimation facts
    from silver class_task_* — 14 tasks.* metrics registered in
    builtin.rs.
  • Post-aggregation value transform (affine + NULL-guarded clamp) so
    ratios and folds render honest units without new computation kinds.
  • Silver class_task_users carries tenant_id for the peer join.

Entity gate is honest-absence: only email-shaped assignees rank; Jira
privacy hides the rest, matching git/collab.

Closes #1696

Summary by CodeRabbit

  • New Features
    • Added task-delivery metrics and a new task observation dataset for unified task metric reporting.
    • Introduced optional post-aggregation metric value transforms (affine with optional clamping) for ratio-style task metrics.
  • Bug Fixes
    • Updated missing-value handling so zero-filled results respect configured metric transforms.
    • Ensured transforms are applied consistently across timeseries, breakdown, period, histogram, and peer views.
  • Tests
    • Added dbt build-integrity tests for task observation shape, non-negativity, and uniqueness/grain.
  • Chores
    • Added task user tenant scoping and updated task-tracking model dependency wiring to the correct upstream sources.

Bounded index metrics (efficiency clamps, accuracy folds) had no
home for their final shaping short of a bespoke computation variant.
Add an optional affine + clamp transform on every definition —
clamp(m*x + b) — applied after aggregation in every view and to
zero-fills, keeping the computation vocabulary a closed algebra.

The clamp is NULL-guarded: ClickHouse >=24.12 least/greatest ignore
NULL arguments, which would resurrect an honest-null value as the
bound and feed it to peer pools. Applied as an outer projection over
column aliases, never inline, so it duplicates no aggregate
placeholders.

Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
Reproduce the task-delivery family as 14 tasks.* metrics over a new
task_metric_observations gold view that reads class_task_* directly:
per-issue state pivot, status-interval spans, and close/reopen
transition pairing, all keyed on the source-neutral status_category.

Metric-level decisions vs the legacy bullet path:
- reopen_rate follows the spec — reopened within 14 days over close
  events, no minimum-close gate
- resolution_time is a median (the legacy "mean" label was wrong)
- flow_efficiency and worklog_accuracy keep their <=100 clamp, and
  estimation_accuracy its symmetric fold, via the value transform
- closed/bugs/due/estimation count currently-done issues; duration
  metrics span every ever-closed issue

Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
The task class tables alone dropped bronze's tenant_id. The unified
gold view resolves an observation's tenant through the assignee
identity join, so the directory that anchors that join must carry
it; project it on both source staging models for a consistent union.

Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
@aleksdotbar
aleksdotbar requested a review from a team as a code owner July 16, 2026 09:53
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 44 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

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

Run ID: 0902054f-b34b-46a9-ba71-873c6da0f998

📥 Commits

Reviewing files that changed from the base of the PR and between 0dc8391 and c51e2a9.

📒 Files selected for processing (2)
  • src/ingestion/scripts/apply-ch-migrations.sh
  • src/ingestion/scripts/create-bronze-placeholders.sh
📝 Walkthrough

Walkthrough

Changes

Task metrics and unified value transforms

Layer / File(s) Summary
Task observation model and contract
src/ingestion/gold/..., src/ingestion/silver/task-tracking/*, src/ingestion/dbt/tests/gold/*, src/ingestion/connectors/task-tracking/*
Adds task issue state, status intervals, unified observations, tenant-aware user fields, connector routing, schema constraints, and dbt integrity checks.
Value transform contract and persistence
src/backend/services/analytics/src/domain/metric_definitions/{definition,repository,seeds}.rs, src/backend/services/analytics/src/migration/*
Adds affine/clamping transforms and persists their components in metric definitions.
Task source and metric catalog
src/backend/services/analytics/src/domain/metric_definitions/builtin.rs
Registers the task source and adds tasks.* metric definitions with transform settings.
Transformed query and result paths
src/backend/services/analytics/src/domain/metric_results/{compiler,builder,batch,validation}.rs
Applies transforms across query shapes and transform-aware zero-filled results, with updated fixtures and tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

  • constructorfabric/insight#1561 — Relates to the unified metric system and normalized task observations.
  • constructorfabric/insight#1439 — Relates to the task-delivery metric definitions and ingestion pipeline.

Possibly related PRs

Suggested reviewers: mitasovr, cyberantonz

Sequence Diagram(s)

sequenceDiagram
  participant TaskTracking
  participant TaskModels
  participant task_metric_observations
  participant MetricDefinitions
  participant QueryCompiler
  participant ResultBuilder
  TaskTracking->>TaskModels: provide task history, users, statuses, and worklogs
  TaskModels->>task_metric_observations: materialize task measures
  MetricDefinitions->>MetricDefinitions: persist and load ValueTransform
  MetricDefinitions->>QueryCompiler: provide metric definition
  QueryCompiler->>QueryCompiler: aggregate and transform metric values
  QueryCompiler->>ResultBuilder: return transformed query results
  ResultBuilder->>ResultBuilder: transform zero-filled values
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main change: migrating task-delivery metrics to unified metrics.
Linked Issues check ✅ Passed The PR adds task observation models, registers task source/metrics, maps peer joins, and includes unit/dbt tests as requested.
Out of Scope Changes check ✅ Passed The remaining changes support the task-delivery migration and do not introduce clear unrelated scope.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/unified-metrics-tasks

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.

@aleksdotbar aleksdotbar changed the title feat(metrics): task-delivery family on unified results feat(metrics): task-delivery family on unified metrics Jul 16, 2026

@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 (2)
src/backend/services/analytics/src/domain/metric_results/compiler.rs (2)

1133-1168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Include the histogram in the “every query shape” regression test.

The test currently covers timeseries, breakdown, period, and peer queries but omits the changed histogram path at Lines 220-246. Assert that histogram windows use a separately transformed event column.

🤖 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 1133 - 1168, Extend the transform_wraps_every_query_shape test to compile
the histogram query and verify its SQL uses a separately transformed event
column within the histogram window. Include the histogram query in the
parameter-placeholder count assertion, preserving the existing coverage for
timeseries, breakdown, period, and peer queries.

220-246: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use a separate transformed histogram column.

  • src/backend/services/analytics/src/domain/metric_results/compiler.rs#L220-L246: project the transformed event value under a distinct name, then compute min/max from that alias instead of reusing value in the same SELECT. That removes ClickHouse alias-shadowing risk.
  • src/backend/services/analytics/src/domain/metric_results/compiler.rs#L1133-L1168: add a transformed histogram assertion so this query shape stays covered.
🤖 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 220 - 246, The histogram query at
src/backend/services/analytics/src/domain/metric_results/compiler.rs:220-246
must project the transformed event value under a distinct alias and calculate
entity_lo/entity_hi from that alias, avoiding reuse of value in the same SELECT.
Add a transformed-histogram assertion at
src/backend/services/analytics/src/domain/metric_results/compiler.rs:1133-1168
to cover this query shape.
🤖 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/ingestion/gold/task_metric_observations.sql`:
- Around line 231-232: Align the numerator and denominator filters in the
estimation percentage calculation so both averages use only completed issues
with positive estimates and non-NULL spent time. Update the avgIf conditions in
the task metric query while preserving the existing nullIf division guard.
- Line 162: Update the interval-end calculation over e.evs so a final event
representing a reopened/current status uses now() rather than the historical
s.final_close_at. Preserve final_close_at for issues that remain closed,
ensuring the resulting end is not before the reopen span start and the
downstream interval filtering retains the in-progress interval.

---

Nitpick comments:
In `@src/backend/services/analytics/src/domain/metric_results/compiler.rs`:
- Around line 1133-1168: Extend the transform_wraps_every_query_shape test to
compile the histogram query and verify its SQL uses a separately transformed
event column within the histogram window. Include the histogram query in the
parameter-placeholder count assertion, preserving the existing coverage for
timeseries, breakdown, period, and peer queries.
- Around line 220-246: The histogram query at
src/backend/services/analytics/src/domain/metric_results/compiler.rs:220-246
must project the transformed event value under a distinct alias and calculate
entity_lo/entity_hi from that alias, avoiding reuse of value in the same SELECT.
Add a transformed-histogram assertion at
src/backend/services/analytics/src/domain/metric_results/compiler.rs:1133-1168
to cover this query shape.
🪄 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: 7a79a17e-1e63-47de-8a72-bde2d7882c2d

📥 Commits

Reviewing files that changed from the base of the PR and between 5124c78 and 3db4f6a.

📒 Files selected for processing (18)
  • src/backend/services/analytics/src/domain/metric_definitions/builtin.rs
  • src/backend/services/analytics/src/domain/metric_definitions/definition.rs
  • 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/domain/metric_results/batch.rs
  • src/backend/services/analytics/src/domain/metric_results/builder.rs
  • src/backend/services/analytics/src/domain/metric_results/compiler.rs
  • src/backend/services/analytics/src/domain/metric_results/validation.rs
  • src/backend/services/analytics/src/migration/m20260714_000001_metric_value_transform.rs
  • src/backend/services/analytics/src/migration/mod.rs
  • src/ingestion/connectors/task-tracking/jira/dbt/jira__task_users.sql
  • src/ingestion/connectors/task-tracking/youtrack/dbt/youtrack__task_users.sql
  • src/ingestion/dbt/tests/gold/assert_task_observations_entity_id_shape.sql
  • src/ingestion/dbt/tests/gold/assert_task_observations_nonnegative.sql
  • src/ingestion/dbt/tests/gold/assert_task_observations_subject_key_shape.sql
  • src/ingestion/dbt/tests/gold/assert_task_observations_unique_grain.sql
  • src/ingestion/gold/schema.yml
  • src/ingestion/gold/task_metric_observations.sql

Comment thread src/ingestion/gold/task_metric_observations.sql Outdated
Comment thread src/ingestion/gold/task_metric_observations.sql Outdated
aleksdotbar and others added 6 commits July 16, 2026 14:49
The last status span ended at final_close_at regardless of current
state, so a closed-then-reopened issue's current span ended before it
started and the interval sanity filter dropped the row. That hid the
reopen transition (reopened_within_14d missed every currently-reopened
issue and changed retroactively on re-close) and froze live in-progress
accrual. End the tail span per current state instead — at the close for
done issues, at now() otherwise — and count only pre-close spans toward
a close's dev/pickup time, so live rework accrues to the next close and
never retroactively inflates one already reported.

Estimation accuracy averaged estimates and spent time over different
row sets: avgIf skips NULLs per column, so NULL-spent issues fed the
estimate average but not the spent average. Pin both averages to the
same set — closed issues with a positive estimate and logged time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
The histogram projected the transformed event value as `value`,
shadowing the source column it reads in the same SELECT. Project it as
`event_value` in its own CTE and derive entity bounds and bin math from
that alias; cover the shape in the transform test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
ClickHouse re-inlines every WITH reference, so each of the eleven
measure branches re-evaluated the per-issue reconstruction — the build
read 70x the silver input and peaked over 5 GiB. Materialize the two
expensive stages (task_issue_state: field-history pivot + attribution +
close; task_status_intervals: status spans) as their own gold tables so
a branch re-scan costs a small sorted read: ~16x fewer rows read, ~14x
less memory, 6x faster, identical output.

Add the query_settings resource caps the other observation models
carry (1.5 GiB memory, 4 threads, external group-by/sort spill).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
The jira dbt scope intersected tag:silver with the jira downstream, so
the gold task-delivery chain hanging off class_task_* was never
selected — task metrics would refresh only on deploys, while every
other connector's plain tag:<name>+ scope rebuilds its observation
models each sync. Union in the gold slice of the jira downstream.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
union_by_tag discovers contributors at runtime and creates no dbt graph
edges — those come from the depends_on hints on each class model, which
listed jira only. A youtrack sync's name-derived tag:youtrack+ scope
therefore built staging views and nothing downstream: class_task_*
never absorbed youtrack rows on youtrack syncs, and a youtrack-only
instance would never refresh task metrics at all. Hint the seven
classes with youtrack staging (field history has no youtrack source
yet), mirroring the multi-source hints on the collaboration classes,
and correct the stale dbt_select note on the youtrack descriptor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/ingestion/gold/task_metric_observations.sql (1)

77-83: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep issue_facts grouped by source.

issue_id is only source-scoped upstream. Without insight_source_id in this grouping, matching IDs for the same tenant/entity/date across Jira or YouTrack sources collapse into one fact and mix interval totals.

Proposed fix
     SELECT
         s.tenant_id,
         s.entity_id,
+        s.insight_source_id,
         toDate(s.final_close_at) AS metric_date,
         s.issue_id,
...
-    GROUP BY s.tenant_id, s.entity_id, s.issue_id, toDate(s.final_close_at)
+    GROUP BY
+        s.tenant_id,
+        s.entity_id,
+        s.insight_source_id,
+        s.issue_id,
+        toDate(s.final_close_at)
🤖 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/gold/task_metric_observations.sql` around lines 77 - 83, Update
the GROUP BY in the issue_facts aggregation to include s.insight_source_id
alongside the existing tenant, entity, issue, and close-date keys. Preserve the
current joins and interval aggregation while ensuring issue facts remain
distinct across insight sources.
🤖 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/ingestion/gold/task_issue_state.sql`:
- Around line 64-74: Replace the non-NULL-preserving conditional aggregates at
all three affected sites: use maxIfOrNull for final_close_at in
task_issue_state.sql, and minIfOrNull for pickup_seconds and reopened_14d in
task_metric_observations.sql. Preserve the existing conditions and
derived-measure logic so no-match cases remain NULL.

---

Outside diff comments:
In `@src/ingestion/gold/task_metric_observations.sql`:
- Around line 77-83: Update the GROUP BY in the issue_facts aggregation to
include s.insight_source_id alongside the existing tenant, entity, issue, and
close-date keys. Preserve the current joins and interval aggregation while
ensuring issue facts remain distinct across insight sources.
🪄 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: d13a2e51-10c4-465c-9c75-774bfee795a3

📥 Commits

Reviewing files that changed from the base of the PR and between 8fafca2 and aed4657.

📒 Files selected for processing (16)
  • src/backend/services/analytics/src/domain/metric_definitions/definition.rs
  • src/backend/services/analytics/src/domain/metric_results/compiler.rs
  • src/ingestion/connectors/task-tracking/jira/dbt/jira__task_users.sql
  • src/ingestion/connectors/task-tracking/jira/descriptor.yaml
  • src/ingestion/connectors/task-tracking/youtrack/descriptor.yaml
  • src/ingestion/gold/schema.yml
  • src/ingestion/gold/task_issue_state.sql
  • src/ingestion/gold/task_metric_observations.sql
  • src/ingestion/gold/task_status_intervals.sql
  • src/ingestion/silver/task-tracking/class_task_comments.sql
  • src/ingestion/silver/task-tracking/class_task_field_metadata.sql
  • src/ingestion/silver/task-tracking/class_task_projects.sql
  • src/ingestion/silver/task-tracking/class_task_sprints.sql
  • src/ingestion/silver/task-tracking/class_task_statuses.sql
  • src/ingestion/silver/task-tracking/class_task_users.sql
  • src/ingestion/silver/task-tracking/class_task_worklogs.sql
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/ingestion/connectors/task-tracking/jira/dbt/jira__task_users.sql
  • src/backend/services/analytics/src/domain/metric_definitions/definition.rs
  • src/backend/services/analytics/src/domain/metric_results/compiler.rs

Comment thread src/ingestion/gold/task_issue_state.sql
@aleksdotbar aleksdotbar changed the title feat(metrics): task-delivery family on unified metrics feat(metrics): task-delivery on unified metrics Jul 16, 2026
@aleksdotbar aleksdotbar changed the title feat(metrics): task-delivery on unified metrics feat(metrics): task delivery on unified metrics Jul 16, 2026
aleksdotbar and others added 3 commits July 16, 2026 17:05
maxIf over the non-Nullable event_at returns the epoch default, not
NULL, when an issue has no done-category transition — so 3007 of 6742
issues carried a 1970 close time and passed the `final_close_at IS NOT
NULL` gate that marks an issue closed. Output was unaffected (the
is_done and duration gates masked them downstream) but the phantom
closes are semantically wrong and processed for nothing. maxIfOrNull
yields NULL on no match. Verified: every measure count/sum unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
aleksdotbar and others added 2 commits July 17, 2026 11:34
The task-users staging views gained tenant_id mid-SELECT and
class_task_users inherits its column order from that union, so the
positional incremental insert breaks on any instance whose table
predates the column. Add the guarded ADD/MODIFY AFTER heal to the
migrate hook; existing rows carry NULL tenant until the next sync
re-emits every user row. Staging needs no heal (both members are
views). Verified by simulating the pre-change table: heal converges
the schema and the positional insert succeeds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
On a fresh instance the deploy-time tag:gold build runs against the
silver placeholders (no staging is materialised yet, so the drop hook
leaves them standing), and the task observation chain reads tenant_id
from class_task_users — absent from the placeholder, the first deploy
would fail at the gold build. Verified by building all three compiled
gold models against a placeholder-schema database.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Aleksandr Barkhatov <pm@aleks.bar>
@aleksdotbar
aleksdotbar enabled auto-merge July 17, 2026 09:39
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.

feat: migrate task delivery metrics to unified metric system (backend)

2 participants