feat: support survey-interaction segment filters (ENG-1275) - #73
Conversation
Move the "<" escaping into a `const optionsJson` before the template and use `replaceAll` + `String.raw` instead of `replace(/</g, "<")`. Clears three SonarQube smells introduced by the fix: - S7781 (prefer replaceAll over replace with a global regex) - S7780 (prefer String.raw over a string literal with an escaped backslash) - the same S7780 on the outer template literal, whose raw text previously contained "<" via the inline explanatory comment (now moved out). Behaviour is unchanged: String.raw`<` yields the same 6-char escape, so survey content still can't break out of the inline <script>. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports the client half of the web SDK change for interaction-based segment
filters ("have seen X", "have completed X", ...). Membership for those filters
is computed server-side and can flip the moment a contact interacts with a
survey, so the SDK now refetches user state instead of waiting for it to expire.
- Add the per-survey `interactionRefresh` gate to `TSurvey`.
- Add `refreshSegmentsAfterInteraction`, mirroring js-core: no-op for anonymous
users, no-op unless the server flagged that survey and event, otherwise nudge
the UpdateQueue so a display -> response -> finish burst debounces into one
request.
- Bridge `onFinished` from the WebView. The prop and the zod flag already
existed but nothing ever fired or read them, so "have completed X" had no
client-side trigger.
- Wire the refresh into all three lifecycle branches of the WebView's onMessage
handler, after the local optimistic config update.
`renderHtml` is exported so the harness can be asserted in tests; it is not
re-exported from the package entry point. `__DEV__` is stubbed in the vitest
setup because React Native injects it at build time and modules that branch on
it fail to import otherwise.
WalkthroughThe change adds optional survey interaction refresh configuration for display, response, and completion events. Enabled refreshes queue identified-user updates through 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/react-native/src/lib/user/interaction-refresh.ts`:
- Around line 39-41: Handle the promise returned by UpdateQueue.processUpdates()
in the interaction refresh flow by attaching rejection handling, while
preserving the queue’s existing failure logging and avoiding duplicate error
reporting.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 322d414d-2120-470e-95f4-8acba59b32a1
📒 Files selected for processing (6)
packages/react-native/src/components/survey-web-view.tsxpackages/react-native/src/components/tests/survey-web-view-harness.test.tspackages/react-native/src/lib/user/interaction-refresh.tspackages/react-native/src/lib/user/tests/interaction-refresh.test.tspackages/react-native/src/types/survey.tspackages/react-native/vitest.setup.ts
`processUpdates()` rejects when the flush throws, and the refresh calls it
fire-and-forget — a bare `void` left that as an unhandled promise rejection,
which React Native surfaces loudly. Attach a `.catch`; the queue already logs the
real cause, so swallowing here avoids reporting it twice.
The test asserts `.catch` is actually attached rather than watching
`process.on("unhandledRejection")`. Node only reports an unhandled rejection on a
later tick, so the listener approach passed with and without the fix — it looked
like coverage without pinning anything.
Also re-establish the mock's resolved value in the local `beforeEach`: the shared
vitest setup calls `resetAllMocks`, which strips implementations, so
`processUpdates()` was returning undefined.
|



What & why
The web app now supports survey-interaction segment filters — targeting contacts by whether they have seen / have not seen / have started responding to / have completed / have not completed a survey within a time window (formbricks#8588).
Membership for those filters is computed server-side, and it can flip the moment a contact interacts with a survey.
js-corereacts by refetching user state right away; this SDK had no equivalent, so it kept using the segment list it received at app launch. A rule like "completed survey A → show survey B" would not fire in the same session.Of the four SDKs this had to be ported to, this one was the closest fit —
UpdateQueuealready exposesupdateUserIdandprocessUpdatesexactly like js-core's, so the js-core recipe transfers as-is.What changed
The gate —
interactionRefreshonTSurveyAbsent for workspaces that don't use interaction targeting, and present-but-all-false for surveys no interaction filter references — both handled by
?? false.The refresh —
lib/user/interaction-refresh.tsMirrors js-core's
refreshSegmentsAfterInteraction. Gated twice, because a/usersync is not cheap:Routed through
UpdateQueueso a display → response → finish burst debounces into a single request.onFinishedbridged from the WebViewTwo thirds of this were already in place and unused:
onFinishedis a long-standing prop of the surveys library, andZJsRNWebViewOnMessageDataalready declared the flag. What was missing was the harness shim that posts it and the handler that reads it — sohaveCompleted/haveNotCompletedhad no client-side trigger.Because the harness passes
getSetIsResponseSendingFinished,isResponseSendingFinishedstartsfalse, soonFinishedgenuinely means the finished response was accepted by the backend.Wired into
onMessageAll three lifecycle branches now call the refresh after the local optimistic config update, matching js-core's ordering.
Verification
168 tests, 0 failuresacross 19 files.tsc --noEmitandbiome checkboth clean.13 new tests: 9 covering the gate (anonymous, absent, all-false, mismatched source, each matching source, partial object, and that it routes through the queue rather than sending directly) and 4 covering the harness.
Confirmed with mutation testing — each of these makes a test go red:
onFinishedfrom thesurveyPropsobjectNotes for reviewers
renderHtmlis now exported. Only so the harness can be asserted in tests — it is not re-exported fromsrc/index.ts, so it stays package-internal. Without it there is no way to check thatonFinishedis actually handed torenderSurvey, which is the mutation the harness test catches.__DEV__is stubbed invitest.setup.ts. React Native injects it at build time, so any test importing a module that branches on it fails at import.falseis the right value for tests and it is a shared setup file, so this benefits future component tests too.addUserStateExpiryCheckListenerextendsexpiresAtevery 60s rather than refetching — same as js-core. That is by design, and it is exactly why the interaction refresh matters: nothing else pulls fresh segments mid-session.onResponseCreatedfires optimistically from the surveys library, before the response-create POST completes, so an interaction-driven/userrefresh can land before the row is committed and the server's array wins. js-core has the same characteristic, so this matches it rather than diverging. The real fix is upstream — movingonResponseCreatedontoResponseQueue's confirmed hook — which fixes every platform at once.