Skip to content

fix: preserve chained error handlers across restarts - #624

Merged
marandaneto merged 2 commits into
mainfrom
fix/chained-error-handler-lifecycle
Sep 30, 2026
Merged

marandaneto merged 2 commits into
mainfrom
fix/chained-error-handler-lifecycle

Conversation

@marandaneto

@marandaneto marandaneto commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

💡 Motivation and Context

When another error handler wraps PostHog's handler, disabling or closing the SDK leaves that wrapper holding our old callback. Teardown clears the callback's upstream delegate but does not stop it from capturing. The original handler no longer runs, and enabling or setting up the SDK again can capture the same error twice.

Each installed Flutter and platform error callback now keeps its own upstream delegate. Retained callbacks only forward after teardown. Only the current callback can capture after a restart, which also prevents circular chains when the same integration is stopped and started. Callback identity is the only capture guard, since teardown clears the stored callback references. Platform handlers keep the upstream handled result and return false when there is no upstream handler.

This follows the wrapper lifecycle described in the consent-gating spec and the integration teardown behavior in the shutdown spec. There are no public API changes or deviations in the behavior this fix touches.

💚 How did you test it?

  • Verified all 10 regression cases fail against the pre-fix implementation for the intended reasons. All 14 lifecycle cases pass with the fix, including four baseline and restoration controls.
  • Ran the full Flutter VM suite: 738 tests passed.
  • Ran the new Flutter handler cases in Chrome: 7 tests passed.
  • Ran the CI browser test selection: 83 tests passed.
  • Ran the isolate error handler test in Chrome with Wasm: 1 test passed.
  • Ran the finite compliance adapter test selection: 25 tests passed.
  • Passed make checkFormatDart, make analyzeDart, make checkApiDart, and git diff --check.
  • Completed the diff-scoped test audit and isolated autoreview with no actionable findings. Autoreview was rerun on 9f0fc6f after removing the redundant enabled checks.
  • After the review cleanup, reran all 738 VM tests, all 7 Chrome lifecycle cases, formatting, and scoped analysis. All passed.

Native example builds were not run locally. CI covers those builds.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed. No documentation changes are needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Added a patch changeset.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Investigated and fixed with Pi, using Git, Flutter/Dart, GitHub CLI, and isolated autoreview. The work stayed in a dedicated worktree. The fix uses per-install callback delegates and callback identity rather than event deduplication, so it preserves the handler chain without changing exception processing.

@marandaneto marandaneto self-assigned this Sep 30, 2026
@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Retrigger

[Medium risk] Fixes error handler lifecycle to preserve chaining across restarts.

The PR appears safe to merge; no actionable regression was identified.

Reviews (1) · Last reviewed commit: "fix: preserve chained error handlers acr..."

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

posthog-flutter Compliance Report

Date: 2026-09-30 15:02:29 UTC
Duration: 96736ms

✅ All Tests Passed!

45/45 tests passed


Capture Tests

✅ 29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 136ms
Format Validation.Event Has Uuid ✅ 118ms
Format Validation.Event Has Lib Properties ✅ 115ms
Format Validation.Distinct Id Is String ✅ 116ms
Format Validation.Token Is Present ✅ 115ms
Format Validation.Custom Properties Preserved ✅ 117ms
Format Validation.Event Has Timestamp ✅ 116ms
Retry Behavior.Retries On 503 ✅ 5332ms
Retry Behavior.Does Not Retry On 400 ✅ 2117ms
Retry Behavior.Does Not Retry On 401 ✅ 2118ms
Retry Behavior.Respects Retry After Header ✅ 8120ms
Retry Behavior.Implements Backoff ✅ 15442ms
Retry Behavior.Retries On 500 ✅ 5225ms
Retry Behavior.Retries On 502 ✅ 5225ms
Retry Behavior.Retries On 504 ✅ 5226ms
Retry Behavior.Max Retries Respected ✅ 15445ms
Deduplication.Generates Unique Uuids ✅ 123ms
Deduplication.Preserves Uuid On Retry ✅ 5225ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10335ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5231ms
Deduplication.No Duplicate Events In Batch ✅ 123ms
Deduplication.Different Events Have Different Uuids ✅ 115ms
Compression.Sends Gzip When Enabled ✅ 117ms
Batch Format.Uses Proper Batch Structure ✅ 116ms
Batch Format.Flush With No Events Sends Nothing ✅ 108ms
Batch Format.Multiple Events Batched Together ✅ 123ms
Error Handling.Does Not Retry On 403 ✅ 2116ms
Error Handling.Does Not Retry On 413 ✅ 2117ms
Error Handling.Retries On 408 ✅ 5224ms

Feature_Flags Tests

✅ 16/16 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 12ms
Request Payload.Flags Request Uses V2 Query Param ✅ 9ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 9ms
Request Payload.Flags Request Omits Authorization Header ✅ 9ms
Request Payload.Token In Flags Body Matches Init ✅ 9ms
Request Payload.Groups Round Trip ✅ 9ms
Request Payload.Groups Default To Empty Object ✅ 9ms
Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It ✅ 8ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 9ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 9ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 8ms
Request Lifecycle.No Flags Request On Init Alone ✅ 4ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 110ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 16ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 8ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 114ms

@marandaneto
marandaneto marked this pull request as ready for review September 30, 2026 13:23
@marandaneto
marandaneto requested a review from a team as a code owner September 30, 2026 13:23
@marandaneto

Copy link
Copy Markdown
Member Author

found while reviewing #621 but the issue was already there

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

Verified locally: with main's implementation the new lifecycle file fails 10 of 14 (upstream dropped after uninstall, retained handler still capturing, double capture after reinstall, recursion on restart), and all 14 pass at da957dba. The per-install closure with its own delegate matches what Android does with its dormant pass-through, and nothing public changes.

One nit, not blocking: _isEnabled && is redundant next to the identical(...) check, since stop() nulls the field.

Approving. #629 can go in after this, and #621 will need a rebase on the same file.

@marandaneto

Copy link
Copy Markdown
Member Author

@turnipdabeets Removed the redundant _isEnabled && checks from both wrappers in 9f0fc6f. Callback identity still disables retired wrappers after teardown and restart. All 14 lifecycle cases, the full 738-test VM suite, and the 7 Chrome lifecycle cases pass. Formatting, scoped analysis, and autoreview are also clean.

@marandaneto
marandaneto enabled auto-merge (squash) September 30, 2026 14:59
@marandaneto
marandaneto merged commit 56293ae into main Sep 30, 2026
28 checks passed
@marandaneto
marandaneto deleted the fix/chained-error-handler-lifecycle branch September 30, 2026 15:14
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