Skip to content

feat: add composable to determine if user is eligible for nightly survey(s)#8189

Merged
christian-byrne merged 16 commits intomainfrom
feat/survey-eligibility
Jan 28, 2026
Merged

feat: add composable to determine if user is eligible for nightly survey(s)#8189
christian-byrne merged 16 commits intomainfrom
feat/survey-eligibility

Conversation

@christian-byrne
Copy link
Contributor

@christian-byrne christian-byrne commented Jan 20, 2026

Summary

Adds useSurveyEligibility composable that determines whether a user should see a survey based on multiple criteria: nightly localhost build only (isNightly && !isCloud && !isDesktop), configurable usage threshold (default 3), 14-day global cooldown between any surveys, once-per-feature-ever display, optional percentage-based sampling, and user opt-out support. All state persists to localStorage. Includes extensive unit tests covering all eligibility conditions.

See:

┆Issue is synchronized with this Notion page by Unito

@christian-byrne christian-byrne requested a review from a team as a code owner January 20, 2026 21:37
@dosubot dosubot bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Jan 20, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

Adds a new composable useSurveyEligibility that computes per-user survey eligibility (env gating, usage threshold, cooldown, opt-out/seen state, configurable delay) with localStorage persistence, and a comprehensive test suite validating scenarios and persistence.

Changes

Cohort / File(s) Summary
Survey eligibility implementation
src/platform/surveys/useSurveyEligibility.ts
New composable useSurveyEligibility(config) and exported FeatureSurveyConfig; resolves config, integrates useFeatureUsageTracker, computes reactive flags (isEligible, hasReachedThreshold, hasSeenSurvey, isInGlobalCooldown, hasOptedOut, delayMs), exposes actions (markSurveyShown, optOut, resetState), and persists per-user SurveyState to localStorage.
Tests for eligibility logic
src/platform/surveys/useSurveyEligibility.test.ts
New comprehensive test suite that mocks environments (nightly/cloud/desktop), simulates feature usage and localStorage state, verifies threshold gating, delayMs default/override, global cooldown behavior and expiry, opt-out/seen handling, persistence loading, and actions (markSurveyShown, optOut, resetState).
Manifest / package metadata
manifest_file, package.json
Metadata changes accompanying the feature and tests (lines added/removed reported in PR metadata).

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Hook as useSurveyEligibility
  participant Tracker as useFeatureUsageTracker
  participant Storage as LocalStorage

  Client->>Hook: initialize(config)
  Hook->>Storage: read SurveyState(key)
  Hook->>Tracker: subscribe(featureId)
  Tracker-->>Hook: usage updates
  Hook->>Hook: evaluate env, threshold, cooldown, opt-out
  Hook-->>Client: expose reactive flags & actions

  Client->>Hook: markSurveyShown()
  Hook->>Storage: persist seenAt / lastShown

  Client->>Hook: optOut()
  Hook->>Storage: persist optedOut

  Client->>Hook: resetState()
  Hook->>Storage: clear state
Loading

Possibly related PRs

Suggested reviewers

  • KarryCharon
  • shinshin86
  • Yorha4D
✨ Finishing touches
  • 📝 Generate docstrings

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.

@github-actions
Copy link

github-actions bot commented Jan 20, 2026

🎭 Playwright Tests: ✅ Passed

Results: 507 passed, 0 failed, 0 flaky, 8 skipped (Total: 515)

📊 Browser Reports
  • chromium: View Report (✅ 495 / ❌ 0 / ⚠️ 0 / ⏭️ 8)
  • chromium-2x: View Report (✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • chromium-0.5x: View Report (✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • mobile-chrome: View Report (✅ 9 / ❌ 0 / ⚠️ 0 / ⏭️ 0)

@github-actions
Copy link

github-actions bot commented Jan 20, 2026

🎨 Storybook Build Status

Build completed successfully!

⏰ Completed at: 01/28/2026, 04:14:08 AM UTC

🔗 Links


🎉 Your Storybook is ready for review!

@github-actions
Copy link

github-actions bot commented Jan 20, 2026

Bundle Size Report

Summary

  • Raw size: 22.1 MB baseline 22.1 MB — 🟢 -202 B
  • Gzip: 4.59 MB baseline 4.59 MB — 🟢 -66 B
  • Brotli: 3.41 MB baseline 3.41 MB — 🟢 -134 B
  • Bundles: 173 current • 173 baseline • 82 added / 82 removed

Category Glance
Other 🟢 -195 B (7.04 MB) · Panels & Settings 🟢 -8 B (470 kB) · Data & Services 🔴 +1 B (2.7 MB) · Vendor & Third-Party ⚪ 0 B (10.7 MB) · Graph Workspace ⚪ 0 B (961 kB) · Views & Navigation ⚪ 0 B (80.7 kB) · + 5 more

Per-category breakdown
App Entry Points — 22.9 kB (baseline 22.9 kB) • ⚪ 0 B

Main entry bundles and manifests

File Before After Δ Raw Δ Gzip Δ Brotli
assets/index-9PH9sTgs.js (new) 22.9 kB 🔴 +22.9 kB 🔴 +6.89 kB 🔴 +6.06 kB
assets/index-aEE-Pn2U.js (removed) 22.9 kB 🟢 -22.9 kB 🟢 -6.9 kB 🟢 -6.06 kB

Status: 1 added / 1 removed

Graph Workspace — 961 kB (baseline 961 kB) • ⚪ 0 B

Graph editor runtime, canvas, workflow orchestration

File Before After Δ Raw Δ Gzip Δ Brotli
assets/GraphView-4cCwGr7S.js (removed) 961 kB 🟢 -961 kB 🟢 -194 kB 🟢 -147 kB
assets/GraphView-8WrVDvCX.js (new) 961 kB 🔴 +961 kB 🔴 +194 kB 🔴 +147 kB

Status: 1 added / 1 removed

Views & Navigation — 80.7 kB (baseline 80.7 kB) • ⚪ 0 B

Top-level views, pages, and routed surfaces

File Before After Δ Raw Δ Gzip Δ Brotli
assets/CloudSurveyView-B8yM1go4.js (removed) 17.1 kB 🟢 -17.1 kB 🟢 -3.6 kB 🟢 -3.05 kB
assets/CloudSurveyView-DM9u-yZN.js (new) 17.1 kB 🔴 +17.1 kB 🔴 +3.6 kB 🔴 +3.05 kB
assets/CloudLoginView-CCGQeJlA.js (removed) 11.8 kB 🟢 -11.8 kB 🟢 -3.09 kB 🟢 -2.72 kB
assets/CloudLoginView-r7Z-J4ZZ.js (new) 11.8 kB 🔴 +11.8 kB 🔴 +3.09 kB 🔴 +2.72 kB
assets/UserCheckView-Bd0QNPhd.js (new) 10.5 kB 🔴 +10.5 kB 🔴 +2.44 kB 🔴 +2.13 kB
assets/UserCheckView-fuAbXqEz.js (removed) 10.5 kB 🟢 -10.5 kB 🟢 -2.44 kB 🟢 -2.13 kB
assets/CloudLayoutView-O_MavfO4.js (removed) 8.54 kB 🟢 -8.54 kB 🟢 -2.24 kB 🟢 -1.95 kB
assets/CloudLayoutView-TBTiKFaQ.js (new) 8.54 kB 🔴 +8.54 kB 🔴 +2.24 kB 🔴 +1.96 kB
assets/CloudSignupView-Dz2fqT58.js (removed) 8.18 kB 🟢 -8.18 kB 🟢 -2.33 kB 🟢 -2.02 kB
assets/CloudSignupView-hbwvI7HV.js (new) 8.18 kB 🔴 +8.18 kB 🔴 +2.33 kB 🔴 +2.02 kB
assets/CloudForgotPasswordView-BUZqXWsG.js (removed) 6.26 kB 🟢 -6.26 kB 🟢 -1.93 kB 🟢 -1.69 kB
assets/CloudForgotPasswordView-Dp6wLfu8.js (new) 6.26 kB 🔴 +6.26 kB 🔴 +1.92 kB 🔴 +1.69 kB
assets/UserSelectView-B_RACjjS.js (removed) 5.28 kB 🟢 -5.28 kB 🟢 -1.76 kB 🟢 -1.57 kB
assets/UserSelectView-C5sGjeDB.js (new) 5.28 kB 🔴 +5.28 kB 🔴 +1.76 kB 🔴 +1.57 kB
assets/CloudSubscriptionRedirectView-DmQvCJc7.js (new) 5.27 kB 🔴 +5.27 kB 🔴 +1.73 kB 🔴 +1.54 kB
assets/CloudSubscriptionRedirectView-dRlAuWEl.js (removed) 5.27 kB 🟢 -5.27 kB 🟢 -1.73 kB 🟢 -1.54 kB
assets/CloudAuthTimeoutView-B-JMwpDU.js (removed) 5.24 kB 🟢 -5.24 kB 🟢 -1.7 kB 🟢 -1.48 kB
assets/CloudAuthTimeoutView-CWrw_-eS.js (new) 5.24 kB 🔴 +5.24 kB 🔴 +1.7 kB 🔴 +1.49 kB
assets/CloudSorryContactSupportView-Db6AhoR8.js 1.97 kB 1.97 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/layout-C3IYFApg.js 500 B 500 B ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 9 added / 9 removed

Panels & Settings — 470 kB (baseline 470 kB) • 🟢 -8 B

Configuration panels, inspectors, and settings screens

File Before After Δ Raw Δ Gzip Δ Brotli
assets/WorkspacePanel-CmUQ51T7.js (removed) 29.8 kB 🟢 -29.8 kB 🟢 -5.89 kB 🟢 -5.14 kB
assets/WorkspacePanel-DRVSt7HI.js (new) 29.8 kB 🔴 +29.8 kB 🔴 +5.89 kB 🔴 +5.14 kB
assets/LegacyCreditsPanel-CFuilpnR.js (removed) 23.8 kB 🟢 -23.8 kB 🟢 -5.94 kB 🟢 -5.22 kB
assets/LegacyCreditsPanel-npJK09xh.js (new) 23.8 kB 🔴 +23.8 kB 🔴 +5.95 kB 🔴 +5.22 kB
assets/SubscriptionPanel-BgD9ifaY.js (removed) 21 kB 🟢 -21 kB 🟢 -5.02 kB 🟢 -4.44 kB
assets/SubscriptionPanel-BQdzdH5u.js (new) 21 kB 🔴 +21 kB 🔴 +5.02 kB 🔴 +4.42 kB
assets/KeybindingPanel-BlCy_k35.js (new) 14.2 kB 🔴 +14.2 kB 🔴 +3.74 kB 🔴 +3.31 kB
assets/KeybindingPanel-CW3Z28K3.js (removed) 14.2 kB 🟢 -14.2 kB 🟢 -3.74 kB 🟢 -3.31 kB
assets/AboutPanel-BHfOCm4R.js (new) 10.8 kB 🔴 +10.8 kB 🔴 +2.68 kB 🔴 +2.43 kB
assets/AboutPanel-Du9-nLPk.js (removed) 10.8 kB 🟢 -10.8 kB 🟢 -2.68 kB 🟢 -2.43 kB
assets/ExtensionPanel-B7MaOXH0.js (removed) 10.2 kB 🟢 -10.2 kB 🟢 -2.71 kB 🟢 -2.4 kB
assets/ExtensionPanel-CCeHQNt6.js (new) 10.2 kB 🔴 +10.2 kB 🔴 +2.71 kB 🔴 +2.4 kB
assets/ServerConfigPanel-CygMYdj5.js (removed) 7.23 kB 🟢 -7.23 kB 🟢 -2.17 kB 🟢 -1.94 kB
assets/ServerConfigPanel-ORIVyjkd.js (new) 7.23 kB 🔴 +7.23 kB 🔴 +2.17 kB 🔴 +1.94 kB
assets/UserPanel-CccDam4t.js (new) 6.58 kB 🔴 +6.58 kB 🔴 +1.9 kB 🔴 +1.67 kB
assets/UserPanel-Ccyxwq82.js (removed) 6.58 kB 🟢 -6.58 kB 🟢 -1.9 kB 🟢 -1.67 kB
assets/config-ghbmY1AJ.js (removed) 1.16 kB 🟢 -1.16 kB 🟢 -610 B 🟢 -541 B
assets/config-B8K_mh4W.js (new) 1.15 kB 🔴 +1.15 kB 🔴 +605 B 🔴 +532 B
assets/refreshRemoteConfig-Bielerqd.js (new) 1.14 kB 🔴 +1.14 kB 🔴 +521 B 🔴 +453 B
assets/refreshRemoteConfig-DH379O-w.js (removed) 1.14 kB 🟢 -1.14 kB 🟢 -522 B 🟢 -452 B
assets/cloudRemoteConfig-BIhLkJvj.js (removed) 1.11 kB 🟢 -1.11 kB 🟢 -509 B 🟢 -441 B
assets/cloudRemoteConfig-DKZdK4SR.js (new) 1.11 kB 🔴 +1.11 kB 🔴 +506 B 🔴 +434 B
assets/refreshRemoteConfig-NbtqAOIe.js (new) 169 B 🔴 +169 B 🔴 +108 B 🔴 +102 B
assets/refreshRemoteConfig-uD1jKrwi.js (removed) 169 B 🟢 -169 B 🟢 -108 B 🟢 -111 B
assets/remoteConfig-DlUK-xIk.js 536 B 536 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-2UNjEj6k.js 32.9 kB 32.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-B2OMGvh7.js 31.2 kB 31.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-BcujOfpn.js 29.6 kB 29.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-BI09_t23.js 29.4 kB 29.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-BKamuseh.js 25.8 kB 25.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-BlTun9tZ.js 26.4 kB 26.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-CZ62uO3e.js 30.2 kB 30.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-DaK-NByz.js 35.2 kB 35.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-DaS3cSXp.js 39.4 kB 39.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-DWbMuaAa.js 32 kB 32 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-S7pA60Hj.js 30.4 kB 30.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 12 added / 12 removed

User & Accounts — 3.94 kB (baseline 3.94 kB) • ⚪ 0 B

Authentication, profile, and account management bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/auth-D_o5ioie.js (removed) 3.54 kB 🟢 -3.54 kB 🟢 -1.24 kB 🟢 -1.05 kB
assets/auth-DFeEqv64.js (new) 3.54 kB 🔴 +3.54 kB 🔴 +1.24 kB 🔴 +1.06 kB
assets/firebaseAuthStore-1BzmV-u1.js (new) 217 B 🔴 +217 B 🔴 +136 B 🔴 +116 B
assets/firebaseAuthStore-BNBz0E0Q.js (removed) 217 B 🟢 -217 B 🟢 -136 B 🟢 -117 B
assets/auth-DLOtH-Ex.js (new) 178 B 🔴 +178 B 🔴 +142 B 🔴 +129 B
assets/auth-Dq6BzZo1.js (removed) 178 B 🟢 -178 B 🟢 -140 B 🟢 -129 B

Status: 3 added / 3 removed

Editors & Dialogs — 2.86 kB (baseline 2.86 kB) • ⚪ 0 B

Modals, dialogs, drawers, and in-app editors

File Before After Δ Raw Δ Gzip Δ Brotli
assets/useSubscriptionDialog-89QsFpMg.js (removed) 2.68 kB 🟢 -2.68 kB 🟢 -1.26 kB 🟢 -1.13 kB
assets/useSubscriptionDialog-CV77tvyi.js (new) 2.68 kB 🔴 +2.68 kB 🔴 +1.26 kB 🔴 +1.13 kB
assets/useSubscriptionDialog-5gyXt30R.js (new) 179 B 🔴 +179 B 🔴 +110 B 🔴 +94 B
assets/useSubscriptionDialog-CcqqpLzM.js (removed) 179 B 🟢 -179 B 🟢 -110 B 🟢 -103 B

Status: 2 added / 2 removed

UI Components — 33.7 kB (baseline 33.7 kB) • ⚪ 0 B

Reusable component library chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/ComfyQueueButton-B1QS9NmH.js (new) 9.52 kB 🔴 +9.52 kB 🔴 +2.69 kB 🔴 +2.42 kB
assets/ComfyQueueButton-Chjh2uTk.js (removed) 9.52 kB 🟢 -9.52 kB 🟢 -2.69 kB 🟢 -2.42 kB
assets/SubscribeButton-C3Devr1A.js (new) 4.63 kB 🔴 +4.63 kB 🔴 +1.56 kB 🔴 +1.39 kB
assets/SubscribeButton-Vl1ApDHW.js (removed) 4.63 kB 🟢 -4.63 kB 🟢 -1.56 kB 🟢 -1.39 kB
assets/cloudFeedbackTopbarButton-BD4rZULl.js (new) 1.24 kB 🔴 +1.24 kB 🔴 +675 B 🔴 +573 B
assets/cloudFeedbackTopbarButton-CDgatCi3.js (removed) 1.24 kB 🟢 -1.24 kB 🟢 -674 B 🟢 -574 B
assets/ComfyQueueButton-DitWSzPn.js (new) 181 B 🔴 +181 B 🔴 +118 B 🔴 +122 B
assets/ComfyQueueButton-WjDiC3bi.js (removed) 181 B 🟢 -181 B 🟢 -118 B 🟢 -112 B
assets/Button-iHso5Wzi.js 3.82 kB 3.82 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/CloudBadge-W_53dSio.js 1.85 kB 1.85 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/TopbarBadge-BISB98D1.js 8.36 kB 8.36 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/UserAvatar-CndbQENT.js 1.73 kB 1.73 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetButton-B6DswKC4.js 2.41 kB 2.41 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 4 added / 4 removed

Data & Services — 2.7 MB (baseline 2.7 MB) • 🔴 +1 B

Stores, services, APIs, and repositories

File Before After Δ Raw Δ Gzip Δ Brotli
assets/dialogService-BJyI1TtR.js (new) 2 MB 🔴 +2 MB 🔴 +424 kB 🔴 +324 kB
assets/dialogService-CMKgHL2U.js (removed) 2 MB 🟢 -2 MB 🟢 -424 kB 🟢 -323 kB
assets/api-BTTIHMWM.js (new) 672 kB 🔴 +672 kB 🔴 +148 kB 🔴 +118 kB
assets/api-BxpyfNHD.js (removed) 672 kB 🟢 -672 kB 🟢 -148 kB 🟢 -118 kB
assets/releaseStore-BKG421Xd.js (removed) 8.91 kB 🟢 -8.91 kB 🟢 -2.4 kB 🟢 -2.12 kB
assets/releaseStore-DDJaSFJR.js (new) 8.91 kB 🔴 +8.91 kB 🔴 +2.4 kB 🔴 +2.12 kB
assets/keybindingService-Cyj0LaOH.js (new) 6.78 kB 🔴 +6.78 kB 🔴 +1.74 kB 🔴 +1.52 kB
assets/keybindingService-DR9LDfFf.js (removed) 6.78 kB 🟢 -6.78 kB 🟢 -1.74 kB 🟢 -1.52 kB
assets/bootstrapStore-C0V8H1JY.js (new) 2.69 kB 🔴 +2.69 kB 🔴 +1.03 kB 🔴 +950 B
assets/bootstrapStore-vaFLnteM.js (removed) 2.69 kB 🟢 -2.69 kB 🟢 -1.03 kB 🟢 -957 B
assets/userStore-C4wAtLXi.js (new) 2.16 kB 🔴 +2.16 kB 🔴 +811 B 🔴 +726 B
assets/userStore-haHGH15q.js (removed) 2.16 kB 🟢 -2.16 kB 🟢 -812 B 🟢 -723 B
assets/audioService-CIws7PFa.js (removed) 2.03 kB 🟢 -2.03 kB 🟢 -930 B 🟢 -810 B
assets/audioService-Dc9acY4X.js (new) 2.03 kB 🔴 +2.03 kB 🔴 +930 B 🔴 +810 B
assets/teamWorkspaceStore-Cbwo4xAf.js (new) 165 B 🔴 +165 B 🔴 +123 B 🔴 +106 B
assets/teamWorkspaceStore-D16H_UXL.js (removed) 165 B 🟢 -165 B 🟢 -123 B 🟢 -108 B
assets/releaseStore-DHDrFLms.js (new) 140 B 🔴 +140 B 🔴 +106 B 🔴 +106 B
assets/releaseStore-nD0YooGd.js (removed) 140 B 🟢 -140 B 🟢 -106 B 🟢 -106 B
assets/serverConfigStore-rKMIA1W-.js 2.64 kB 2.64 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 9 added / 9 removed

Utilities & Hooks — 25.2 kB (baseline 25.2 kB) • ⚪ 0 B

Helpers, composables, and utility bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/useErrorHandling-C0_62-ou.js (new) 5.12 kB 🔴 +5.12 kB 🔴 +1.51 kB 🔴 +1.32 kB
assets/useErrorHandling-ClsEFEk3.js (removed) 5.12 kB 🟢 -5.12 kB 🟢 -1.51 kB 🟢 -1.32 kB
assets/useWorkspaceUI-B8FH4m4M.js (removed) 3.42 kB 🟢 -3.42 kB 🟢 -974 B 🟢 -841 B
assets/useWorkspaceUI-BRRpoRho.js (new) 3.42 kB 🔴 +3.42 kB 🔴 +973 B 🔴 +842 B
assets/useSubscriptionActions-BVT7ugOz.js (new) 2.22 kB 🔴 +2.22 kB 🔴 +870 B 🔴 +762 B
assets/useSubscriptionActions-dCxwnUCv.js (removed) 2.22 kB 🟢 -2.22 kB 🟢 -867 B 🟢 -762 B
assets/subscriptionCheckoutUtil-CmXLSdfp.js (new) 2.03 kB 🔴 +2.03 kB 🔴 +871 B 🔴 +761 B
assets/subscriptionCheckoutUtil-DYgNq7UG.js (removed) 2.03 kB 🟢 -2.03 kB 🟢 -874 B 🟢 -770 B
assets/useSubscriptionCredits-BLshXdb8.js (new) 1.39 kB 🔴 +1.39 kB 🔴 +597 B 🔴 +533 B
assets/useSubscriptionCredits-kgMTqosi.js (removed) 1.39 kB 🟢 -1.39 kB 🟢 -597 B 🟢 -528 B
assets/audioUtils-BRB0NCNE.js (removed) 970 B 🟢 -970 B 🟢 -547 B 🟢 -484 B
assets/audioUtils-CT8VqdiW.js (new) 970 B 🔴 +970 B 🔴 +548 B 🔴 +457 B
assets/useCurrentUser-Bd2pwgeO.js (removed) 145 B 🟢 -145 B 🟢 -114 B 🟢 -98 B
assets/useCurrentUser-Dnsnjo95.js (new) 145 B 🔴 +145 B 🔴 +114 B 🔴 +99 B
assets/_plugin-vue_export-helper-DuK_Fly3.js 467 B 467 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/colorUtil-D9PHPF1v.js 7.2 kB 7.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/markdownRendererUtil-BaRQfvQB.js 1.78 kB 1.78 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/tailwindUtil-BZpXL7of.js 488 B 488 B ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 7 added / 7 removed

Vendor & Third-Party — 10.7 MB (baseline 10.7 MB) • ⚪ 0 B

External libraries and shared vendor chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/vendor-chart-BD4LLnJB.js 408 kB 408 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-other-CqVFsPgr.js 4.1 MB 4.1 MB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-primevue-bkmKwsUf.js 3.04 MB 3.04 MB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-reka-ui-DCUHeuwM.js 256 kB 256 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-three-BeIVXtul.js 1.83 MB 1.83 MB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-tiptap-DdP0ccEc.js 650 kB 650 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-vue-BiXzLAAm.js 13.6 kB 13.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-xterm-Ce2gRtuj.js 398 kB 398 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
Other — 7.04 MB (baseline 7.04 MB) • 🟢 -195 B

Bundles that do not match a named category

File Before After Δ Raw Δ Gzip Δ Brotli
assets/core-Byp-5_X-.js (removed) 180 kB 🟢 -180 kB 🟢 -43.3 kB 🟢 -36.2 kB
assets/core-BRLAwnNR.js (new) 180 kB 🔴 +180 kB 🔴 +43.3 kB 🔴 +36.2 kB
assets/WidgetSelect-DccDeIpU.js (removed) 51 kB 🟢 -51 kB 🟢 -11.2 kB 🟢 -9.8 kB
assets/WidgetSelect-FVost0iS.js (new) 51 kB 🔴 +51 kB 🔴 +11.2 kB 🔴 +9.81 kB
assets/Load3DControls-Bjgy4JNJ.js (removed) 35.9 kB 🟢 -35.9 kB 🟢 -5.87 kB 🟢 -5.08 kB
assets/Load3DControls-Dmt1baeP.js (new) 35.9 kB 🔴 +35.9 kB 🔴 +5.87 kB 🔴 +5.08 kB
assets/SubscriptionRequiredDialogContent-DN-bSLI4.js (new) 28.7 kB 🔴 +28.7 kB 🔴 +6.78 kB 🔴 +5.92 kB
assets/SubscriptionRequiredDialogContent-DX2rClvJ.js (removed) 28.7 kB 🟢 -28.7 kB 🟢 -6.79 kB 🟢 -5.91 kB
assets/CurrentUserPopoverWorkspace-D4VJxFLj.js (new) 22.2 kB 🔴 +22.2 kB 🔴 +4.99 kB 🔴 +4.43 kB
assets/CurrentUserPopoverWorkspace-Dy5N0t6V.js (removed) 22.2 kB 🟢 -22.2 kB 🟢 -4.99 kB 🟢 -4.43 kB
assets/Load3D-DfjCD0Zt.js (removed) 20.9 kB 🟢 -20.9 kB 🟢 -4.58 kB 🟢 -4.01 kB
assets/Load3D-JmUmK3V3.js (new) 20.9 kB 🔴 +20.9 kB 🔴 +4.58 kB 🔴 +4.01 kB
assets/WidgetRecordAudio-B6lYY8aB.js (removed) 18.3 kB 🟢 -18.3 kB 🟢 -4.97 kB 🟢 -4.44 kB
assets/WidgetRecordAudio-BlXxBj6K.js (new) 18.3 kB 🔴 +18.3 kB 🔴 +4.97 kB 🔴 +4.44 kB
assets/WidgetInputNumber-BOBFtkxI.js (removed) 18.3 kB 🟢 -18.3 kB 🟢 -4.51 kB 🟢 -4.01 kB
assets/WidgetInputNumber-euEThmqj.js (new) 18.3 kB 🔴 +18.3 kB 🔴 +4.51 kB 🔴 +4.02 kB
assets/SubscriptionPanelContentWorkspace-B3kK4mBp.js (removed) 18.2 kB 🟢 -18.2 kB 🟢 -4.47 kB 🟢 -3.89 kB
assets/SubscriptionPanelContentWorkspace-CDIkbTIB.js (new) 18.2 kB 🔴 +18.2 kB 🔴 +4.47 kB 🔴 +3.89 kB
assets/WidgetImageCrop-DccWChT1.js (removed) 17.1 kB 🟢 -17.1 kB 🟢 -4.14 kB 🟢 -3.63 kB
assets/WidgetImageCrop-oHuwxA9a.js (new) 17.1 kB 🔴 +17.1 kB 🔴 +4.14 kB 🔴 +3.62 kB
assets/PanelTemplate-Bus9WkdK.js (new) 16.2 kB 🔴 +16.2 kB 🔴 +5.45 kB 🔴 +4.8 kB
assets/PanelTemplate-CwtxqMka.js (removed) 16.2 kB 🟢 -16.2 kB 🟢 -5.45 kB 🟢 -4.8 kB
assets/AudioPreviewPlayer-BPVJ6RjA.js (new) 10.8 kB 🔴 +10.8 kB 🔴 +2.97 kB 🔴 +2.65 kB
assets/AudioPreviewPlayer-D4uPDL4H.js (removed) 10.8 kB 🟢 -10.8 kB 🟢 -2.97 kB 🟢 -2.66 kB
assets/InviteMemberDialogContent-Be9QwLZM.js (removed) 8.36 kB 🟢 -8.36 kB 🟢 -2.51 kB 🟢 -2.16 kB
assets/InviteMemberDialogContent-CACBZytM.js (new) 8.36 kB 🔴 +8.36 kB 🔴 +2.5 kB 🔴 +2.17 kB
assets/WidgetWithControl-B6hkx6OX.js (new) 8.04 kB 🔴 +8.04 kB 🔴 +2.66 kB 🔴 +2.39 kB
assets/WidgetWithControl-DU_5Zz4K.js (removed) 8.04 kB 🟢 -8.04 kB 🟢 -2.66 kB 🟢 -2.39 kB
assets/CreateWorkspaceDialogContent-CFHVNJqH.js (removed) 5.93 kB 🟢 -5.93 kB 🟢 -1.93 kB 🟢 -1.68 kB
assets/CreateWorkspaceDialogContent-CGEcicsU.js (new) 5.93 kB 🔴 +5.93 kB 🔴 +1.93 kB 🔴 +1.68 kB
assets/EditWorkspaceDialogContent-BxRn3Eno.js (new) 5.7 kB 🔴 +5.7 kB 🔴 +1.88 kB 🔴 +1.64 kB
assets/EditWorkspaceDialogContent-U8zxfLM6.js (removed) 5.7 kB 🟢 -5.7 kB 🟢 -1.88 kB 🟢 -1.64 kB
assets/ValueControlPopover-CohIsDA5.js (removed) 5.17 kB 🟢 -5.17 kB 🟢 -1.69 kB 🟢 -1.5 kB
assets/ValueControlPopover-DXjD304y.js (new) 5.17 kB 🔴 +5.17 kB 🔴 +1.69 kB 🔴 +1.5 kB
assets/DeleteWorkspaceDialogContent-BAuVG_E6.js (new) 4.59 kB 🔴 +4.59 kB 🔴 +1.56 kB 🔴 +1.35 kB
assets/DeleteWorkspaceDialogContent-Bht-E7gN.js (removed) 4.59 kB 🟢 -4.59 kB 🟢 -1.56 kB 🟢 -1.35 kB
assets/LeaveWorkspaceDialogContent-C3_ImbnL.js (new) 4.41 kB 🔴 +4.41 kB 🔴 +1.5 kB 🔴 +1.3 kB
assets/LeaveWorkspaceDialogContent-D6TQgf27.js (removed) 4.41 kB 🟢 -4.41 kB 🟢 -1.51 kB 🟢 -1.3 kB
assets/RemoveMemberDialogContent-Dn7wWiZG.js (removed) 4.38 kB 🟢 -4.38 kB 🟢 -1.45 kB 🟢 -1.27 kB
assets/RemoveMemberDialogContent-pv3rmgsf.js (new) 4.38 kB 🔴 +4.38 kB 🔴 +1.45 kB 🔴 +1.27 kB
assets/RevokeInviteDialogContent-BL2XpK7o.js (new) 4.29 kB 🔴 +4.29 kB 🔴 +1.47 kB 🔴 +1.29 kB
assets/RevokeInviteDialogContent-DLTYfCPD.js (removed) 4.29 kB 🟢 -4.29 kB 🟢 -1.47 kB 🟢 -1.29 kB
assets/GlobalToast-DA9TJCwh.js (removed) 3.05 kB 🟢 -3.05 kB 🟢 -1.1 kB 🟢 -946 B
assets/GlobalToast-PUkSXa_Y.js (new) 3.05 kB 🔴 +3.05 kB 🔴 +1.1 kB 🔴 +946 B
assets/SubscribeToRun-b2HnldmM.js (removed) 2.96 kB 🟢 -2.96 kB 🟢 -1.15 kB 🟢 -1.04 kB
assets/SubscribeToRun-ld4FaFZ5.js (new) 2.96 kB 🔴 +2.96 kB 🔴 +1.15 kB 🔴 +1.04 kB
assets/cloudSessionCookie-B-5X1_yY.js (new) 2.94 kB 🔴 +2.94 kB 🔴 +931 B 🔴 +801 B
assets/cloudSessionCookie-CBR3Lg6_.js (removed) 2.94 kB 🟢 -2.94 kB 🟢 -928 B 🟢 -805 B
assets/BaseViewTemplate-BE4Pp4cc.js (removed) 2.42 kB 🟢 -2.42 kB 🟢 -1.04 kB 🟢 -938 B
assets/BaseViewTemplate-DPNXqT0X.js (new) 2.42 kB 🔴 +2.42 kB 🔴 +1.04 kB 🔴 +944 B
assets/CloudRunButtonWrapper-Ccgcu0wp.js (new) 1.79 kB 🔴 +1.79 kB 🔴 +644 B 🔴 +563 B
assets/CloudRunButtonWrapper-DQjqgVcB.js (removed) 1.79 kB 🟢 -1.79 kB 🟢 -642 B 🟢 -560 B
assets/cloudBadges-C4VsiYwO.js (new) 1.08 kB 🔴 +1.08 kB 🔴 +537 B 🔴 +479 B
assets/cloudBadges-IDqLaJFv.js (removed) 1.08 kB 🟢 -1.08 kB 🟢 -537 B 🟢 -499 B
assets/graphHasMissingNodes-CMIewLI5.js (removed) 1.06 kB 🟢 -1.06 kB 🟢 -461 B 🟢 -416 B
assets/graphHasMissingNodes-D-sGdy13.js (new) 1.06 kB 🔴 +1.06 kB 🔴 +461 B 🔴 +420 B
assets/cloudSubscription-CKU2xmc6.js (removed) 976 B 🟢 -976 B 🟢 -460 B 🟢 -398 B
assets/cloudSubscription-rF5rSzMY.js (new) 976 B 🔴 +976 B 🔴 +462 B 🔴 +398 B
assets/nightlyBadges-BDXWqkG4.js (removed) 595 B 🟢 -595 B 🟢 -358 B 🟢 -309 B
assets/nightlyBadges-DYS922DE.js (new) 595 B 🔴 +595 B 🔴 +357 B 🔴 +309 B
assets/SubscriptionPanelContentWorkspace-CtQEU5K8.js (new) 266 B 🔴 +266 B 🔴 +136 B 🔴 +120 B
assets/SubscriptionPanelContentWorkspace-sOt1H5Pg.js (removed) 266 B 🟢 -266 B 🟢 -136 B 🟢 -117 B
assets/WidgetInputNumber-CS1h6TSd.js (new) 186 B 🔴 +186 B 🔴 +119 B 🔴 +108 B
assets/WidgetInputNumber-D5M3InzM.js (removed) 186 B 🟢 -186 B 🟢 -119 B 🟢 -111 B
assets/WidgetLegacy-D9BMai6R.js (removed) 164 B 🟢 -164 B 🟢 -125 B 🟢 -109 B
assets/WidgetLegacy-Dgna3JyQ.js (new) 164 B 🔴 +164 B 🔴 +125 B 🔴 +103 B
assets/Load3D-9l_u-4xY.js (new) 131 B 🔴 +131 B 🔴 +107 B 🔴 +106 B
assets/Load3D-CzMIlzs7.js (removed) 131 B 🟢 -131 B 🟢 -107 B 🟢 -106 B
assets/auto-RHVQpwL9.js 1.73 kB 1.73 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-BEw5ErI4.js 18.5 kB 18.5 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-BGeHkplA.js 17.9 kB 17.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-BV0l36Iz.js 17.2 kB 17.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-C_Y3D6Cn.js 17.8 kB 17.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-C6piRza5.js 19.3 kB 19.3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-Cf8Zq1td.js 18.8 kB 18.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-CiziP3Xs.js 18 kB 18 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-D1595tOr.js 19.3 kB 19.3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-DXauvccL.js 20.6 kB 20.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-P5QCEfZc.js 18 kB 18 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-WbYP_D61.js 17 kB 17 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/i18n-DRm-ok3j.js 496 kB 496 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/i18n-tJosGSDW.js 188 B 188 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/LazyImage-D0QBx17q.js 14.1 kB 14.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-_Vi60AGa.js 125 kB 125 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-3xQXroMq.js 112 kB 112 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-B-fN60cC.js 129 kB 129 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-DcPG2fUX.js 132 kB 132 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-DFa_s6l5.js 111 kB 111 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-Do-oawDh.js 150 kB 150 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-DpM2tcEq.js 143 kB 143 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-DQY0Poqn.js 128 kB 128 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-DVGct6t3.js 172 kB 172 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-Fbu5slCi.js 125 kB 125 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-mdvCJJpq.js 154 kB 154 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/Media3DTop-CUbzOuTJ.js 2.38 kB 2.38 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/MediaAudioTop-BYIydMOt.js 2 kB 2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/MediaImageTop-sHKcENPZ.js 2.34 kB 2.34 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/MediaVideoTop-WHbgZ4CI.js 2.82 kB 2.82 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/mixpanel.module-Dary_meB.js 143 B 143 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-B0UaQKt6.js 363 kB 363 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-B68z80AD.js 413 kB 413 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-Bg1UEeRw.js 448 kB 448 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-CicFSATk.js 339 kB 339 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-CinCueZ7.js 373 kB 373 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-DgzjkU5p.js 366 kB 366 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-DKHHBXVW.js 369 kB 369 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-Dn1Haq99.js 342 kB 342 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-DxR-7ogK.js 383 kB 383 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-qCfy7PeV.js 412 kB 412 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-xDY-sSQw.js 369 kB 369 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/OBJLoader2WorkerModule-DTMpvldF.js 109 kB 109 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/previousFullPath-CmezY7As.js 838 B 838 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/rolldown-runtime-cVp-94Rc.js 1.96 kB 1.96 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/Slider-DOV35UxE.js 4.21 kB 4.21 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/widget-BJiJuR5i.js 518 B 518 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetBoundingBox-BTNRImOa.js 186 B 186 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetBoundingBox-DClY9LmH.js 4.71 kB 4.71 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetChart-c8c4iM6B.js 2.79 kB 2.79 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetColorPicker-BS_tCS3h.js 3.71 kB 3.71 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetGalleria-DmU86G89.js 4.57 kB 4.57 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetImageCompare-DsYV0YHu.js 3.79 kB 3.79 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetInputText-DXduTo3L.js 2.58 kB 2.58 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetLayoutField-CZ7gBw6n.js 2.61 kB 2.61 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetMarkdown-reAHF2_w.js 3.22 kB 3.22 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/widgetPropFilter-ERx8czR8.js 1.31 kB 1.31 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetTextarea-DwEtzm2X.js 3.52 kB 3.52 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetToggleSwitch-ByONd5aY.js 3.08 kB 3.08 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 34 added / 34 removed

@github-actions
Copy link

🔧 Auto-fixes Applied

This PR has been automatically updated to fix linting and formatting issues.

⚠️ Important: Your local branch is now behind. Run git pull before making additional changes to avoid conflicts.

Changes made:

  • ESLint auto-fixes
  • Prettier formatting

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/platform/surveys/useSurveyEligibility.test.ts`:
- Around line 1-5: Add a unit test in useSurveyEligibility.test.ts that seeds
USER_SAMPLING_ID_KEY with a user id known to produce the problematic hash (the
value that yields Math.abs(hash) === 2147483648), then call isUserInSample with
the same divisor used in production and assert the function returns the
expected, stable boolean (call it twice to ensure deterministic behavior); this
ensures the edge case where Math.abs(-2147483648) overflows is covered and
prevents regressions if the hash implementation changes.

In `@src/platform/surveys/useSurveyEligibility.ts`:
- Around line 131-139: The isUserInSample function can produce normalized > 1
when hash equals -2147483648; change the final hashing/normalization to produce
a uniform [0,1] by converting the 32-bit value to an unsigned integer (use >>> 0
on the computed hash) and divide by 0xffffffff (4294967295) instead of
0x7fffffff; alternatively you can short-circuit when sampleRate === 1 to always
return true. Update the logic in isUserInSample to use the unsigned conversion
and the correct divisor so normalized is always within [0,1].
- Around line 43-45: The current call to
useFeatureUsageTracker(resolvedConfig.value.featureId) captures featureId at
setup so changes to a reactive config won't recreate the tracker; update the
composable to watch resolvedConfig (or resolvedConfig.value.featureId) with
watchEffect or watch and recreate the feature tracker when featureId changes,
disposing/cleanup the previous tracker if applicable and reassigning useCount
(or the returned tracker object) so downstream logic uses the new tracker;
alternatively document that featureId must be static or accept featureId as a
separate non-reactive parameter (referencing resolvedConfig,
useFeatureUsageTracker, featureId, and useCount).

- Remove sampleRate config option, getUserSamplingId, isUserInSample
- Simplifies eligibility to ~2k nightly users without random sampling
- Addresses coderabbitai review: hash edge case bug, YAGNI for cohort size
- Document featureId must remain static after initialization
@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Jan 20, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/platform/surveys/useSurveyEligibility.ts`:
- Around line 62-65: The computed isInGlobalCooldown uses Date.now() which isn’t
reactive over time; to make the cooldown expire automatically without state
changes, replace the Date.now() call with a reactive now from useNow() (import
useNow from `@vueuse/core`) and use now.value in the computed that references
state.value.lastSurveyShown and GLOBAL_COOLDOWN_MS so the computed updates as
time progresses.


import { useFeatureUsageTracker } from './useFeatureUsageTracker'

/** @public */
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Remove this /** @public */ annotation.

}

const STORAGE_KEY = 'Comfy.SurveyState'
const GLOBAL_COOLDOWN_MS = 14 * 24 * 60 * 60 * 1000 // 14 days
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Change global cooldown to 4 days. The release cycle is short and sample size too low to be 14 days.

Comment on lines +30 to +37
function getStorageState() {
return useStorage<SurveyState>(STORAGE_KEY, {
seenSurveys: {},
lastSurveyShown: null,
optedOut: false
})
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a better way to make it testable without requiring this clunky initialization function? Would prefer to just inline useStorageState

Copy link
Contributor

Choose a reason for hiding this comment

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

Usually through module mocking.

Copy link
Contributor

Choose a reason for hiding this comment

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

Comment on lines +99 to +107
isEligible,
hasReachedThreshold,
hasSeenSurvey,
isInGlobalCooldown,
hasOptedOut,
delayMs,
markSurveyShown,
optOut,
resetState
Copy link
Contributor

Choose a reason for hiding this comment

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

A quick check: Do we need to expose all/each of these, or can we get away with just the aggregate isEligble and the actions?

const delayMs = computed(
() => resolvedConfig.value.delayMs ?? DEFAULT_DELAY_MS
)
const enabled = computed(() => resolvedConfig.value.enabled ?? true)
Copy link
Contributor

Choose a reason for hiding this comment

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

enabled is a little generic. What's enabled?


interface SurveyState {
seenSurveys: Record<string, number>
lastSurveyShown: number | null
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Can this be optional instead of nullable?
(I wouldn't mind sorting the fields either)

function markSurveyShown() {
const now = Date.now()
state.value.seenSurveys[resolvedConfig.value.featureId] = now
state.value.lastSurveyShown = now
Copy link
Contributor

Choose a reason for hiding this comment

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

Very nit: Maintaining this as a separate value instead of deriving it from the collection can lead to skew.

Comment on lines +6 to +8
const mockIsNightly = vi.hoisted(() => ({ value: true }))
const mockIsCloud = vi.hoisted(() => ({ value: false }))
const mockIsDesktop = vi.hoisted(() => ({ value: false }))
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional, but then you could just return this object instead of having to assign and read from .value for each of them.

Suggested change
const mockIsNightly = vi.hoisted(() => ({ value: true }))
const mockIsCloud = vi.hoisted(() => ({ value: false }))
const mockIsDesktop = vi.hoisted(() => ({ value: false }))
const mockData = vi.hoisted(() => ({
isNightly: true,
isCloud: false,
isDesktop: false
}))

mockIsNightly.value = false
setFeatureUsage('test-feature', 5)

const { useSurveyEligibility } = await import('./useSurveyEligibility')
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you import this at the top instead of doing a dynamic import?

DrJKL
DrJKL previously approved these changes Jan 24, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/platform/surveys/useSurveyEligibility.ts`:
- Around line 9-73: The config lacks a sampling gate: add an optional
sampleRate:number (0-1) to FeatureSurveyConfig and compute a deterministic
bucket (e.g., hash(stableUserId + resolvedConfig.value.featureId) / MAX ->
[0,1)) and expose it as a computed (e.g., sampleBucket) then include a check in
isEligible that returns false when sampleBucket >=
resolvedConfig.value.sampleRate; reference FeatureSurveyConfig to add
sampleRate, use resolvedConfig and resolvedConfig.value.featureId to build the
bucket, use a stable identity source (e.g., installation/user id) and add the
new bucket check in the isEligible computed alongside the existing gates.
- Around line 32-79: state read from useStorage is untrusted and can be
malformed; ensure normalization and immutable writes: when initializing/reading,
validate that state.value is an object with seenSurveys as a plain object,
lastSurveyShown as number|null and optedOut as boolean (falling back to {
seenSurveys: {}, lastSurveyShown: null, optedOut: false } if not), update
hasSeenSurvey to safely check (e.g., const seen = state.value?.seenSurveys ?? {}
and use seen[resolvedConfig.value.featureId]), and change markSurveyShown to
perform an immutable update (e.g., const now = Date.now(); state.value = {
...state.value, seenSurveys: { ...(state.value?.seenSurveys ?? {}),
[resolvedConfig.value.featureId]: now }, lastSurveyShown: now };) so mutations
are safe and reactive; reference useStorage, state, hasSeenSurvey,
isInGlobalCooldown, markSurveyShown and resolvedConfig/featureId.

Comment on lines +9 to +73
interface FeatureSurveyConfig {
/** Feature identifier. Must remain static after initialization. */
featureId: string
typeformId: string
triggerThreshold?: number
delayMs?: number
enabled?: boolean
}

interface SurveyState {
seenSurveys: Record<string, number>
lastSurveyShown: number | null
optedOut: boolean
}

const STORAGE_KEY = 'Comfy.SurveyState'
const GLOBAL_COOLDOWN_MS = 4 * 24 * 60 * 60 * 1000 // 4 days
const DEFAULT_THRESHOLD = 3
const DEFAULT_DELAY_MS = 5000

export function useSurveyEligibility(
config: MaybeRefOrGetter<FeatureSurveyConfig>
) {
const state = useStorage<SurveyState>(STORAGE_KEY, {
seenSurveys: {},
lastSurveyShown: null,
optedOut: false
})
const resolvedConfig = computed(() => toValue(config))

const { useCount } = useFeatureUsageTracker(resolvedConfig.value.featureId)

const threshold = computed(
() => resolvedConfig.value.triggerThreshold ?? DEFAULT_THRESHOLD
)
const delayMs = computed(
() => resolvedConfig.value.delayMs ?? DEFAULT_DELAY_MS
)
const enabled = computed(() => resolvedConfig.value.enabled ?? true)

const isNightlyLocalhost = computed(() => isNightly && !isCloud && !isDesktop)

const hasReachedThreshold = computed(() => useCount.value >= threshold.value)

const hasSeenSurvey = computed(
() => !!state.value.seenSurveys[resolvedConfig.value.featureId]
)

const isInGlobalCooldown = computed(() => {
if (!state.value.lastSurveyShown) return false
return Date.now() - state.value.lastSurveyShown < GLOBAL_COOLDOWN_MS
})

const hasOptedOut = computed(() => state.value.optedOut)

const isEligible = computed(() => {
if (!enabled.value) return false
if (!isNightlyLocalhost.value) return false
if (!hasReachedThreshold.value) return false
if (hasSeenSurvey.value) return false
if (isInGlobalCooldown.value) return false
if (hasOptedOut.value) return false

return true
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing sampling gate described in PR objectives.

I don’t see a sample-rate field or any deterministic sampling check in isEligible, so every eligible user will be shown the survey. If sampling is still required, add a sampleRate (and a stable user bucketing key) to the config and gate eligibility on it; otherwise, please update the PR objective/test expectations to reflect its removal.

🤖 Prompt for AI Agents
In `@src/platform/surveys/useSurveyEligibility.ts` around lines 9 - 73, The config
lacks a sampling gate: add an optional sampleRate:number (0-1) to
FeatureSurveyConfig and compute a deterministic bucket (e.g., hash(stableUserId
+ resolvedConfig.value.featureId) / MAX -> [0,1)) and expose it as a computed
(e.g., sampleBucket) then include a check in isEligible that returns false when
sampleBucket >= resolvedConfig.value.sampleRate; reference FeatureSurveyConfig
to add sampleRate, use resolvedConfig and resolvedConfig.value.featureId to
build the bucket, use a stable identity source (e.g., installation/user id) and
add the new bucket check in the isEligible computed alongside the existing
gates.

Comment on lines +32 to +79
const state = useStorage<SurveyState>(STORAGE_KEY, {
seenSurveys: {},
lastSurveyShown: null,
optedOut: false
})
const resolvedConfig = computed(() => toValue(config))

const { useCount } = useFeatureUsageTracker(resolvedConfig.value.featureId)

const threshold = computed(
() => resolvedConfig.value.triggerThreshold ?? DEFAULT_THRESHOLD
)
const delayMs = computed(
() => resolvedConfig.value.delayMs ?? DEFAULT_DELAY_MS
)
const enabled = computed(() => resolvedConfig.value.enabled ?? true)

const isNightlyLocalhost = computed(() => isNightly && !isCloud && !isDesktop)

const hasReachedThreshold = computed(() => useCount.value >= threshold.value)

const hasSeenSurvey = computed(
() => !!state.value.seenSurveys[resolvedConfig.value.featureId]
)

const isInGlobalCooldown = computed(() => {
if (!state.value.lastSurveyShown) return false
return Date.now() - state.value.lastSurveyShown < GLOBAL_COOLDOWN_MS
})

const hasOptedOut = computed(() => state.value.optedOut)

const isEligible = computed(() => {
if (!enabled.value) return false
if (!isNightlyLocalhost.value) return false
if (!hasReachedThreshold.value) return false
if (hasSeenSurvey.value) return false
if (isInGlobalCooldown.value) return false
if (hasOptedOut.value) return false

return true
})

function markSurveyShown() {
const now = Date.now()
state.value.seenSurveys[resolvedConfig.value.featureId] = now
state.value.lastSurveyShown = now
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize localStorage state before access to avoid crashes.

state is sourced from localStorage (untrusted) and is accessed/mutated directly; if stored data is malformed (e.g., seenSurveys is null), hasSeenSurvey/markSurveyShown can throw. Normalize the shape before use and prefer immutable writes to keep reactivity reliable.

✅ Proposed fix (normalize + immutable updates)
   const state = useStorage<SurveyState>(STORAGE_KEY, {
     seenSurveys: {},
     lastSurveyShown: null,
     optedOut: false
   })
   const resolvedConfig = computed(() => toValue(config))

+  const safeSeenSurveys = computed<Record<string, number>>(() => {
+    const seen = state.value.seenSurveys
+    return seen && typeof seen === 'object' ? seen : {}
+  })
+
+  const safeLastSurveyShown = computed(() =>
+    typeof state.value.lastSurveyShown === 'number'
+      ? state.value.lastSurveyShown
+      : null
+  )
+
   const { useCount } = useFeatureUsageTracker(resolvedConfig.value.featureId)
@@
   const hasSeenSurvey = computed(
-    () => !!state.value.seenSurveys[resolvedConfig.value.featureId]
+    () => !!safeSeenSurveys.value[resolvedConfig.value.featureId]
   )

   const isInGlobalCooldown = computed(() => {
-    if (!state.value.lastSurveyShown) return false
-    return Date.now() - state.value.lastSurveyShown < GLOBAL_COOLDOWN_MS
+    const lastShown = safeLastSurveyShown.value
+    if (lastShown === null) return false
+    return Date.now() - lastShown < GLOBAL_COOLDOWN_MS
   })
@@
   function markSurveyShown() {
     const now = Date.now()
-    state.value.seenSurveys[resolvedConfig.value.featureId] = now
-    state.value.lastSurveyShown = now
+    state.value = {
+      ...state.value,
+      seenSurveys: {
+        ...safeSeenSurveys.value,
+        [resolvedConfig.value.featureId]: now
+      },
+      lastSurveyShown: now
+    }
   }

As per coding guidelines, validate trusted sources before processing data and prefer immutability.

🤖 Prompt for AI Agents
In `@src/platform/surveys/useSurveyEligibility.ts` around lines 32 - 79, state
read from useStorage is untrusted and can be malformed; ensure normalization and
immutable writes: when initializing/reading, validate that state.value is an
object with seenSurveys as a plain object, lastSurveyShown as number|null and
optedOut as boolean (falling back to { seenSurveys: {}, lastSurveyShown: null,
optedOut: false } if not), update hasSeenSurvey to safely check (e.g., const
seen = state.value?.seenSurveys ?? {} and use
seen[resolvedConfig.value.featureId]), and change markSurveyShown to perform an
immutable update (e.g., const now = Date.now(); state.value = { ...state.value,
seenSurveys: { ...(state.value?.seenSurveys ?? {}),
[resolvedConfig.value.featureId]: now }, lastSurveyShown: now };) so mutations
are safe and reactive; reference useStorage, state, hasSeenSurvey,
isInGlobalCooldown, markSurveyShown and resolvedConfig/featureId.

DrJKL
DrJKL previously approved these changes Jan 26, 2026
- Rename 'enabled' to 'isSurveyEnabled' for clarity
- Remove lastSurveyShown field, derive from seenSurveys collection
- Make SurveyState interface fields optional and alphabetically sorted
- Reduce exposed return values to just isEligible, delayMs, and actions
- Simplify mock object in tests, use top-level import
- Update tests to verify behavior through isEligible only
@github-actions
Copy link

🔧 Auto-fixes Applied

This PR has been automatically updated to fix linting and formatting issues.

⚠️ Important: Your local branch is now behind. Run git pull before making additional changes to avoid conflicts.

Changes made:

  • ESLint auto-fixes
  • Oxfmt formatting

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/platform/surveys/useSurveyEligibility.test.ts`:
- Around line 241-245: Remove the "uses default delayMs when not specified" unit
test (or fold its expectation into an existing behavior-driven test) because it
merely asserts a constant value; locate the test named 'uses default delayMs
when not specified' in the useSurveyEligibility tests, remove the direct
default-value assertion on delayMs.value (and instead, if needed, verify that
omitting delayMs in the defaultConfig leads to the expected observable behavior
of useSurveyEligibility—e.g., eligibility timing or debounce behavior—within an
existing test that exercises useSurveyEligibility).

In `@src/platform/surveys/useSurveyEligibility.ts`:
- Around line 23-25: The GLOBAL_COOLDOWN_MS constant in useSurveyEligibility is
incorrectly set to 4 days; update GLOBAL_COOLDOWN_MS to represent 14 days (14 *
24 * 60 * 60 * 1000) so the global cooldown matches the PR objective, and then
update any tests that assert cooldown durations or eligibility timing to expect
14 days instead of 4; locate the constant by name (GLOBAL_COOLDOWN_MS) in
src/platform/surveys/useSurveyEligibility.ts and update corresponding unit tests
that reference this constant or hard-coded millisecond values.

Comment on lines +23 to +25
const STORAGE_KEY = 'Comfy.SurveyState'
const GLOBAL_COOLDOWN_MS = 4 * 24 * 60 * 60 * 1000 // 4 days
const DEFAULT_THRESHOLD = 3
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Global cooldown is 4 days, not 14.

PR objective calls for a 14‑day global cooldown, but GLOBAL_COOLDOWN_MS is set to 4 days. This changes eligibility timing and will make surveys appear much more frequently than intended. Update the constant (and adjust related tests) to 14 days.

🛠️ Proposed fix
-const GLOBAL_COOLDOWN_MS = 4 * 24 * 60 * 60 * 1000 // 4 days
+const GLOBAL_COOLDOWN_MS = 14 * 24 * 60 * 60 * 1000 // 14 days
🤖 Prompt for AI Agents
In `@src/platform/surveys/useSurveyEligibility.ts` around lines 23 - 25, The
GLOBAL_COOLDOWN_MS constant in useSurveyEligibility is incorrectly set to 4
days; update GLOBAL_COOLDOWN_MS to represent 14 days (14 * 24 * 60 * 60 * 1000)
so the global cooldown matches the PR objective, and then update any tests that
assert cooldown durations or eligibility timing to expect 14 days instead of 4;
locate the constant by name (GLOBAL_COOLDOWN_MS) in
src/platform/surveys/useSurveyEligibility.ts and update corresponding unit tests
that reference this constant or hard-coded millisecond values.

@christian-byrne christian-byrne merged commit e8b088c into main Jan 28, 2026
27 checks passed
@christian-byrne christian-byrne deleted the feat/survey-eligibility branch January 28, 2026 04:31
christian-byrne added a commit that referenced this pull request Feb 21, 2026
## Summary
Adds a centralized registry for feature survey configurations.

## Changes
- Add `surveyRegistry.ts` with `FEATURE_SURVEYS` record for survey
configs
- Add helper functions `getSurveyConfig()` and `getEnabledSurveys()`
- Export `FeatureId` type for type-safe feature references

## Part of Nightly Survey System
This is part 3 of a stacked PR chain:
1. ✅ feat/feature-usage-tracker - useFeatureUsageTracker (merged in
#8189)
2. ✅ feat/survey-eligibility - useSurveyEligibility (#8189, merged)
3. **feat/survey-config** - surveyRegistry.ts (this PR)
4. feat/survey-popover - NightlySurveyPopover.vue
5. feat/survey-integration - NightlySurveyController.vue

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8355-feat-add-survey-registry-for-feature-survey-configurations-2f66d73d365081faae6bda0c14c069d9)
by [Unito](https://www.unito.io)
christian-byrne added a commit that referenced this pull request Feb 23, 2026
## Summary

Adds NightlySurveyPopover component that displays a Typeform survey to
eligible nightly users after a configurable delay.

## Changes

- **What**: Vue component that uses `useSurveyEligibility` to show/hide
a survey popover with accept, dismiss, and opt-out actions. Loads
Typeform embed script dynamically with HTTPS and deduplication.

## Review Focus

- Typeform script injection security (HTTPS-only, load-once guard,
typeformId alphanumeric validation)
- Timeout lifecycle (clears pending timeout when eligibility changes)

## Part of Nightly Survey System

This is part 4 of a stacked PR chain:
1. ✅ feat/feature-usage-tracker - useFeatureUsageTracker (merged in
#8189)
2. ✅ feat/survey-eligibility - useSurveyEligibility (#8189, merged)
3. ✅ feat/survey-config - surveyRegistry.ts (#8355, merged)
4. **feat/survey-popover** - NightlySurveyPopover.vue (this PR)
5. feat/survey-integration - NightlySurveyController.vue (#8480)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9083-feat-add-NightlySurveyPopover-component-for-feature-surveys-30f6d73d365081d1beb2f92555a4b2f4)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants