Skip to content

feat: adds user identification functionality#8

Open
pandeymangg wants to merge 6 commits into
mainfrom
feat/user-identification
Open

feat: adds user identification functionality#8
pandeymangg wants to merge 6 commits into
mainfrom
feat/user-identification

Conversation

@pandeymangg

@pandeymangg pandeymangg commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

ENG-1126: User identification — setUserId / setAttribute(s) / setLanguage / logout + UpdateQueue

Summary

Lands the user-identification pipeline for the Flutter SDK: the public methods setUserId, setAttribute, setAttributes, setLanguage, logout, and the UpdateQueue behind them — a 500 ms debounce that coalesces rapid identity/attribute changes into a single POST /api/v2/client/{workspaceId}/user. Surveys are no longer anonymous-only; the returned TUserState (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.ts tearDown and index.ts public wiring, carrying the Flutter-established conventions: Result<T,E> over thrown control flow, DateTime→ISO at one boundary, awaited update() persistence, logger configured in setup().

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, _sendUpdates persists returned state with no retry on failure, response errors suppress the success log (hasWarnings). dispose() cancels the timer for clean hot-reload/teardown.
  • lib/src/user/user.dartsetUserId (idempotent for same value; tears down prior state when switching identity), logout.
  • lib/src/user/attribute.dartsetAttributes (DateTime→UTC ISO-8601, num/String passed through so the backend infers type), setLanguagesetAttributes({language}).
  • lib/src/common/setup.darttearDown resets user state to defaultNoUserId and persists (refilter left as a TODO).
  • lib/src/widgets/formbricks_widget.dart — static setUserId / setAttribute / setAttributes / setLanguage / logout, each routed through CommandQueue with checkSetup: true (matches track wiring). Plus a debugClearStoredConfig() helper.

Playground (apps/playground/lib/main.dart)

  • Wired the five previously-stubbed identity buttons to the real API.
  • New "Local storage" section: Log Local Storage (dumps persisted JSON to console) and Clear Local Storage.

Behavior notes / parity

  • Public API stays sequential via CommandQueue — a setUserId followed by setAttribute can't race.
  • The debounce is fire-and-forget at the call site (unawaited(processUpdates())); a public call returns Ok once 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.
  • A failed batch is dropped (no retry, no partial update) — it re-sends on the next identity/attribute change.

Testing

Every source file ships with tests covering happy path, errors, and edge cases. Debounce/coalescing tested deterministically with fake_async.

Source Test
lib/src/user/update_queue.dart test/user/update_queue_test.dart
lib/src/user/user.dart (+ setup.dart tearDown) test/user/user_test.dart
lib/src/user/attribute.dart test/user/attribute_test.dart
lib/src/widgets/formbricks_widget.dart (public wiring) test/user/user_api_test.dart
  • Full suite: 197 passing. flutter analyze clean (package + playground).
  • Coverage on touched files: user.dart 100%, attribute.dart 100%, update_queue.dart 96.4% (tearDown lines covered via user_test).

Out of scope / follow-ups

  • Eligibility filtering / segment targeting → ENG-1128. (Note: local display/response tracking already exists in survey_webview.dart from ENG-1127, so that ticket's scope should be adjusted.)
  • Offline queueing of user updates — RN doesn't retry; neither do we.
  • Optional client-side attribute-key validation (lowercase + _, 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.

@pandeymangg pandeymangg requested a review from itsjavi June 9, 2026 11:43
…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>
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds user identity and attribute state management to the Flutter SDK with debounced batch synchronization. The core contribution is an UpdateQueue singleton that coalesces rapid user identity and attribute updates into a single backend call after a 500ms debounce window. The queue supports identity operations (setUserId, logout), contact attributes (setAttributes, setLanguage with DateTime normalization), and special-case language-only updates when no user ID is present. All operations are exposed through the Formbricks widget facade with setup checking, and comprehensive test suites validate debounce timing, error handling, state persistence, and API behavior. The playground app is updated with UI controls to exercise these new features and display results via snackbars.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: adds user identification functionality' directly and clearly summarizes the main change—the introduction of user identification features (setUserId, setAttribute(s), setLanguage, logout, and the UpdateQueue debouncing mechanism)—which is the primary objective of this changeset.
Description check ✅ Passed The pull request description is comprehensive and well-organized, thoroughly explaining the user identification pipeline implementation, including the UpdateQueue debouncing mechanism, public API methods, behavior notes, testing coverage, and out-of-scope items—all of which directly relate to and justify the changes in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between c0d8f12 and 94791f7.

📒 Files selected for processing (10)
  • apps/playground/lib/main.dart
  • packages/formbricks_flutter/lib/src/common/setup.dart
  • packages/formbricks_flutter/lib/src/user/attribute.dart
  • packages/formbricks_flutter/lib/src/user/update_queue.dart
  • packages/formbricks_flutter/lib/src/user/user.dart
  • packages/formbricks_flutter/lib/src/widgets/formbricks_widget.dart
  • packages/formbricks_flutter/test/user/attribute_test.dart
  • packages/formbricks_flutter/test/user/update_queue_test.dart
  • packages/formbricks_flutter/test/user/user_api_test.dart
  • packages/formbricks_flutter/test/user/user_test.dart

Comment thread apps/playground/lib/main.dart
Comment thread packages/formbricks_flutter/lib/src/user/update_queue.dart
Comment thread packages/formbricks_flutter/test/user/attribute_test.dart
Comment thread packages/formbricks_flutter/test/user/user_test.dart
pandeymangg and others added 3 commits June 9, 2026 17:50
…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>
Comment thread packages/formbricks_flutter/lib/src/user/update_queue.dart
Comment thread packages/formbricks_flutter/lib/src/user/user.dart
Comment thread packages/formbricks_flutter/lib/src/user/update_queue.dart Outdated
@sonarqubecloud

Copy link
Copy Markdown

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