feat: adds user identification functionality#8
Conversation
…buttons The CI format-check (and pana) flagged 5 unformatted files, and the playground widget test still asserted the old 'not wired to the SDK yet' stub which was replaced by real SDK calls. Reformat and rewrite the test to verify the new behavior: identity buttons gated off until connected, local-storage buttons always enabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WalkthroughThis PR adds user identity and attribute state management to the Flutter SDK with debounced batch synchronization. The core contribution is an 🚥 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: 4
🤖 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/lib/main.dart`:
- Around line 151-173: The _runAction method only catches FormbricksError and
lets other exceptions escape, so add a generic fallback catch (catch (e, st) or
catch Exception) after the FormbricksError catch to convert any unexpected error
into a user-facing message and optionally log it; set message to a safe string
like '$action -> unexpected error: ${e.toString()}' (or localized text) and
ensure the snackbar is shown in the existing post-try mounted check; apply the
same pattern to the other action/storage helper(s) in this file (the block
around lines 175-204) so every path (Ok, Err(FormbricksError), and any other
exception) results in a snackbar outcome.
In `@packages/formbricks_flutter/lib/src/user/update_queue.dart`:
- Around line 137-170: The _flush() function can leave _updates set if
_sendUpdates throws an exception, causing unintended retries; wrap the await
_sendUpdates(effectiveUserId, attributes) call in a try { await
_sendUpdates(...); } finally { _updates = null; } so _updates is cleared
regardless of success or exception (rethrow the exception if you need to
preserve error propagation), ensuring the "no retry" policy is enforced; update
references in _flush() to use this try/finally around _sendUpdates.
In `@packages/formbricks_flutter/test/user/attribute_test.dart`:
- Around line 44-51: Tests are not hermetic because setAttributes/setLanguage
trigger a debounced flush that may call the real backend; in setUp, replace the
real API client on UpdateQueue.instance with a test double (e.g., a no-op or
in-memory mock) so flushes won't perform network I/O — assign the mock to the
queue/api client field used by UpdateQueue (replace the live client after
obtaining UpdateQueue.instance and setting configOverride), and keep tearDown
as-is to reset state; reference UpdateQueue.instance, configOverride,
setAttributes, setLanguage, setUp, and tearDown when making the change.
In `@packages/formbricks_flutter/test/user/user_test.dart`:
- Around line 53-87: The tests for setUserId are leaving the UpdateQueue's
network path un-stubbed causing flakiness; update the other two tests
("different value when one set → tearDown then queue new id" and "from anonymous
→ no tearDown, id queued") to set queue.apiClientOverride (or otherwise stub the
network client) on the local UpdateQueue instance used in each test (the queue
variable created via UpdateQueue.instance..configOverride = config) just like
the first test does, so the debounced flush won't make outbound calls during the
test run.
🪄 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: b8b7398a-7ac2-417d-abbb-45f1de87015a
📒 Files selected for processing (10)
apps/playground/lib/main.dartpackages/formbricks_flutter/lib/src/common/setup.dartpackages/formbricks_flutter/lib/src/user/attribute.dartpackages/formbricks_flutter/lib/src/user/update_queue.dartpackages/formbricks_flutter/lib/src/user/user.dartpackages/formbricks_flutter/lib/src/widgets/formbricks_widget.dartpackages/formbricks_flutter/test/user/attribute_test.dartpackages/formbricks_flutter/test/user/update_queue_test.dartpackages/formbricks_flutter/test/user/user_api_test.dartpackages/formbricks_flutter/test/user/user_test.dart
…h can't retry _sendUpdates can throw (null appUrl/workspaceId asserts, or a failing config.update disk write); the trailing _updates = null was skipped on throw, leaving the batch buffered to re-send on the next change. Wrap in try/finally and add a regression test (null workspaceId → throw). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setAttributes/setUserId schedule a 500ms flush; without an apiClientOverride a slow run could fire it against the real backend before tearDown cancels the timer. Inject a no-op MockClient in attribute_test setUp and in the two setUserId tests that queue a flush. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|



ENG-1126: User identification —
setUserId/setAttribute(s)/setLanguage/logout+ UpdateQueueSummary
Lands the user-identification pipeline for the Flutter SDK: the public methods
setUserId,setAttribute,setAttributes,setLanguage,logout, and theUpdateQueuebehind them — a 500 ms debounce that coalesces rapid identity/attribute changes into a singlePOST /api/v2/client/{workspaceId}/user. Surveys are no longer anonymous-only; the returnedTUserState(segments, displays, responses, expiry) is persisted into config.Direct 1:1 port of the RN SDK's
lib/user/*(update-queue.ts,user.ts,attribute.ts,update.ts) +setup.tstearDownandindex.tspublic wiring, carrying the Flutter-established conventions:Result<T,E>over thrown control flow,DateTime→ISO at one boundary, awaitedupdate()persistence, logger configured insetup().Stops at "set identity + attributes + language, debounce, persist returned state." Eligibility filtering (
filterSurveys: displayOption / recontactDays / segment targeting) is out of scope — segments are persisted but not yet consumed. Tracked in ENG-1128; two// TODO(filtering ticket)seams are left at the refilter points.What's included
SDK
lib/src/user/update_queue.dart— debounce singleton. Accumulator merge (later keys win), userId resolution (pending → config), language-without-userId → local config only (no network), attributes-without-userId →MissingFieldError+ buffer cleared,_sendUpdatespersists returned state with no retry on failure, responseerrorssuppress the success log (hasWarnings).dispose()cancels the timer for clean hot-reload/teardown.lib/src/user/user.dart—setUserId(idempotent for same value; tears down prior state when switching identity),logout.lib/src/user/attribute.dart—setAttributes(DateTime→UTC ISO-8601,num/Stringpassed through so the backend infers type),setLanguage→setAttributes({language}).lib/src/common/setup.dart—tearDownresets user state todefaultNoUserIdand persists (refilter left as a TODO).lib/src/widgets/formbricks_widget.dart— staticsetUserId/setAttribute/setAttributes/setLanguage/logout, each routed throughCommandQueuewithcheckSetup: true(matchestrackwiring). Plus adebugClearStoredConfig()helper.Playground (
apps/playground/lib/main.dart)Behavior notes / parity
CommandQueue— asetUserIdfollowed bysetAttributecan't race.unawaited(processUpdates())); a public call returnsOkonce accepted, before the network round-trip. Verified against a live workspace: rapid taps coalesce into one request ~500 ms after the last; an invalid attribute key (signupDate) returns a backend warning that correctly suppresses the success log while valid keys still persist.Testing
Every source file ships with tests covering happy path, errors, and edge cases. Debounce/coalescing tested deterministically with
fake_async.lib/src/user/update_queue.darttest/user/update_queue_test.dartlib/src/user/user.dart(+setup.darttearDown)test/user/user_test.dartlib/src/user/attribute.darttest/user/attribute_test.dartlib/src/widgets/formbricks_widget.dart(public wiring)test/user/user_api_test.dartflutter analyzeclean (package + playground).user.dart100%,attribute.dart100%,update_queue.dart96.4% (tearDownlines covered viauser_test).Out of scope / follow-ups
survey_webview.dartfrom ENG-1127, so that ticket's scope should be adjusted.)_, must start with a letter) to fail fast instead of round-tripping for the backend warning. RN relies on the backend; could be a small DX follow-up.Closes ENG-1126.