feat: filter surveys by eligibility before triggering (ENG-1128)#11
Conversation
Port RN's filterSurveys pipeline: displayOption, recontactDays, and segment targeting stages, persisted as TConfig.filteredSurveys and recomputed at every state-change point (setup, sync, user update, display, response, close, logout, expiry). trackAction now reads the filtered set instead of raw workspace surveys, and triggerSurvey applies the per-display displayPercentage roll. Add displayOption, displayLimit, recontactDays, displayPercentage, and segment fields to TSurvey, plus a minimal TSurveySegment type. Extract tryParseSurvey into common/utils.
Add unit and widget tests for the survey filtering pipeline introduced with eligibility-based triggering: - filter_surveys: diffInDays, segment-filter detection, display/recontact limits, and full filtering scenarios - expiry_ticker: refetch recomputes filteredSurveys for anonymous and identified users, and survives concurrent config writes (Ok and Err) - setup, action, update_queue, user, survey_webview, survey type, and utils: extend fixtures with segments/displays and assert filtered results drive triggering
Add integration_test driver and scenario-based E2E coverage (baseline, displayOnce, displayLimit2, recontactDays, displayPercentage, segmentAnonymous, segmentIdentified) for ENG-1128, running against a real backend and survey runtime.
WalkthroughThis PR implements survey eligibility filtering for the Formbricks Flutter SDK (ENG-1128). It introduces a three-stage filtering algorithm ( 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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: 2
🤖 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 `@apps/playground/integration_test/survey_filtering_qa_test.dart`:
- Around line 120-353: The switch over _scenario has many case blocks
('baseline', 'displayOnce', 'displayLimit2', 'recontactDays',
'displayPercentage', 'segmentAnonymous', 'segmentIdentified') that are missing
terminating statements and cause a compile error; fix by adding an explicit
terminator (e.g., break; or return;) at the end of each case body (after the
final await/_expect/_closeSurvey calls) so control does not fall through to the
next case—apply this to the blocks handling _scenario values named above in the
switch(_scenario) block.
In `@packages/formbricks_flutter/lib/src/survey/action.dart`:
- Around line 31-41: The gating currently only runs when displayPercentage > 0
so a value of 0 bypasses the gate; update the logic around the displayPercentage
variable (used with shouldDisplayBasedOnPercentage and Logger.debug for
survey.id) to run whenever displayPercentage != null and treat 0 as a
forced-suppress: i.e., if displayPercentage == 0 OR
shouldDisplayBasedOnPercentage(displayPercentage, random: random) returns false
then log the skip and return. This ensures a configured 0% actually suppresses
the survey.
🪄 Autofix (Beta)
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
Run ID: 71afccdb-b855-4db9-ab14-4186d701f9d5
⛔ Files ignored due to path filters (1)
pubspec.lockis excluded by!**/*.lock
📒 Files selected for processing (19)
apps/playground/integration_test/survey_filtering_qa_test.dartapps/playground/pubspec.yamlpackages/formbricks_flutter/lib/src/common/expiry_ticker.dartpackages/formbricks_flutter/lib/src/common/filter_surveys.dartpackages/formbricks_flutter/lib/src/common/setup.dartpackages/formbricks_flutter/lib/src/common/utils.dartpackages/formbricks_flutter/lib/src/survey/action.dartpackages/formbricks_flutter/lib/src/types/survey.dartpackages/formbricks_flutter/lib/src/user/update_queue.dartpackages/formbricks_flutter/lib/src/widgets/survey_webview.dartpackages/formbricks_flutter/test/common/expiry_ticker_test.dartpackages/formbricks_flutter/test/common/filter_surveys_test.dartpackages/formbricks_flutter/test/common/setup_test.dartpackages/formbricks_flutter/test/common/utils_test.dartpackages/formbricks_flutter/test/survey/action_test.dartpackages/formbricks_flutter/test/types/survey_test.dartpackages/formbricks_flutter/test/user/update_queue_test.dartpackages/formbricks_flutter/test/user/user_test.dartpackages/formbricks_flutter/test/widgets/survey_webview_test.dart
|



What does this PR do?
Implements survey eligibility filtering for the Flutter SDK — a 1:1 port of the React Native SDK's
filterSurveyspipeline.Previously
trackActionshowed every cached survey whose trigger name matched. Now:filterSurveys(newfilter_surveys.dart) computes the eligible set in three stages:displayOption(respondMultiple/displayOnce/displayMultiple/displaySome+displayLimit),recontactDays(survey-level value with workspacesettings.recontactDaysfallback, compared againstlastDisplayAt), and segment targeting (anonymous sessions drop segment-filtered surveys; identified users only see surveys whosesegment.idis inuser.segments).TSurveygains the typed eligibility fields (displayOption,displayLimit,recontactDays,displayPercentage,segment— tolerant of the legacy{filters: [...]}shape). Raw JSON round-trip stays lossless.trackActionreadsconfig.filteredSurveysinstead of the raw workspace survey list, andtriggerSurveyapplies thedisplayPercentagedice-roll (plainRandom(), injectable for tests; skips are logged).filteredSurveysis recomputed and persisted at every state-change point: setup (both paths), workspace refresh (expiry ticker), successfulcreateOrUpdateUser, survey display/response/close (WebView events, matching RNsurvey-web-view.tsx), andtearDown/logout. Both ENG-1126TODO(filtering ticket)markers are gone.Intentional divergences from RN (documented inline): an unknown/missing
displayOptionexcludes only that survey instead of throwing; non-num workspacerecontactDaysis treated as unset instead of crashing the filter; the expiry ticker re-reads config after the network round-trip so concurrent writes aren't clobbered.Each touched source file ships with its test file.
Also adds
apps/playground/integration_test/survey_filtering_qa_test.dart— a manual E2E QA harness (not executed by CI;flutter testonly runstest/).How should this be tested?
Unit/widget suite (CI runs this):
make test— 272 SDK tests + playground tests green.make analyzeandmake format-check— clean.make test-sdk-coverage).E2E on a simulator against a real backend (manual, validated on iOS):
code). Segment scenarios need an enterprise license.SCENARIO=displayOnce | displayLimit2 | recontactDays | displayPercentage | segmentAnonymous | segmentIdentified, flipping the survey's dashboard settings to match (Recontact options, Cooldown Period, "Show survey to % of users", Target Audience filters).displayPercentage: 50; segment-filtered surveys are hidden from anonymous sessions, shown to a matching identified user, and dropped again on logout.Manual smoke via the playground app:
make run ios RUN_ARGS="--dart-define=APP_URL=... --dart-define=WORKSPACE_ID=..."filteredSurveysafter each display/close/identity change.Fixes ENG-1128