Skip to content

feat(personhog): tombstone by default in DeletePersons - #110276

Draft
nickbest-ph wants to merge 1 commit into
masterfrom
nick/person-replica-tombstone-default
Draft

nickbest-ph wants to merge 1 commit into
masterfrom
nick/person-replica-tombstone-default

Conversation

@nickbest-ph

Copy link
Copy Markdown
Contributor

Problem

A caller of the replica's DeletePersons RPC that leaves the mode unset gets a hard delete. Every production caller now names a mode, and the hard delete is the order that leaves a reused distinct id hidden in ClickHouse, so it should take an explicit request rather than be the thing you get by omission.

Changes

Nothing user-visible changes. No production caller sends an unset mode.

  • DELETE_PERSONS_MODE_UNSPECIFIED now means TOMBSTONE. HARD keeps its behavior and stays explicit.
  • The Python fake client follows the same rule, so a test that omits the mode gets a tombstone, and the test helper that mirrors the legacy ClickHouse delete now asks for HARD explicitly.
  • Mechanical: the proto comment, the replica README and the regenerated Node stub say the new default.

How did you test this code?

Test rationale: the replica service test for tombstone mode is parametrized over the unset and explicit modes, so a regression back to a hard default fails it. The fake-client test that expects hard removal now names the mode, which is the same contract the Temporal purge relies on.

Run locally: the replica unit and service_tests cases for DeletePersons against a persons test database, clippy, and the Python suites that go through the fake (personhog_client, models/person, the person API tests, project deletion).

Release status

  • No feature flag controls this change
  • This change is behind a feature flag and is not available to users
  • This change makes a previously flagged feature available to everyone

Automatic notifications

  • Publish to changelog?

Docs update

None.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Fable 5.1

@nickbest-ph nickbest-ph self-assigned this Oct 1, 2026
@trunk-io

trunk-io Bot commented Oct 1, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Trunk lane — backend Python lane

This PR is assigned to the backend Python lane. It runs backend Python tests and may merge in parallel with PRs in other lanes.

✅ Duplication (Python) — clean

New Python 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.

✅ 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.

@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Retrigger

[Critical risk] Changes default behavior of person deletion from hard to tombstone.

The PR appears safe to merge; the remaining issue is non-blocking test coverage for the Python fake’s new default.

Reviews (1) · Last reviewed commit: "feat(personhog): tombstone by default in..."

) -> person_pb2.DeletePersonsResponse:
self.calls.append(_Call("delete_persons", request))
tombstone = request.mode == person_pb2.DELETE_PERSONS_MODE_TOMBSTONE
tombstone = request.mode != person_pb2.DELETE_PERSONS_MODE_HARD

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.

P2 Fake default lacks test coverage The fake now tombstones when the mode is omitted, but its deletion tests specify either HARD or TOMBSTONE. The old hard-delete default could return without failing the Python suite, making tests that omit the mode behave differently. Parameterize the tombstone test to cover both an omitted mode and explicit TOMBSTONE.

Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/personhog_client/fake_client.py
Line: 627

Comment:
**Fake default lacks test coverage** The fake now tombstones when the mode is omitted, but its deletion tests specify either `HARD` or `TOMBSTONE`. The old hard-delete default could return without failing the Python suite, making tests that omit the mode behave differently. Parameterize the tombstone test to cover both an omitted mode and explicit `TOMBSTONE`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (12)
proto/AGENTS.md — auto-discovered
.cursor/rules/rust.mdc — auto-discovered
.agents/security.md — configured
rust/personhog-replica/AGENTS.md — auto-discovered
proto/README.md — configured
.agents/skills/adding-personhog-rpc/SKILL.md — configured
.agents/skills/reviewing-personhog-protocol/SKILL.md — configured
.agents/skills/sending-notifications/SKILL.md — configured
.agents/skills/writing-tests/SKILL.md — configured
docs/internal/person-data-access.md — configured
.claude/commands/conventions.md — configured
.agents/skills/writing-code-comments/SKILL.md — configured
📝 Walkthrough

Walkthrough

Unspecified person deletion now uses tombstoning in the Rust service and fake client. Hard deletion remains selected explicitly by DELETE_PERSONS_MODE_HARD. The changes update the mode comment, README, and tests to reflect these modes.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to 8e9f6

The default change is intentional, and inspected production callers explicitly select a mode. The remaining concerns are limited to the Python fake accepting unknown modes and lacking a test for its new default behavior; address or track these small consistency and coverage gaps.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 8e9f6

Known production deletion and purge callers retain their existing behavior, and the new default uses an established transactional tombstone path. The remaining risk is compatibility with older or external callers that may expect immediate physical deletion without specifying a mode.

Retained concerns

  • Low · security · inferred: If a legacy or external caller relies on the former omitted-mode behavior, deployment changes its request from physical deletion to retained person and distinct-ID tombstones. Such a caller must satisfy the tombstone revival, publication, and later-cleanup lifecycle rather than assume immediate erasure. Known in-repository production callers select explicit modes; no affected legacy caller was identified.
Security review details

Security Blast Radius

  • inferred — The changed behavior affects requests omitting mode at the existing DeletePersons endpoint. Each request is limited to 1000 UUIDs within its supplied team scope; the lifecycle spans PostgreSQL person and identity records, publication-queue state, and downstream ClickHouse tombstones. This per-request bound does not establish authorization or limit cumulative exposure through repeated requests.

Trust Boundaries and Controls

  • observed — The existing service rejects oversized UUID lists, malformed UUIDs, and unknown mode values before storage mutation. Both hard-delete resolution and tombstone selection use team-scoped predicates. The PR changes mode interpretation, not these controls; caller authorization for the supplied team is not established by this service excerpt.

Resilience and Maintainability Implications

  • observed — The existing tombstone path couples property scrubbing, identity deletion state, version increments, auxiliary cleanup, and durable publication queueing in one transaction. Lost-response retries can recover the same versions. Cleanup checks current identity reachability before removal, supporting failure containment without making downstream publication or eventual physical erasure an independently verified guarantee.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description includes the required Problem, Changes, testing rationale, release status, notifications, docs, and agent context sections. It clearly explains the behavior change, affected callers, a…
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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

@coderabbitai coderabbitai 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.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
posthog/personhog_client/fake_client.py-627-627 (1)

627-627: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reject unknown delete modes in the fake.

For a valid request with an unrecognized mode such as 99, the Rust service returns invalid_argument. The fake treats that mode as tombstone and returns success. A test can therefore accept a request that production rejects.

Suggested fix
         self.calls.append(_Call("delete_persons", request))
+        if request.mode not in (
+            person_pb2.DELETE_PERSONS_MODE_UNSPECIFIED,
+            person_pb2.DELETE_PERSONS_MODE_HARD,
+            person_pb2.DELETE_PERSONS_MODE_TOMBSTONE,
+        ):
+            raise ValueError(f"Unknown DeletePersonsMode {request.mode}")
         tombstone = request.mode != person_pb2.DELETE_PERSONS_MODE_HARD
🧹 Nitpick comments (1)
posthog/personhog_client/test_fake_client.py (1)

616-620: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the omitted delete mode in the fake client.

This test passes DELETE_PERSONS_MODE_HARD, and the other fake test passes DELETE_PERSONS_MODE_TOMBSTONE. No fake test omits mode, so changing omitted-mode handling back to hard deletion can pass all fake-client tests.

Add a separate case that omits mode and asserts tombstoning.

Suggested fix
     def test_delete_persons_still_removes_tombstoned_and_live_alike(self):
         resp = self.client.delete_persons(
             person_pb2.DeletePersonsRequest(
                 team_id=self.TEAM_ID,
                 person_uuids=["tombstoned", "live", "blocked"],
                 mode=person_pb2.DELETE_PERSONS_MODE_HARD,
             )
         )

         assert resp.deleted_count == 3
         for uuid in ("tombstoned", "live", "blocked"):
             assert not self._present(uuid)

+    def test_delete_persons_unspecified_mode_tombstones(self):
+        resp = self.client.delete_persons(
+            person_pb2.DeletePersonsRequest(team_id=self.TEAM_ID, person_uuids=["live"])
+        )
+
+        assert resp.tombstoned
+        assert resp.deleted_count == 1
+        stored = self.client.get_person_by_uuid(
+            person_pb2.GetPersonByUuidRequest(team_id=self.TEAM_ID, uuid="live")
+        ).person
+        assert stored.is_deleted
+
     def test_delete_persons_tombstone_mode_keeps_rows_and_reports_versions(self):

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 8fe2f680-7a36-4c35-ad4c-c2a76b213c45

📥 Commits

Reviewing files that changed from the base of the PR and between 3d99563 and 8e9f66c.

⛔ Files ignored due to path filters (1)
  • nodejs/src/common/generated/personhog/personhog/types/v1/person_pb.ts is excluded by !**/generated/**
📒 Files selected for processing (7)
  • posthog/personhog_client/fake_client.py
  • posthog/personhog_client/test_fake_client.py
  • posthog/test/persons.py
  • proto/personhog/types/v1/person.proto
  • rust/personhog-replica/README.md
  • rust/personhog-replica/src/service/mod.rs
  • rust/personhog-replica/tests/service_tests.rs

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.

@trunk-io

trunk-io Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

View Full Report ↗︎ ⋅ Docs

This branch has not been deployed

No deployments
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.

1 participant