Skip to content

fix: prune scoped metric deletes and enforce sample identity - #3235

Merged
Asherlc merged 2 commits into
mainfrom
foundation-independent-fixes
Nov 8, 2026
Merged

Asherlc merged 2 commits into
mainfrom
foundation-independent-fixes

Conversation

@Asherlc

@Asherlc Asherlc commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Scoped metric-stream deletion currently joins candidate IDs against unrelated physical versions across the table. Resolve complete versions through immutable (id,user_id,provider_id,recorded_at) membership, then recheck mutable fields on the latest full row. Enforce the existing deterministic ID at new-publication ingress while retaining historical wire IDs and main’s Redpanda contracts.

Also restrict Docker latest tagging to the default branch. Preserve curated history-recovery/identity evidence and provide an explicit disposition for all 728 frozen paths from superseded #3231. Broker infrastructure belongs to #3233; account erasure and required analytics/schema dependencies belong to #3234. Deferred transport/marker-v2 prototypes remain at the immutable source rather than being imported wholesale.

Validation:

  • RED observed on old source: pruning read 77,824 rows (bound <32,768); ingress accepted caller identity.
  • 129 focused unit tests passed (ingress, sink, consumer, writer, producer and routes).
  • 20 sink integration tests passed on each real engine: ClickHouse 26.10.2.103 and exact 26.6.1.1193, retries disabled.
  • Root TypeScript check, Biome and diff whitespace checks passed.
  • Isolated Compose containers/network removed; no production actions, image build, migrations, offset changes, archive-body reads or historical re-audits.

The fixture proves pruning and semantic regressions, not a universal production scan bound. Historical receipts are dated observations with their original source revisions. No timeout/retry or optimizer tuning was added.


Summary by cubic

Fixes scoped metric-stream deletion so it no longer joins candidate IDs against unrelated physical versions across the table. The sink now resolves complete versions through immutable (id,user_id,provider_id,recorded_at) membership before rechecking mutable scope fields on the latest row. Also enforces the deterministic ID at publication ingress while retaining historical wire IDs, and restricts Docker latest tagging to the default branch.

Written for commit a04b1dc. Summary will update on new commits.

View guided diff Turn on auto-fix

Summary by Sourcery

Correct scoped metric-stream deletion identity handling and publication ID validation while preserving historical compatibility and tightening release evidence.

Bug Fixes:

  • Prevent scoped metric-stream deletions from selecting unrelated physical versions by resolving immutable sample identities before applying latest-row scope checks.
  • Enforce deterministic metric sample IDs for new publications while continuing to accept historical wire IDs.

Enhancements:

  • Preserve curated history-recovery, identity-audit, and superseded-PR disposition evidence, including ownership boundaries for deferred work.

CI:

  • Restrict Docker latest image tags to builds from the default branch.

Documentation:

  • Document completed history recovery, validation evidence, limitations, and the disposition of all paths from the superseded change.

Tests:

  • Add coverage for deletion pruning, mutable field corrections, mixed scopes, identity enforcement, historical IDs, and both supported ClickHouse versions.

@coderabbitai

coderabbitai Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 16 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 2752451 and a04b1dc.

Files ignored due to path filters: 1

  • docs/evidence/pr-split-20261108/inventory.tsv is excluded by !**/*.tsv
Files selected for processing: 25

  • .github/workflows/build-docker-ml.yml
  • .github/workflows/build-docker.yml
  • .github/workflows/deploy.yml
  • docs/evidence/history-clickhouse-recovery-20261108/README.md
  • docs/evidence/history-clickhouse-recovery-20261108/progress.jsonl
  • docs/evidence/history-clickhouse-recovery-20261108/summary.json
  • docs/evidence/metric-stream-deletion-scope-audit-20261108/query.sql
  • docs/evidence/metric-stream-deletion-scope-audit-20261108/summary.json
  • docs/evidence/metric-stream-history-identity-audit-20261107/retained-comparison.sql
  • docs/evidence/metric-stream-history-identity-audit-20261107/summary.json
  • docs/evidence/metric-stream-identity-local-20261107/candidate-select.sql
  • docs/evidence/metric-stream-identity-local-20261107/implementation-verification.json
  • docs/evidence/metric-stream-identity-local-20261107/summary.json
  • docs/evidence/pr-split-20261108/README.md
  • docs/evidence/pr-split-20261108/summary.json
  • docs/production-incident-baseline.md
  • src/db/metric-stream-writer.ts
  • src/metric-stream/clickhouse-sink-identity.integration.test.ts
  • src/metric-stream/clickhouse-sink.integration.test.ts
  • src/metric-stream/clickhouse-sink.test.ts
  • src/metric-stream/clickhouse-sink.ts
  • src/metric-stream/events.test.ts
  • src/metric-stream/events.ts
  • src/metric-stream/redpanda-consumer.integration.test.ts
  • src/metric-stream/redpanda-consumer.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@sourcery-ai

sourcery-ai Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Data structures

Remove caller-supplied IDs from the metric-stream row input and always derive the deterministic sample identity for newly published events, while preserving historical wire-event parsing.

  • src/metric-stream/events.ts L47 (removed lines), L195, L246

API

Remove the optional ID from the writer insert interface so new callers cannot supply an identity that bypasses deterministic ID derivation.

  • src/db/metric-stream-writer.ts L22 (removed lines)

Implementation

Discover deletion candidates by immutable identity tuples, resolve complete versions, and then apply mutable scope predicates to each latest row without adding runtime tuning or changing acknowledgement behavior.

  • src/metric-stream/clickhouse-sink.ts L596-611, L680-689 (+2 small edits)

Configuration

Restrict the Docker latest tag to builds on the default branch across the standard, ML, and deploy workflows.

  • .github/workflows/build-docker-ml.yml L42
  • .github/workflows/build-docker.yml L42
  • .github/workflows/deploy.yml L93, L157

Documentation

Record the completed history-only recovery, its bounded health observations, and explicit limits on what those observations establish.

  • docs/evidence/history-clickhouse-recovery-20261108/README.md L1-36
  • docs/evidence/history-clickhouse-recovery-20261108/progress.jsonl L1-26
  • docs/evidence/history-clickhouse-recovery-20261108/summary.json L1-102
  • docs/production-incident-baseline.md L35654-35661

Preserve the deletion-candidate identity audit and its frozen query as historical evidence, not as authorization to rerun it.

  • docs/evidence/metric-stream-deletion-scope-audit-20261108/query.sql L1-10
  • docs/evidence/metric-stream-deletion-scope-audit-20261108/summary.json L1-146

Retain the frozen comparison and summary documenting the historical identity precondition and retained-version audit.

  • docs/evidence/metric-stream-history-identity-audit-20261107/retained-comparison.sql L1-32
  • docs/evidence/metric-stream-history-identity-audit-20261107/summary.json L1-131

Preserve the local candidate-selection qualification and implementation verification with their stated assumptions and scope limits.

  • docs/evidence/metric-stream-identity-local-20261107/candidate-select.sql L1-61
  • docs/evidence/metric-stream-identity-local-20261107/implementation-verification.json L1-55
  • docs/evidence/metric-stream-identity-local-20261107/summary.json L1-2338

Document the extraction from the superseded draft, dispositions for its 728 paths, sibling ownership boundaries, deferred experiments, and review follow-up.

  • docs/evidence/pr-split-20261108/README.md L1-38
  • docs/evidence/pr-split-20261108/inventory.tsv L1-729
  • docs/evidence/pr-split-20261108/summary.json L1-43

Tests

Exercise pruning and scoped-deletion semantics on ClickHouse, including mutable corrections, mixed scopes, timestamp precision, and acknowledgement behavior.

  • src/metric-stream/clickhouse-sink-identity.integration.test.ts L1-290

Update sink integration fixtures and regressions to retain historical wire IDs while checking latest-row behavior and deletion chunk boundaries.

  • src/metric-stream/clickhouse-sink.integration.test.ts L72-79, L337-343, L348-356 (+12 small edits)

Align sink query assertions with immutable identity tuple membership and latest-row scope checks.

  • src/metric-stream/clickhouse-sink.test.ts L812

Test deterministic publication identity, rejection of caller-provided IDs, and continued preservation of historical wire identity and timestamp precision.

  • src/metric-stream/events.test.ts L58, L91-133, L165-167

Remove caller-supplied IDs from new consumer fixtures while leaving historical wire-ID coverage intact.

  • src/metric-stream/redpanda-consumer.integration.test.ts L246 (removed lines)
  • src/metric-stream/redpanda-consumer.test.ts L70 (removed lines)

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Storybook previews for a4c060e1 are ready:

This comment updates automatically on each PR push.

@Asherlc
Asherlc marked this pull request as ready for review November 8, 2026 14:45
Copilot AI balanced review requested due to automatic review settings November 8, 2026 14:45
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@mergestorm-vortex

mergestorm-vortex Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Mergestorm reviews this repository's pull requests

Every PR gets AI code review that flags real bugs before they merge. This account's 30 free credits for this monthly window are used, so reviews are paused at a04b1dc and resume automatically on Nov 29 (UTC).

The Starter plan includes 120 credits a month.

Upgrade for more reviews →

@codecov

codecov Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.41%. Comparing base (2752451) to head (a04b1dc).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3235      +/-   ##
==========================================
- Coverage   97.41%   97.41%   -0.01%     
==========================================
  Files        1124     1124              
  Lines       61626    61625       -1     
  Branches    19092    19091       -1     
==========================================
- Hits        60032    60031       -1     
  Misses       1548     1548              
  Partials       46       46              
Flag Coverage Δ
integration 56.51% <75.00%> (+0.01%) ⬆️
unit 91.85% <100.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/metric-stream/clickhouse-sink.ts" line_range="679-683" />
<code_context>
-        )
-        WHERE ${correlatedLatestPredicates}
+        WHERE (metric_stream_row.id, metric_stream_row.user_id,
+            metric_stream_row.provider_id, metric_stream_row.recorded_at) IN (
+            SELECT id, user_id, provider_id, recorded_at FROM (
+              ${scopedCandidates}
+            )
+          )
+        GROUP BY metric_stream_row.id
</code_context>
<issue_to_address>
**Out-of-scope samples get deleted**

When a retained wire ID has versions with different user, provider, or recorded-time values, and an older version matches the delete scope while the latest does not, `scopedCandidates` limits the versions passed to `argMax`, so it can select the older matching row and exclude the actual latest row. The sink writes a tombstone based on that stale row, and the caller sees the current out-of-scope sample deleted.

Run `argMax` across every version of each candidate ID before applying the delete-scope predicate to the latest row.

Also at `src/metric-stream/clickhouse-sink.integration.test.ts:373`.
</issue_to_address>

### Comment 2
<location path="src/metric-stream/clickhouse-sink.ts" line_range="684" />
<code_context>
+          )
+        GROUP BY metric_stream_row.id
       )
-      WHERE latest_row.14 = 0
+      WHERE (${latestScopePredicate})
+        AND latest_row.14 = 0
</code_context>
<issue_to_address>
**Unrelated sample versions get deleted**

When a batch contains multiple scopes and a sample's older mutable fields match one scope while its latest row matches another, the sink can retain a sample through one candidate scope, then accept its latest row through a different scope because `latestScopePredicate` is an uncorrelated OR. It writes a tombstone even though no single scope matched both versions, so the caller sees data deleted unexpectedly.

Preserve the association between each candidate scope and its latest-row predicate when resolving identities.
</issue_to_address>

### Comment 3
<location path=".github/workflows/build-docker.yml" line_range="42" />
<code_context>
           tags: |
             type=sha,value=${{ github.sha }}
-            type=raw,value=latest
+            type=raw,value=latest,enable={{is_default_branch}}
       - name: Compute web asset base URL
         id: web-assets
</code_context>
<issue_to_address>
**Non-default builds replace latest**

When an ML or Deploy build runs from a non-default branch, including through manual dispatch, the updated `build-docker.yml` tag is conditional, but the ML and Deploy metadata entries still emit an unconditional `latest` tag. Those builds overwrite the shared tag, so users pulling `latest` receive the non-default-branch image.

Make the `latest` tag conditional on the default branch in every build workflow and metadata entry.
</issue_to_address>

### Comment 4
<location path="src/metric-stream/clickhouse-sink.ts" line_range="603" />
<code_context>
+      (conditions) => `SELECT candidate_row.id AS id, candidate_row.user_id AS user_id,
+            candidate_row.provider_id AS provider_id, candidate_row.recorded_at AS recorded_at
           FROM ${METRIC_STREAM_TABLE} AS candidate_row
           WHERE ${conditions.join(" AND ")}
-          GROUP BY candidate_row.id`,
+          GROUP BY candidate_row.id, candidate_row.user_id,
</code_context>
<issue_to_address>
**Activity-only deletes scan broadly**

When a deletion scope has `activityId` but no `userId`, `clickHouseDeleteScopeConditions` permits scopes containing only `activityId`, so this candidate query filters on `activity_id` without the leading `user_id` key. The table is ordered by `(user_id, activity_id, channel, recorded_at, id)`, so ClickHouse cannot prune by a primary-key prefix for that scope and reads broadly across the table, risking another slow delete.

Add an access path suitable for activity-only candidate lookup, or require a leading-key scope before running this query.
</issue_to_address>

### Comment 5
<location path="src/metric-stream/clickhouse-sink.integration.test.ts" line_range="140-217" />
<code_context>
     await client.close?.();
   });

+  it("prunes unrelated identities while applying a scoped deletion", async () => {
+    const userId = randomUUID();
+    const otherUserId = randomUUID();
+    const activityId = randomUUID();
+    const queryId = randomUUID();
+    const fixtureRows = 65_536;
+    try {
+      await client.command({
+        query: `INSERT INTO ${METRIC_STREAM_TABLE}
+          (id, user_id, activity_id, provider_id, recorded_at, channel, scalar)
+          SELECT reinterpretAsUUID(MD5(concat(toString({userId:UUID}), toString(number)))),
+            if(number % 16 = 0, {userId:UUID}, {otherUserId:UUID}),
+            {activityId:UUID}, 'identity-pruning-test',
+            toDateTime64('2026-06-08 12:00:00', 6), 'heart_rate', 72
+          FROM numbers(${fixtureRows})`,
+        query_params: { userId, otherUserId, activityId },
+      });
+      await markMetricStreamScopesDeletedInClickHouse(
+        {
</code_context>
<issue_to_address>
**Integration test file exceeds limit**

The added integration scenarios leave `clickhouse-sink.integration.test.ts` at 1,131 lines, exceeding the repository’s 1,000-line ceiling and leaving the sink coverage in an oversized test file.

Split the added integration scenarios into focused `*.integration.test.ts` files so each remains below 1,000 lines.

Also at `src/metric-stream/clickhouse-sink.integration.test.ts:441-599`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 5 findings to address first, and if the identity tuple or scope matching is wrong, a deletion event could write tombstones for metric rows that should remain live. Reverting stops the new selection logic but does not remove tombstones already written; affected rows would need explicit repair.

Blocking findings: src/metric-stream/clickhouse-sink.ts:683, src/metric-stream/clickhouse-sink.ts:684, .github/workflows/build-docker.yml:42, src/metric-stream/clickhouse-sink.ts:603, src/metric-stream/clickhouse-sink.integration.test.ts:217


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/metric-stream/clickhouse-sink.ts Resolved
Comment thread src/metric-stream/clickhouse-sink.ts Resolved
Comment thread .github/workflows/build-docker.yml Resolved
Comment thread src/metric-stream/clickhouse-sink.ts Resolved
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@Asherlc

Asherlc commented Nov 8, 2026

Copy link
Copy Markdown
Owner Author

Addressed all five items in Sourcery review, with individual evidence replies in each thread.

a04b1dcc8 guards the remaining three latest-tag entries, splits the integration file below the line limit, and documents the activity-only candidate-scan boundary. The approved immutable identity contract and same-revision scope-union semantics are retained; their counterexamples and requested strategy changes are explained in the corresponding threads and extraction record.

All 20 integration tests pass after the split on ClickHouse26.6.1.1193, retries disabled. Root typecheck, Biome and diff checks pass. CI on the new head is pending; previous head passed all103 checks. No production changes.

@Asherlc
Asherlc merged commit afa8b6e into main Nov 8, 2026
118 checks passed
@Asherlc
Asherlc deleted the foundation-independent-fixes branch November 8, 2026 17:20
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.

2 participants