Skip to content

feat(error-tracking): move autocapture opt-in to settings (phase 1) - #70701

Merged
ablaszkiewicz merged 4 commits into
masterfrom
feat/et-autocapture-opt-in-to-settings
Jul 14, 2026
Merged

feat(error-tracking): move autocapture opt-in to settings (phase 1)#70701
ablaszkiewicz merged 4 commits into
masterfrom
feat/et-autocapture-opt-in-to-settings

Conversation

@ablaszkiewicz

Copy link
Copy Markdown
Contributor

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 the ErrorTrackingSettings model so the setting lives with its product and stops adding surface to the hot Team table.

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.

  • New nullable autocapture_exceptions_opt_in column on ErrorTrackingSettings. Nullable on purpose: it matches the sibling fields, and null reads as disabled just like the old bool(None) coercion. It also sidesteps the cross-language NOT NULL hazard, since nodejs writes raw INSERTs into this table.
  • A post_save signal on Team that dual-writes any opt-in change onto ErrorTrackingSettings. Enabling creates a row, disabling syncs the existing row, and teams that never opted in stay row-less.
  • A sync_autocapture_opt_in helper as the single source of the "only opted-in teams get a row" rule, shared by the signal and the backfill.
  • A backfill_error_tracking_autocapture_opt_in management 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_save receiver 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-loaded team.py and project.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.
  • Regression sweep for the new global signal: 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 in test_team.py is pre-existing (confirmed by stashing this change).

I did not do manual UI testing.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

🤖 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 companion autocapture_exceptions_errors_to_ignore field 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-language NOT NULL checks) and /writing-tests (gated each test to a named regression).

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>
@ablaszkiewicz ablaszkiewicz self-assigned this Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Migration SQL Changes

Hey 👋, we've detected some migrations on this PR. Here's the SQL output for each migration, make sure they make sense:

products/error_tracking/backend/migrations/0020_errortrackingsettings_autocapture_exceptions_opt_in.py

BEGIN;
--
-- Add field autocapture_exceptions_opt_in to errortrackingsettings
--
ALTER TABLE "posthog_errortrackingsettings" ADD COLUMN "autocapture_exceptions_opt_in" boolean NULL;
COMMIT;

Last updated: 2026-07-14 12:25 UTC (5762ffe)

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Migration Risk Analysis

We've analyzed your migrations for potential risks.

Summary: 1 Safe | 0 Needs Review | 0 Blocked

✅ Safe

Brief or no lock, backwards compatible

error_tracking.0020_errortrackingsettings_autocapture_exceptions_opt_in
  └─ #1 ✅ AddField
     Adding nullable field requires brief lock
     model: errortrackingsettings, field: autocapture_exceptions_opt_in

📚 How to Deploy These Changes Safely

AddField:

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)

@trunk-io

trunk-io Bot commented Jul 14, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

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>
@ablaszkiewicz
ablaszkiewicz marked this pull request as ready for review July 14, 2026 16:32
Copilot AI review requested due to automatic review settings July 14, 2026 16:32

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 reached their quota limit.

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested review from a team, cat-ph and hpouillot July 14, 2026 16:32
@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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>
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Backend coverage — 93.0% of changed backend lines covered — 10 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ███████████████████░ 93.0% (136 / 146)

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.

@cat-ph cat-ph 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.

🚀

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>
@ablaszkiewicz
ablaszkiewicz enabled auto-merge (squash) July 14, 2026 19:22
@ablaszkiewicz
ablaszkiewicz merged commit bac5cbc into master Jul 14, 2026
179 checks passed
@ablaszkiewicz
ablaszkiewicz deleted the feat/et-autocapture-opt-in-to-settings branch July 14, 2026 19:33
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 14, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-14 20:07 UTC Run
prod-us ✅ Deployed 2026-07-14 20:31 UTC Run
prod-eu ✅ Deployed 2026-07-14 20:34 UTC Run

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.

3 participants