Repository navigation
fix: wait for pending user updates before evaluating targeting [ENG-3153] - #81
Merged
mattinannt merged 2 commits intoSep 28, 2026
Conversation
…153] `setUserId()` / `setAttribute()` queue a debounced `POST /user`, but `track()` picked a survey straight away. Identify-then-track therefore rendered before the contact existed: segment-targeted surveys were dropped by the anonymous filter, and an unsegmented survey rendered with no `contactId`, so its response was recorded as anonymous. `track()` now resolves the action class, waits for the queued update to land (flushing it at once instead of sitting out the debounce), and only then evaluates. A failed or timed-out update skips surveys with segment filters rather than showing them on stale membership. Two queue bugs fixed on the way: a write during an in-flight request was wiped by the success path's reset, and a commit firing mid-request sent a second concurrent `POST /user`. Values now move out of the queue with the request, and a commit is deferred while one is airborne. Same fix as the JS SDK (#7498) and iOS (formbricks/ios#56).
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ttp to AGP 8.9-compatible versions Every CI run on main has been red since the 2026-09-23 dependabot merges. Two independent breakages: - Kotlin 2.4.20 (#69) turns `kotlinOptions { jvmTarget = "11" }` into a script compilation error, so the build never configures. - With that fixed, the test APK's AAR-metadata check rejects androidx.core 1.19.0 (#67, needs AGP >= 9.1) and okhttp 5.5.0 (#71, needs compileSdk 37) on AGP 8.9.2. Migrate the script to `kotlin { compilerOptions { ... } }`, pin core-ktx back to 1.16.0 and okhttp to 4.12.0, and have dependabot ignore newer versions of those two until the AGP 9 migration (#80) lands. appcompat, fragment and the Kotlin bump itself stay.
|
mattinannt
approved these changes
Sep 28, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Ref ENG-3153
What & why
Was:
setUserId()/setAttribute()queue a debouncedPOST /user, buttrack()picked a survey straight away. Identify-then-track on login rendered the survey before the contact existed: segment-targeted surveys were dropped (the filter still saw an anonymous user), and an unsegmented survey rendered with nocontactId, so its response was recorded as Anonymous.Now:
track()resolves the action class, then waits for the queued update to land (flushing it at once instead of sitting out the 500ms debounce), and only then picks a survey. If the update fails or times out (5s), a survey with segment filters is skipped rather than shown on stale membership; one without filters still shows.Reported in #60. Same fix as the JS SDK (formbricks/formbricks#7498) and iOS (formbricks/ios#56).
Also unbreaks
main. Every CI run since the 2026-09-23 dependabot merges has been red:kotlinOptions { jvmTarget }is a script compilation error on Kotlin 2.4.20 (#69), and the test APK's AAR-metadata check rejectsandroidx.core 1.19(#67, needs AGP ≥ 9.1) andokhttp 5.5(#71, needs compileSdk 37) on AGP 8.9.2. The second commit migrates the script tocompilerOptions, pins those two back to 1.16.0 / 4.12.0, and tells dependabot to leave them alone until the AGP 9 migration (#80) lands. appcompat 1.8, fragment 1.9 and the Kotlin bump stay.Where to look
UpdateQueue.waitForPendingWork/syncDidFinish: the wait, its two short circuits (nothing queued, anonymous user), the timeout, and the hand-back on failure.UserManager.syncUser:filterSurveys()now runs before waiters are released. That order is load-bearing.SurveyManager.track/evaluate: the split and the fail-closed branch.Two bugs this had to fix first
reset()cleared them wholesale.commit()now moves its values out; a failure hands them back.commit()had an in-flight flag but no guard, so a debounce firing mid-request sent a second concurrentPOST /user, whose response could overwritesegments/displays/responses. Now deferred and re-armed bysyncDidFinish.Coverage
setAttribute(tier=pro)+track()back-to-back shows the segment-targeted survey; display and response carry the contacttrack()with no attributes: unsegmented survey shows, attributedPOST /user, timeout fired at 5.0sUpdateQueueInstrumentedTestelse if (hasNewWork && userId == null)branch inUpdateQueue.syncDidFinishif (isSyncInFlight)guard inUpdateQueue.commitRerun:
./gradlew :android:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.formbricks.android.network.queue.UpdateQueueInstrumentedTestFull suite: 133 tests, 0 failures (Pixel 9a, API 36).
Manual QA evidence (Pixel 9a emulator, local web, a segment-targeted copy of the app survey seeded on the same
codeaction)Before (38ba734): tap at 07:18:04.4 UTC → WebView loading at +0.6s → contact created 07:18:14 →
Display07:18:20 withcontactId = null→Response07:19:49 withcontactId = null. The survey shown was the unsegmented one; the segment-targeted copy had been dropped by the anonymous filter.After: tap at 07:21:29.3 →
UpdateQueue - commit() calledin the same millisecond (flushed, no debounce) → contact created 07:21:32.1 →Display07:21:39 on the segment-targeted survey withcontactId = cmukx5lg9…→Response07:22:19 attributed. Identify without attributes: unsegmented survey shown,Displayattributed.Timeout: 8s
setTimeoutinjected into the localPOST /user, thensetAttribute(email)+track()back-to-back:No survey was shown.
Open gaps
unit (red on main)row:waitForPendingWorkandsyncDidFinish(success)do not exist on main, so the tests cannot compile against it. The red-on-main proof is the manual before run: the unsegmented survey rendered 0.6s aftertrack(), the contact was created 9s later, and both the display and the response havecontactId = null.isShowingSurveyis write-only on Android (never read or reset), so iOS's "claim the latch before the wait" has nothing to port onto. Left as is.Breaking changes
track()already returnedUnit; it now resolves asynchronously. No public signature changes.