fix: make layout observation lifecycle thread-safe - #814
Conversation
Keep this as a reproduction-only draft. The stale-count test intentionally fails, and Thread Sanitizer detects the existing hasSwizzled race. No production fix is included.
posthog-ios Compliance ReportDate: 2026-09-11 07:26:45 UTC
|
| Test | Status | Duration |
|---|---|---|
| Format Validation.Event Has Required Fields | ✅ | 2832ms |
| Format Validation.Event Has Uuid | ✅ | 2795ms |
| Format Validation.Event Has Lib Properties | ✅ | 391ms |
| Format Validation.Distinct Id Is String | ✅ | 299ms |
| Format Validation.Token Is Present | ✅ | 2754ms |
| Format Validation.Custom Properties Preserved | ✅ | 2746ms |
| Format Validation.Event Has Timestamp | ✅ | 2710ms |
| Retry Behavior.Retries On 503 | ✅ | 9167ms |
| Retry Behavior.Does Not Retry On 400 | ✅ | 2284ms |
| Retry Behavior.Does Not Retry On 401 | ✅ | 4650ms |
| Retry Behavior.Respects Retry After Header | ✅ | 7778ms |
| Retry Behavior.Implements Backoff | ✅ | 15403ms |
| Retry Behavior.Retries On 500 | ✅ | 9362ms |
| Retry Behavior.Retries On 502 | ✅ | 9235ms |
| Retry Behavior.Retries On 504 | ✅ | 9179ms |
| Retry Behavior.Max Retries Respected | ❌ | 15294ms |
| Deduplication.Generates Unique Uuids | ✅ | 2974ms |
| Deduplication.Preserves Uuid On Retry | ✅ | 8818ms |
| Deduplication.Preserves Uuid And Timestamp On Retry | ✅ | 16712ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ✅ | 7844ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 2901ms |
| Deduplication.Different Events Have Different Uuids | ✅ | 2833ms |
| Compression.Sends Gzip When Enabled | ✅ | 2733ms |
| Batch Format.Uses Proper Batch Structure | ✅ | 2666ms |
| Batch Format.Flush With No Events Sends Nothing | ✅ | 230ms |
| Batch Format.Multiple Events Batched Together | ✅ | 2854ms |
| Error Handling.Does Not Retry On 403 | ✅ | 4800ms |
| Error Handling.Does Not Retry On 413 | ✅ | 4820ms |
| Error Handling.Retries On 408 | ✅ | 8853ms |
Failures
retry_behavior.max_retries_respected
Expected 4 requests, got 5
Feature_Flags Tests
✅ 16/16 tests passed
View Details
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ✅ | 2745ms |
| Request Payload.Flags Request Uses V2 Query Param | ✅ | 2831ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ✅ | 2833ms |
| Request Payload.Flags Request Omits Authorization Header | ✅ | 2792ms |
| Request Payload.Token In Flags Body Matches Init | ✅ | 2837ms |
| Request Payload.Groups Round Trip | ✅ | 2713ms |
| Request Payload.Groups Default To Empty Object | ✅ | 2691ms |
| Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It | ✅ | 2721ms |
| Request Payload.Disable Geoip False Propagates As Geoip Disable False | ✅ | 2787ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ✅ | 2873ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ✅ | 2836ms |
| Request Lifecycle.No Flags Request On Init Alone | ✅ | 86ms |
| Request Lifecycle.No Flags Request On Normal Capture | ✅ | 457ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ✅ | 5467ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ✅ | 2766ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ✅ | 3139ms |
|
@ioannisj or @turnipdabeets can you help me testing this out? its very specific and we dont want to break anything, burnt quite some time figuring this out already |
|
Reviews (1): Last reviewed commit: "fix: bound subscriber count notification..." | Re-trigger Greptile |
|
Live session replay smoke test passed on an iPhone 17 Pro simulator (iOS 26.5), using commit
Replay: https://us.posthog.com/shared/ivb2F5GoUwu1-l4WxOuE4a4E1jVsLg?t=11 This validates the tested simulator flows, not every replay scenario or the production cause of #806. Web-player playback was not independently verified during the smoke test. Temporary example configuration/instrumentation was removed, the baseline app restored, and the worktree left clean. No project token was committed. |
…-layout-lifecycle # Conflicts: # PostHog/ApplicationViewLayoutPublisher.swift # PostHogTests/ApplicationViewLayoutPublisherTest.swift
|
Did a pre-pr and PR repro as well and could not find anything that this changes could break. Extra locking was marked as insignificant and nothing above simulator noise. I'll run through the code as well but lgtm |
💡 Motivation and Context
Converts this reproduction-only PR into a production lifecycle fix. The reproductions are now passing regression tests; no intentionally failing tests remain.
Related to #806, but these confirmed lifecycle defects do not establish the cause of the customer's production
NSISEnginecrash. The diagnostic warning from merged #815 is preserved in the new hook. The run-loop proposal in #813 remains separate work. This preserves synchronous UIKit forwarding on its original calling thread; it does not suppress background layout.Changes
An older library restoring UIKit can remove PostHog from the chain. Recovery now handles that sequence on the next subscriber-count notification rather than trusting stale installation state. It does not continuously monitor runtime replacement or coordinate arbitrary concurrent third-party writes. Forwarding implementations retained by other libraries must remain valid.
💚 How did you test it?
Reproduction-first validation confirmed stale count delivery, a
hasSwizzledTSan race, multiple callback objects from concurrent first access, stack-overflowing forwarding after unsubscribe, and clobbering a newer swizzler before the fixes. A retained-old-hook regression also caught duplicate notifications during implementation.make test6f31b314c: 799 Swift Testing tests, plus XCTestmake testOniOSSimulator6f31b314c: 193 XCTest tests and 926 Swift Testing testsmake buildmake format,make lint,git diff --checkmake buildFull build stops at
ExternalSDK-iOS: the example expects package identityposthog-ios, while the isolated checkout is namedposthog-ios-issue-806-lifecycle. Subsequent CocoaPods examples were not reached. No unrelated build configuration was changed.Autoreview command:
$HOME/.pi/agent/skills/autoreview/scripts/autoreview --mode branch --base origin/main --prompt-file autoreview-scope.md. One test-ordering finding was rejected after checking the actual code: this test uses only zero-interval trailing subscriptions, whoseinvokeTrailingdirectly enqueues on main before the continuation barrier. It does not traverse the background leading-throttle queue. The bundle-only reviewer could not see that unchanged implementation. No accepted review blockers remain.The bounded-delivery follow-up (
5826de1b0) passed isolated autoreview with no findings:$HOME/.pi/agent/skills/autoreview/scripts/autoreview --mode commit --commit HEAD --prompt-file autoreview-bounded-scope.md. Its tests first failed against the unbounded loop, then passed along with the existing lifecycle tests under Thread Sanitizer across three repetitions/relaunches.Latest main merge validation (
a4be3e52c)All 47 CI checks passed for this exact commit, including SDK/example builds, tests, replay masking snapshots, and compliance. The combined warning/lifecycle tests passed locally under TSan across three repetitions, and the full iOS suite passed (190 XCTest tests and 921 Swift Testing tests). Format and lint passed. Local macOS full-suite attempts had varying failures in unchanged feature-flag/event/queue timing tests; the queue test passed in isolation, and the full macOS CI job passed. No tests were weakened or disabled. The existing local external-client package-identity build limitation remains; its CI build passed.
Current head (
6f31b314c)Recovery fix
7e043c190addresses the clarified review sequence. Both detach sequences first failed with zero notifications. The retained-subscriber variants also failed on the partial zero-only fix before runtime reconciliation was added. No intentionally failing tests remain.Merged latest
main(05bbc8d6e, including v3.73.0) without conflicts in6f31b314c. The table above reflects this head: full SPM/iOS suites passed, combined layout and bounded/reentrant tests passed under TSan across three repetitions/relaunches, and format/lint passed. The local external-client build limitation is unchanged. CI is running for this exact head.Final autoreview:
$HOME/.pi/agent/skills/autoreview/scripts/autoreview --mode branch --base origin/main --prompt-file autoreview-final-lifecycle-scope.md. Clean, no actionable findings.📝 Checklist
If releasing new changes
.changeset/stable-layout-lifecycle.md.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Manoel requested implementing the lifecycle fixes in this existing PR worktree and pushing to this PR. Pi used reproduction-first tests, file edits, make-based validation, Thread Sanitizer, GitHub CLI, and isolated Pi autoreview. Changes remain within layout-hook lifecycle and throttled subscriber-count delivery; no run-loop architecture or public API changes are included. Human review is required.