feat(error-tracking): move autocapture opt-in to settings (phase 1) - #70701
Conversation
Expand phase of moving Team.autocapture_exceptions_opt_in onto ErrorTrackingSettings. Adds a nullable column, a post_save signal on Team that dual-writes changes to the settings model, and a backfill command for teams that opted in before the signal shipped. Reads still come from Team, so behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migration SQL ChangesHey 👋, we've detected some migrations on this PR. Here's the SQL output for each migration, make sure they make sense:
|
🔍 Migration Risk AnalysisWe've analyzed your migrations for potential risks. Summary: 1 Safe | 0 Needs Review | 0 Blocked ✅ SafeBrief or no lock, backwards compatible 📚 How to Deploy These Changes SafelyAddField: This operation acquires a brief lock but doesn't rewrite the table. Deployment uses lock timeouts with automatic retries, so lock contention will cause retries rather than connection pile-up. Last updated: 2026-07-14 12:25 UTC (5762ffe) |
The backfill snapshotted opted-in team ids once at start, then wrote a hard-coded True per team. A team that disabled autocapture during the run would get clobbered back to True from the stale snapshot, diverging from Team until the next toggle. Process candidates in batches (default 100); each batch re-reads the teams still opted in right now and bulk-upserts only those, so a disable landing after the snapshot is skipped. Also logs per-batch progress with the last team_id for precise resume. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Reviews (1): Last reviewed commit: "fix(error-tracking): re-read live team s..." | Re-trigger Greptile |
A --batch-size of 0 crashed range() with ValueError and a negative value iterated empty chunks and finished syncing nothing. Validate up front and raise CommandError so this manual pre-cutover step fails fast with a clear message instead of crashing or silently reporting no synced rows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🤖 CI report
|
| File | Patch | Uncovered changed lines |
|---|---|---|
products/error_tracking/backend/management/commands/backfill_error_tracking_autocapture_opt_in.py |
81.8% | 56, 61, 67, 73, 79, 110–111, 159, 162, 164 |
🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 29361469017 -n patch-coverage), or the coverage-data block at the end of this comment.
Per-product line coverage (touched products)
| Product | Coverage | Lines |
|---|---|---|
error_tracking |
██████████████████░░ 89.6% |
9,749 / 10,883 |
Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.
Batching shrank the read-vs-disable window but didn't close it. Lock the Team rows with select_for_update while re-reading live opt-in state and upsert in the same transaction, so a concurrent disabling Team.save() serializes with the batch: it either commits first (we skip the team) or blocks until we commit (its signal then flips the row to False). Removes the staleness window entirely. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem
The exception autocapture toggle is stored on
Team.autocapture_exceptions_opt_in, but it belongs with the rest of the error tracking settings. We want it on theErrorTrackingSettingsmodel so the setting lives with its product and stops adding surface to the hotTeamtable.This is the first of a few PRs. It only stages the data. Nothing reads the new location yet, so behavior is unchanged.
Changes
Expand phase of a standard expand / backfill / contract move.
autocapture_exceptions_opt_incolumn onErrorTrackingSettings. Nullable on purpose: it matches the sibling fields, andnullreads as disabled just like the oldbool(None)coercion. It also sidesteps the cross-languageNOT NULLhazard, since nodejs writes rawINSERTs into this table.post_savesignal onTeamthat dual-writes any opt-in change ontoErrorTrackingSettings. Enabling creates a row, disabling syncs the existing row, and teams that never opted in stay row-less.sync_autocapture_opt_inhelper as the single source of the "only opted-in teams get a row" rule, shared by the signal and the backfill.backfill_error_tracking_autocapture_opt_inmanagement command for teams that opted in before the signal shipped. Dry-run by default, idempotent, run manually on a pod.Why a signal instead of hooking the serializers: there are three real writers of this field (the Team serializer, the Project serializer passthrough, and product enablement). One
post_savereceiver covers all of them plus admin and shell writes. It also keeps the dependency pointing product to core, so we avoid importing the isolated error tracking facade into the hot, early-loadedteam.pyandproject.py. No serializer changes at all.Note
Reads still come from
Team. Moving reads (remote config, serializers, the frontend toggle) and dropping the old column come in later PRs.Deploy order: ship this first so the signal starts dual-writing, then run the backfill on a pod for the historical rows. Because the signal covers everything from deploy onward, there is no need to re-run the backfill before the read cutover.
How did you test this code?
Automated tests I (Claude) actually ran locally, all green:
products/error_tracking/backend/tests/test_autocapture_opt_in_sync.py— the dual-write signal. Catches: enabling creates the row, disabling syncs the existing row without deleting it (the stale-row bug a snapshot-only backfill would leave), never-opted-in teams get no row, and a save that doesn't touch the field doesn't resync.products/error_tracking/backend/management/commands/test/test_backfill_error_tracking_autocapture_opt_in.py— the backfill only touches historical opted-in teams, creates or updates their row while preserving other settings, and writes nothing on a dry run.posthog/api/test/test_team.py(218),posthog/api/test/test_product_enablement.py(11), and the error tracking settings API tests (10) all pass unchanged. No query-count assertions wrap Team saves, and the one unused-snapshot warning intest_team.pyis pre-existing (confirmed by stashing this change).I did not do manual UI testing.
Automatic notifications
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
I (actually Claude, Opus 4.8) wrote this under my direction. We scoped it first: located every reader and writer of
autocapture_exceptions_opt_in, and found that the companionautocapture_exceptions_errors_to_ignorefield is effectively dead (not in remote config, no SDK consumer, no UI), so it is deliberately left out of this move.The dual-write mechanism was the main design decision. A backfill-only Phase 1 leaves a hole: a team that disables autocapture between the backfill and the read cutover would keep capturing, because the backfill only writes
True. So we added the signal to keep the new column continuously correct, and demoted the backfill to historical rows.Signal vs serializer hooks was the other call. Explicit hooks would have meant three write sites and importing the isolated product facade into core hot paths. The signal is one chokepoint with a clean dependency direction, at the cost of a guarded no-op query on Team saves (rare settings actions, not per-request).
Skills invoked:
/django-migrations(nullable column, hot-table and cross-languageNOT NULLchecks) and/writing-tests(gated each test to a named regression).