Skip to content

fix: wait for pending user updates before evaluating targeting [ENG-3153] - #81

Merged
mattinannt merged 2 commits into
mainfrom
anshuman/eng-3153-survey-may-render-before-setuserid-identification-completes
Sep 28, 2026
Merged

mattinannt merged 2 commits into
mainfrom
anshuman/eng-3153-survey-may-render-before-setuserid-identification-completes

Conversation

@pandeymangg

@pandeymangg pandeymangg commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Ref ENG-3153

What & why

Was: setUserId() / setAttribute() queue a debounced POST /user, but track() 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 no contactId, 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 rejects androidx.core 1.19 (#67, needs AGP ≥ 9.1) and okhttp 5.5 (#71, needs compileSdk 37) on AGP 8.9.2. The second commit migrates the script to compilerOptions, 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
  • A write during an in-flight request was silently lost: values stayed queued during the request and the success path's 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 concurrent POST /user, whose response could overwrite segments / displays / responses. Now deferred and re-armed by syncDidFinish.

Coverage

Behaviour Level
Identify + setAttribute(tier=pro) + track() back-to-back shows the segment-targeted survey; display and response carry the contact manual — emulator against local web, evidence in the fold below
Identify + track() with no attributes: unsegmented survey shows, attributed manual
Update times out → segment-targeted survey skipped with a warning, nothing shown manual — 8s latency injected into POST /user, timeout fired at 5.0s
Wait short-circuits when nothing is queued and for an anonymous user; flushes now and coalesces; reports failure; times out; failed values ride along with the next commit unit (guard) — UpdateQueueInstrumentedTest
Mid-flight write survives and gets its own request unit (mutation) — drop the else if (hasNewWork && userId == null) branch in UpdateQueue.syncDidFinish
No second concurrent sync while one is in flight unit (mutation) — remove the if (isSyncInFlight) guard in UpdateQueue.commit

Rerun: ./gradlew :android:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.formbricks.android.network.queue.UpdateQueueInstrumentedTest

Full 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 code action)

Before (38ba734): tap at 07:18:04.4 UTC → WebView loading at +0.6s → contact created 07:18:14 → Display 07:18:20 with contactId = null → Response 07:19:49 with contactId = 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() called in the same millisecond (flushed, no debounce) → contact created 07:21:32.1 → Display 07:21:39 on the segment-targeted survey with contactId = cmukx5lg9… → Response 07:22:19 attributed. Identify without attributes: unsegmented survey shown, Display attributed.

Timeout: 8s setTimeout injected into the local POST /user, then setAttribute(email) + track() back-to-back:

12:53:17.957 D FormbricksSDK: UpdateQueue - commit() called on UpdateQueue with user-04e8a11c and {email=…}
12:53:22.960 D FormbricksSDK: UpdateQueue - a queued user update did not land within 5000ms
12:53:22.961 W FormbricksSDK: Skipping survey "eng3153segmentedtierpro1": the pending user update did not land, so segment membership is stale.

No survey was shown.

Open gaps

  • No unit (red on main) row: waitForPendingWork and syncDidFinish(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 after track(), the contact was created 9s later, and both the display and the response have contactId = null.
  • isShowingSurvey is 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

  • This PR contains a breaking change

track() already returned Unit; it now resolves asynchronously. No public signature changes.

…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).
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: b325abb6-7463-43d6-bddd-4b662bac96ce


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…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.
@sonarqubecloud

Copy link
Copy Markdown

@mattinannt
mattinannt added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit c790555 Sep 28, 2026
8 checks passed
@pandeymangg pandeymangg mentioned this pull request Oct 19, 2026
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