Repository navigation
fix: keep calls in order while beforeSend runs - #672
Conversation
There was a problem hiding this comment.
PostHog Review · Standard · reviewed ebaf918: 1 Should fix.
See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.
posthog-flutter Compliance ReportDate: 2026-10-30 15:16:58 UTC ✅ All Tests Passed!45/45 tests passed Capture Tests✅ 29/29 tests passed View Details
Feature_Flags Tests✅ 16/16 tests passed View Details
|
Web capture(), screen() and captureException() always awaited beforeSend, even with no callbacks, so an un-awaited call followed by reset() reached posthog-js after the reset and went out as the new anonymous user. Without callbacks the event now goes to posthog-js synchronously. With callbacks, the event is dropped if the distinct id changed while they ran, as Windows and Linux already do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loader snippet's get_distinct_id stub returns undefined, so an event whose beforeSend straddled the load saw null then a real id and was dropped. Compare only when an id was available at the start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
b62b923 to
c2193e1
Compare
When reset() lands while beforeSend runs, send the event with the distinct_id it was captured for rather than dropping it.
dustinbyrne
left a comment
There was a problem hiding this comment.
The ordinary reset attribution regression is fixed, but the new identity fallback also restores the cookieless sentinel after opt-in. The inline regression passes on base and fails on head for capture, screen, and exception capture. This needs correction before approval.
AI-assisted review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dustinbyrne
left a comment
There was a problem hiding this comment.
The explicit opt-in and opt-out sentinel cases are fixed. One consent-boundary case remains: reset can restore rejected default consent before the raw distinct ID becomes the sentinel, so the fallback still restores the old user onto a cookieless event. The inline regression passes on base and fails on this head.
AI-assisted review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
PostHog Review · Standard · reviewed 7785c00: 1 Should fix.
This is review round 3 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.
dustinbyrne
left a comment
There was a problem hiding this comment.
The previous implicit-consent reset case is fixed. A separate shared-consent transition still restores a stale ID, and the existing account-context issue needs an explicit preservation decision before approval.
AI-assisted review.
Consent shared from another tab can be rejected while the cached id is still the old user; opt-in then resets to a fresh id, which the pin would otherwise override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces restoring the old distinct_id on web with ordered dispatch on web and the method-channel platforms. Calls made after an un-awaited capture, screen or captureException now reach posthog-js or native after that event, so it keeps the user, super properties, groups and consent it was captured under, as in the native SDKs. Without callbacks the event is handed over synchronously; this was broken on main for every app, since beforeSend was always awaited. A beforeSend that runs longer than 10s stops holding later calls back and its event is still sent. Windows and Linux are a follow-up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
PostHog Review · Standard · reviewed 87ee4b9: 3 Must fix.
This is review round 4 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.
Routes main's new getFeatureFlag/isFeatureEnabled sendFeatureFlagEvent arguments and getAllFeatureFlags through the call order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
PostHog Review · Standard · reviewed 9929c6c: 1 Must fix.
This is review round 5 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.
A capture made while an earlier event was in flight ran its hook right away, so it saw the state from before a queued identify() or reset() while its event was sent after them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since hooks run one at a time, starting the limit when each hook started let hooks that never finish hold later calls back for 10s per queued event. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ioannisj
left a comment
There was a problem hiding this comment.
🤖 An AI agent wrote this on behalf of @ioannisj.
One doc gap: past the 10s release the late event is sent under whoever is the current user by then, and neither the beforeSend doc nor the changeset says so. Nothing else in the way, the ordering, the timeout release and the in-hook escape all line up with the tests and posthog-js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndroid 3.74.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Removes the CallOrder queue #672 brought in from main, since synchronous beforeSend makes it redundant.
💡 Motivation and Context
Flutter is the only PostHog SDK whose
beforeSendcan be async. It runs in Dart before the call reaches posthog-js (web) or the native SDK (iOS, Android, macOS). Every other SDK (posthog-js, iOS, Android, React Native, KMP) runsbeforeSendsynchronously after building the event, so an event always carries the user, super properties, groups and consent it was captured under.In Flutter,
capture(),screen()andcaptureException()always awaitedbeforeSend, even with no callbacks configured. SoPosthog().capture('logged_out'); Posthog().reset();sentlogged_outas the new anonymous user, on every platform except Windows and Linux. The same applied toidentify(),alias(),register(),group(),optIn()andoptOut()made right after an un-awaited event. It came up in review of #667.Earlier revisions of this PR restored the original
distinct_idon web. Review kept finding cases that approach couldn't cover, such as the cookieless sentinel, consent rejected in another tab, and the next user's super properties and$groupslanding on the old user's event. This revision replaces it with ordered dispatch, which removes that whole class of cases.Changes
CallOrder(lib/src/utils/call_order.dart) hands calls to the SDK in the order the app made them:beforeSend, events are now handed over in the same call.beforeSendrunning: its hook runs right away, but the event is handed over only after every earlier call, and later calls wait behind it.capture(),screen()andcaptureException(), plus web feature events, go through it. Web feature events now look up their flag at hand-over time, so areset()called earlier applies first.beforeSenddoc says later calls, getters included, wait while a callback runs.Behavior change (patch): while an async
beforeSendruns, awaited PostHog calls made after it, including getters, complete only after that event has been handed over, for at most 10 seconds.💚 How did you test it?
test/call_order_test.dart(7 tests) covers:fakeAsync).test/web_before_send_test.dart):reset,identify,alias,register,unregister,group, opt-in and opt-out each wait for events whosebeforeSendis still running;capture→reset→identify→register→group);test/posthog_flutter_io_test.dart): ordering with and withoutbeforeSend.CallOrderon purpose, and the tests caught each one.posthog_flutter_web_handler_test.dart, which fail locally becauseFLUTTER_BUILD_NAMEis empty in my environment.flutter analyzeanddart formatare clean.Follow-ups
beforeSendsynchronous: feat!: make beforeSend callbacks synchronous #686 (stacked on chore!: Flutter Major Release v6 #637). Once the hook can't await, events are handed over in the same call on every platform, Windows and Linux included, andCallOrdercan be deleted.optOut(),reset()andidentify()wait for it. Browser autocapture and session replay don't go throughCallOrder, so for that time they keep collecting under the previous consent and identity. With synchronous hooks there's nothing to wait for, and these calls apply immediately, as in the native SDKs.enable()/disable()tooptIn()/optOut(), and this PR routes web'senable()/disable()throughCallOrder. When chore!: Flutter Major Release v6 #637 rebases ontomainafter this merges, keep the new names and route them through_call. If the syncbeforeSendPR is in by then, deleteCallOrderinstead.before-send-hooksays hooks run synchronously and never reorder events. Flutter's async hooks never matched that, and the v6 change brings them in line.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Claude Code switched the approach after the fourth review round kept finding holes in restoring the old
distinct_id. Before this revision, it checked that every other PostHog SDK runsbeforeSendsynchronously after building the event. Two independent review passes ran on the new diff. The first found a feature-flag lookup that ran too early and a throwing-hook ordering gap, both fixed here with tests. The second found nothing further in the code. ReviewHog's next round found that a hook could run before calls queued ahead of its event; since c21c4a3, hooks run one at a time in call order, as in native.🤖 Generated with Claude Code