Repository navigation
feat(flags): sweep flag_evaluations in property removal requests - #112066
Conversation
Property removal now runs its per-shard copy, delete, reingest and verify ops on sharded_flag_evaluations as well as the events tables, so a request whose flag calls carry a named property completes instead of being refused. - DeletionTarget.property_rewrite marks each table the job sweeps, and the fan-out builds its targets from it instead of a fixed list. - The job drops the person_properties half for a table that does not store it, and finds extracted columns on each table's own read table. - A request with a HogQL predicate skips flag_evaluations and refuses while the table holds matching rows. - The whole-request verify counts every swept table through a data node. - assert_no_unsweepable_rows logs each target it skips without a count, with the reason. Claude-Session: https://claude.ai/code/session_015t1ujRuwiinSWVo39yGvcE
|
😎 Merged successfully - details. |
🦔 PostHog Review reviewed this pull requestFound 0 must fix, 3 should fix, 2 consider. Published 5 findings (view the review). Resolved comments: 4 fixed, 2 declined, 2 already settled, 2 left for you |
|
[Critical risk] Extends property deletion to flag evaluations table. No new blocking issue was identified; the PR appears safe to merge on this review’s findings. Reviews (2) · Last reviewed commit: "fix(flags): leave rows past the TTL out ..." |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Rewriting _timestamp can invalidate the Kafka consumer-lag safety check and permit duplicate backfill ingestion.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Extends property-removal requests to rewrite matching flag_evaluations rows alongside events.
Changes:
- Adds target-specific property-rewrite capabilities and verification.
- Resets extracted flag-evaluation columns during rewrites.
- Adds integration coverage and updates deletion documentation.
| File | Description |
|---|---|
posthog/models/test/test_deletion_targets.py |
Tests rewrite capability validation. |
posthog/models/flag_evaluations/sql.py |
Updates extracted-column documentation. |
posthog/models/deletion_targets.py |
Registers rewrite capabilities and skip reasons. |
posthog/models/data_deletion_request.py |
Clarifies event-only admin counts. |
posthog/dags/tests/test_data_deletion_requests.py |
Tests flag-evaluation rewriting and verification. |
posthog/dags/data_deletion_requests.py |
Extends rewrite fan-out and verification. |
docs/internal/clickhouse-deletion-coverage.md |
Documents coverage, limitations, and recovery behavior. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
HostHog preview —
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughProperty-removal requests now use declared rewrite coverage and target-specific criteria. The workflow gates unsupported rewrites and HogQL-excluded requests, then copies, deletes, and verifies rows on eligible targets. Verification counts remaining rows across targets. Tests and documentation cover flag-evaluation rewrites and their limitations. Priority: ➖ Normal Merge Risk: 🔵 Low · up to The change extends property removal to flag evaluations with target-aware rewrite and verification. The remaining issue is stale wording that could lead an operator to think an erasure is incomplete when it is complete. It is safe to merge once the comments and docs are cleaned up. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to A failed removal resumed after a later removal can restore personal data covered by the later completed request. Approval and team scoping limit exposure, but completed deletion guarantees need to remain valid through recovery. Retained concerns
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 1✅ Passed checks (1 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
posthog/dags/tests/test_data_deletion_requests.py (1)
2687-2724: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winAdd unaffected flag-evaluation controls.
Every inserted row has both candidate properties and falls within the request window. The test can pass if the rewrite ignores the property filter, and it has no time-boundary control.
_flag_evaluation_rowsreads onlyPROP_TEAM_ID, so it cannot detect changes to another team’s rows. Add rows without the requested property and outside the request window, plus a row for another team that you read separately. Assert that all remain unchanged.
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: PostHog/posthog/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
0b62db7d-0bdd-493e-b9db-c12ba65a5c18
📒 Files selected for processing (7)
docs/internal/clickhouse-deletion-coverage.mdposthog/dags/data_deletion_requests.pyposthog/dags/tests/test_data_deletion_requests.pyposthog/models/data_deletion_request.pyposthog/models/deletion_targets.pyposthog/models/flag_evaluations/sql.pyposthog/models/test/test_deletion_targets.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.
|
PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏 |
The admin Verify button marks a property removal completed when count_remaining_property_events returns zero, but that count read only events. With flag_evaluations now swept, a request whose events chain finished and whose flag_evaluations chain failed could be marked completed while flag rows still held the property. The count now loops over every default deletion target the job rewrites, the same targets the job sweeps: - Extracted columns are found on each target's own read table. - A target without person_properties drops that half, and is skipped when the request names only person properties. - A target that cannot take the HogQL fragment is counted without it, which matches a superset and can only hold a request open. The flag_evaluations verify test now also asserts that the admin count agrees with the job's verify on each case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 1d01e0a4-f637-41c7-b519-c09a2dd1e3a2
🤖 CI report
|
| First copy | Second copy | Lines | Tokens |
|---|---|---|---|
posthog/models/data_deletion_request.py:824 |
posthog/models/data_deletion_request.py:967 |
19 | 99 |
✅ Duplication (TypeScript) — clean
New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.
🚨 Comment density — 10% of added code lines are comments (50 of 490)
This section warns when comments are more than 3% of the code lines a PR adds, and alerts above 6%. Before agent-assisted PRs, the typical share was about 2%. Only full-line comments count. Docstrings, generated files, snapshots, migrations, and workflow files are left out.
Comments that restate the code, record how the change came about, or narrate the next line add noise for the next reader. Keep the comments that explain a reason the code cannot show, and remove the rest. See .agents/skills/writing-code-comments/SKILL.md for the house rules.
Files with the most added comment lines:
| File | Comment lines | Added lines |
|---|---|---|
posthog/models/deletion_targets.py |
17 | 42 |
posthog/dags/tests/test_data_deletion_requests.py |
16 | 185 |
posthog/dags/data_deletion_requests.py |
10 | 189 |
posthog/models/flag_evaluations/sql.py |
7 | 7 |
This check does not block merging. It updates on every push and clears when the share drops.
⚠️ Playwright — 2 flaky
🎭 Playwright report · View test results →
- Logout in another tab results in logout in the current tab too (chromium)
- Inline editing insight description via compact card popover (chromium)
These issues are not necessarily caused by your changes.
Annoyed by this section? Help fix flakies and failures and it will go green!
⚠️ Backend snapshots — 1 updated (1 modified, 0 added, 0 deleted)
Query snapshots: Backend query snapshots updated
Changes: 1 snapshots (1 modified, 0 added, 0 deleted)
What this means:
- Query snapshots have been automatically updated to match current output
- These changes reflect modifications to database queries or schema
Next steps:
- Review the query changes to ensure they're intentional
- If unexpected, investigate what caused the query to change
The property-removal copy replaced _timestamp with the marker plus one second on every table. That bump is the ReplacingMergeTree version on the events tables. sharded_flag_evaluations is a plain MergeTree, so the bump has no effect on its merges. There _timestamp is the Kafka message time, and flag_evaluations_backfill reads max(_timestamp) per partition as the consumer position. A rewritten row could make a stalled partition look current and let the backfill pass its lag guard. The copy now reads the table engine and bumps _timestamp only on a ReplacingMergeTree. flag_evaluations keeps the original value, and inserted_at still carries the marker. The flag_evaluations full-job test pins a Kafka time on its rows and asserts that it survives the rewrite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 1d01e0a4-f637-41c7-b519-c09a2dd1e3a2
The copy, delete and shard verify ops each run SYSTEM SYNC REPLICA LIGHTWEIGHT before they read. The reingest did not. A re-execution can land on another replica of the shard than the failed attempt. If that replica has not fetched the month the earlier attempt inserted, the leftover count misses those rows and the insert adds them again with deduplication off. On events the ReplacingMergeTree merges the extra copies away. sharded_flag_evaluations is a plain MergeTree, so they stay and inflate per-flag usage counts. The reingest now syncs the replica once before its month loop, as its sibling ops do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 1d01e0a4-f637-41c7-b519-c09a2dd1e3a2
…ring The _refuse_property_removal_unsweepable docstring pointed at _property_rewrite_scopes, which does not exist. The helper that leaves HogQL-excluded targets out of the sweep is _property_rewrite_targets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 1d01e0a4-f637-41c7-b519-c09a2dd1e3a2
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟠 Major · Gate final verification on Kafka progress through the marker. · data_deletion_requests.py:1710-1759
posthog/dags/data_deletion_requests.py:1710-1759
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftGate final verification on Kafka progress through the marker.
FLAG_EVALUATIONSnow enters property-removal targeting, but its Kafka materialized view uses message time forinserted_at. If a pre-marker message reaches ClickHouse afterverify_property_removalruns, the count misses it. The job then cleans staging and marks the immediate requestCOMPLETED, leaving the property in the new row. Wait for the consumer to process through the marker before running the final check.
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: PostHog/posthog/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
e7092da6-50bf-4997-b724-d4334ecef2ba
📒 Files selected for processing (3)
posthog/dags/data_deletion_requests.pyposthog/dags/tests/test_data_deletion_requests.pyposthog/models/data_deletion_request.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 4 remain after this review.
- accepts_property_rewrite is a plain flag again, and the fan-out sweeps only tables that set it. sharded_events_json keeps it off, so the gate alone handles that table instead of shard ops that copy nothing once the gate passes. - The whole-request verify builds its predicate with _shard_predicate and counts with surviving_rows_sql, one table per future. - _scoped_to returns the narrowed request, and the empty-request check sits in the two places that skip a table. - The coverage doc describes how property removal treats sharded_events_json. - The admin remaining count skips tables without accepts_property_rewrite. Claude-Session: https://claude.ai/code/session_015t1ujRuwiinSWVo39yGvcE
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: PostHog/posthog/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
bdf25bef-f7b5-44fb-9c74-f5a295178ed5
📒 Files selected for processing (5)
docs/internal/clickhouse-deletion-coverage.mdposthog/dags/data_deletion_requests.pyposthog/models/data_deletion_request.pyposthog/models/deletion_targets.pyposthog/models/test/test_deletion_targets.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.
…hecks sharded_flag_evaluations drops a part once its newest row passes the 90-day TTL. A request window can reach expired rows that a live part still holds, and the copy stages them. A reingested part that holds only those rows can drop before the reingest or the shard verify counts it, so both checks reported missing uuids and every retry failed the same way. DeletionTarget.ttl_days now marks a table with a TTL. On such a table both checks compare only rows at least two days short of their TTL, counted with one cutoff on the table and on the staged files. A part that holds one of those rows cannot drop. Tables without a TTL keep the counts the copy recorded. Claude-Session: https://claude.ai/code/session_015t1ujRuwiinSWVo39yGvcE
|
Risk: No findings The only change since the last review is one automated commit (tests-posthog[bot], "test(backend): update query snapshots") that regenerates the .ambr snapshot file for dashboard tests — renumbering recorded queries and adding a new snapshot of the dashboard-tiles serializer query. No production code changed. The snapshot contains only column-name lists from recorded SQL (names like api_token and password are column references, not values), so there is no security impact. Sentinel reviewed |
…tions-property-removal
There was a problem hiding this comment.
Not approved — escalated to a human reviewer.
Re-add the stamphog label to request another review once you have addressed this.
This extends irreversible data-erasure logic to a new table, which is risky territory. There's an unresolved thread describing a real failure: rows past the 90-day TTL can make verification fail on every retry. The author's familiarity is only moderate and no human or owning-team reviewer has signed off.
- Author wrote 48% of the modified lines and has 52 merged PRs in these paths (familiarity MODERATE).
- 👍 on the PR from greptile-apps[bot].
- Unresolved inline thread (@PostHog on posthog/dags/data_deletion_requests.py): rows past the flag_evaluations 90-day TTL can be dropped by a TTL merge after reingest, so the uuid count no longer matches and every retry fails verification. The thread says a human must choose between leaving expired rows out of the sweep or adding per-table TTL handling on DeletionTarget.
- The PR's own warning describes late-arrival races, caused by inserted_at being stamped with the Kafka message time. In some of these cases a row is deleted without a staged copy, or keeps the property after the request completes. The fix is deferred to a follow-up change to the materialized view.
- CodeRabbit raised an unresolved concern about gating the final verify on Kafka consumer progress. Copilot raised one about preserving the Kafka _timestamp, which the PR says it addressed.
Gate mechanics and policy version
| Gate | Result | |
|---|---|---|
| prerequisites | ✓ | all clear |
| deny-list | ✓ | no deny categories matched |
| size | ✓ | 511L, 4F substantive, 853L/7F incl. docs/generated/snapshots — within ceiling |
| tier | ✓ | T1-agent / T1d-complex (853L, 7F, two-areas, feat) |
| stamphog 2.3.1 | .stamphog/policy.yml @ d74301c · reviewed head d74301c |
…tions-property-removal # Conflicts: # docs/internal/clickhouse-deletion-coverage.md # posthog/dags/data_deletion_requests.py # posthog/dags/tests/test_data_deletion_requests.py # posthog/models/deletion_targets.py
There was a problem hiding this comment.
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.
🟡 Other comments (1)
posthog/models/deletion_targets.py-104-115 (1)
104-115: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winRemove the stale pre-cutover blind-spot text.
The comment says rows stored before the producer stopped sending
person_properties"hold real values wherever the column survives." The column was dropped on both prod clusters, andposthog/models/flag_evaluations/sql.pyno longer declares it. So no stored value is left in reach. The same claim appears at Lines 206-208. It also appears indocs/internal/clickhouse-deletion-coverage.mdLine 159. An operator who reads this text can conclude that an erasure is incomplete when it is complete. Remove the blind-spot sentences, or state that the column is absent.This follows from the author's clarification on the earlier review thread.
Source: Learnings
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: PostHog/posthog/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
72988ec1-f028-4818-8395-9708f8e64354
📒 Files selected for processing (6)
docs/internal/clickhouse-deletion-coverage.mdposthog/dags/data_deletion_requests.pyposthog/dags/tests/test_data_deletion_requests.pyposthog/models/data_deletion_request.pyposthog/models/deletion_targets.pyposthog/models/flag_evaluations/sql.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.
There was a problem hiding this comment.
Not approved — escalated to a human reviewer.
Re-add the stamphog label to request another review once you have addressed this.
This extends the destructive copy, delete and reingest path for customer data deletion to a new production table. The only assurance is bot reviews with some findings still open, the author's familiarity is only moderate, and merging safely depends on a migration having already run in US and EU. A teammate who knows this area needs to review it. Suggested reviewers: Paweł Szczur, Rory Shanks.
- Author wrote 47% of the modified lines and has 54 merged PRs in these paths (familiarity MODERATE).
- coderabbitai[bot] reviewed the current head.
- Risky territory: the property-removal job now deletes and reinserts rows in
flag_evaluationsin production, and a mistake there means data loss or leaves the property in place. The only review on the current head is a CodeRabbit COMMENTED review, which has a docs nit still open. - The PR description says to merge only after the #112264 migration has run in US and EU. Otherwise late rows can lose their staged copy, wedge verification, or keep the property after the request completes. A human needs to confirm that ordering.
- The author lists as untested a multi-replica shard, prod-sized data and the extracted-column behavior on production ClickHouse. Those are the conditions where a delete, reingest and verify cycle is most likely to go wrong.
Gate mechanics and policy version
| Gate | Result | |
|---|---|---|
| prerequisites | ✓ | all clear |
| deny-list | ✓ | no deny categories matched |
| size | ✓ | 514L, 4F substantive, 849L/7F incl. docs/generated/snapshots — within ceiling |
| tier | ✓ | T1-agent / T1d-complex (849L, 7F, two-areas, feat) |
| stamphog 2.3.1 | .stamphog/policy.yml @ f417331 · reviewed head f417331 |


Problem
flag_evaluationstable holds matching rows, and only the table's TTL clears that, which takes up to about 120 days.Changes
flag_evaluationsrows with the same per-shard copy, delete, reingest and verify ops it runs on events.$feature_flagcompletes. The cleaned row goes back with an emptyflag_key, so per-flag usage counts stop counting it.flag_key,response,session_id,request_id,$group_0to$group_4) when their source property is erased. The job finds them by theircolumn_materializercomments on theflag_evaluationsread table.flag_evaluationsstores no person properties, so the job drops that half of a request there. A request naming only person properties skips the table.flag_evaluationsout of the sweep, because the predicate only compiles against events. The gate refuses that request while the table holds rows matching its other criteria.DeletionTarget.accepts_property_rewritedecides which tables the fan-out sweeps, instead of a fixed list of events tables. The gate and the whole-request verify read the same flag.sharded_events_jsonis swept on the events cluster, as on master.flag_evaluationsrows keep their Kafka_timestamp, which the backfill reads as its consumer position. Events rows still get the version bump theirReplacingMergeTreeneeds.DeletionTarget.ttl_daysset, both checks count only rows at least two days short of the TTL, on the table and in the staged files alike.sharded_events_json, as master does, because its predicate is legacy-only.flag_evaluationsstays out ofMATERIALIZATION_VALID_TABLES. The job finds the extracted columns throughsystem.columns, and adding the table there would also enroll it inmaterialize()and the HogQL property planner.Warning
Merge this only after the #112264 migration has run in US and EU. It stamps
flag_evaluations.inserted_atat insert, so a row the consumer reads after the request starts falls outside it, as on events. With the old Kafka-time stamp, a late row could lose its staged copy, wedge verification, or keep the property after the request completes.Note
A request covering all events now also rewrites the team's whole
flag_evaluationsrange through S3, which lengthens the job. The size estimate an approver sees still counts onlyevents.How did you test this code?
Test rationale:
test_full_job_property_removal_rewrites_flag_evaluations(3 cases, one with a Kafka replay duplicate) fails if the sweep skipsflag_evaluationsor an extracted column keeps its old value. It also fails if a duplicate breaks the request, the job queriesperson_propertiesthere, or a rewritten row loses its Kafka_timestamp. The events full-job tests cannot see this table.test_verify_property_removal_narrows_person_properties_on_flag_evaluations(5 cases) replaces a fan-out-only version. Verify runs after the shards rewrote rows, so a query error there leaves an erasure half done. It also checks that the admin count matches the job's verify.test_get_property_removal_shards_refuses_a_hogql_predicate_when_flag_evaluations_holds_matching_rowsreplaces the old refusal test, now scoped to the HogQL case.test_an_events_schema_target_cannot_take_the_property_rewrite_without_person_propertiesis a pure unit test for the remainingDeletionTargetguard.test_full_job_property_removal_survives_the_ttl_dropping_reingested_flag_evaluationsfails if a restore check counts rows past the TTL. Tests build the table without a TTL, so it deletes the expired cleaned row between reingest and verify, where the drop would. It failed on the previous head with the mismatch ReviewHog reported.test_native_property_removal_gate_checks_retained_copiesnow runs the gate on a copy ofEVENTS_JSONwithout the flag, since the job sweeps that table after feat(clickhouse): sweep events_json with patch parts on the events cluster #112514.events.posthog/dags/tests/test_data_deletion_requests.py,test_deletes.py,test_person_overrides.py,posthog/models/test/test_deletion_targets.py,posthog/models/test/test_data_deletion_request.pyandposthog/clickhouse/test/test_deletion_coverage.py.test_delete_person_events_op_runs_lightweight_delete_per_shardfailed once in a full local run and passed three times alone. This PR does not touch the person-deletion path.test_data_deletion_requests.pystays out of.github/new-events-schema-targets.txt, as that file explains, so no label is needed.Release status
Automatic notifications
Docs update
Internal only:
docs/internal/clickhouse-deletion-coverage.mdgains a "Property removal onflag_evaluations" section.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: Claude Code, Claude Opus 5.5 (
claude-opus-5-5)MATERIALIZEDcolumns. feat(data-deletion): split property removal into per-shard s3-staged ops #110902 replaced that design, so I rebuilt the change on it, where its SELECT-based copy and uuid counting make all three unnecessary.DeletionTarget.property_rewrite.--deepagainst the merge base. Its one finding, a stale line aboutsharded_events_jsonin the coverage doc, is fixed._timestamp, the replica sync before reingest, the admin count, and a docstring. I kept them and adapted the admin count to the flag.simplifypass found that thesharded_events_jsonsweep could never copy a row, which led to dropping it and returning to a plain flag. feat(clickhouse): sweep events_json with patch parts on the events cluster #112514 then made that sweep real on the events cluster, and the merge with master took its targets.DeletionTarget./writing-tests,/writing-code-comments,/simplify,/reviewing-with-coderabbit,/stacking-prs,/writing-pr-descriptions,/plain-writing,/go.https://claude.ai/code/session_015t1ujRuwiinSWVo39yGvcE