Skip to content

fix(cloud): stop bouncing working users to /cloud/survey mid-session#12301

Merged
deepme987 merged 7 commits into
mainfrom
deep/fix-survey-gate-false-positives
May 18, 2026
Merged

fix(cloud): stop bouncing working users to /cloud/survey mid-session#12301
deepme987 merged 7 commits into
mainfrom
deep/fix-survey-gate-false-positives

Conversation

@deepme987
Copy link
Copy Markdown
Contributor

@deepme987 deepme987 commented May 15, 2026

Summary

getSurveyCompletedStatus (auth.ts) now resolves ambiguous responses to "completed" instead of "not completed", so transient backend errors no longer bounce working users to /cloud/survey.

Backend response Old behavior New behavior
200 with non-empty value true (completed) true (completed)
200 with empty value false (not completed) false (not completed)
404 false → bounced true (treat as completed)
5xx false → bounced true (treat as completed)
401 / 403 false → bounced true (treat as completed)
Network error false → bounced true (treat as completed)

Only a definitive 200 with empty value is treated as "not completed". Everything else fails open. The dedicated auth layer handles re-authentication on the next API call, so 401/403 doesn't need a separate branch here.

Why

User reports from team-plan customers: "I was working in a workflow, hit run, and then got logged out and redirected to a survey screen." Datadog shows ~7,000 distinct users/day hitting the setting key onboarding_survey not found path on prod ingest. With onboarding_survey_enabled: true in prod dynamic config and the catch-all !response.ok returning false, any mid-session reload tripped a redirect to /cloud/survey.

User-validated requirement: rather miss showing the survey to a few users than show it duplicately or interrupt working customers.

Trade-off worth product review

A genuinely brand-new user whose User.Settings JSON is empty also returns 404 from /api/settings/onboarding_survey — the backend doesn't distinguish "key absent for existing user" from "user has no settings yet". With this change, that 404 is treated as "completed", so the survey gate does not fire on the strict 404 path. New users will still see the survey if signup pre-populates the onboarding_survey key with an empty object (200 with empty value); if not, the survey is missed on initial signup.

We picked this trade-off per the product call that false positives (bouncing paying customers) are strictly worse than false negatives (occasionally missing a new user).

The clean fix to recover the new-user signal is a backend change: return 200 with value: null when the User row exists but the key is absent — distinguishing "no survey saved" from "user not found". Out of scope for this PR; filing as follow-up if accepted.

Test plan

  • Logged-in user with completed survey navigates around — no redirect
  • Logged-in user with no survey, fresh tab — redirected to /cloud/survey (gate still works for new sessions)
  • Logged-in user with no survey, after submitting — no redirect on next nav
  • Simulate transient 5xx on /api/settings/onboarding_survey (DevTools blocking) — user stays on current page, no redirect

Unit coverage in auth.test.ts locks the resolution table above against drift (one test per branch, 8 total).

Companion PRs

None — frontend only.

┆Issue is synchronized with this Notion page by Unito

## What

Two coupled changes to the onboarding-survey gate so that customers who are
already in their workflow do not get yanked to /cloud/survey by transient
errors or background reloads.

1. `getSurveyCompletedStatus` (auth.ts) now resolves ambiguous responses to
   "completed":
   - 200 with non-empty `value` → true  (definitely completed)
   - 200 with empty `value`     → false (definitely not completed)
   - 404 / 5xx / network        → true  (ambiguous → safer default)
   - 401 / 403                  → throws (auth issue, not a survey signal)

   Previously every non-2xx response collapsed to `false`, meaning any
   transient 5xx, network blip, or token-refresh race during the
   `/api/settings/onboarding_survey` call redirected the user to the survey.
   The new resolution preserves the genuine "user has never saved a survey"
   signal (only fires on a 200 with an empty value) while refusing to treat
   a 5xx as a survey-state signal.

2. The router's survey gate (router.ts beforeEach) is now evaluated at most
   once per browser-tab session, using sessionStorage. sessionStorage
   persists across `window.location.reload()` in the same tab, so any
   background reload — token refresh recovery, GraphCanvas's 401 path, the
   cloud-remote-config 10-minute poll — can no longer re-bounce a working
   user. A new tab still gets a fresh check (matches the intent of showing
   the survey to first-time and existing-but-never-prompted users).

   The catch branch is also fixed: previously a thrown auth error bounced
   to /cloud-user-check, which calls the same endpoints and can loop. Now
   we mark the session as evaluated and let the user proceed; the auth
   layer handles re-auth on the next API call.

## Why

User reports from team-plan customers: "I was working in a workflow, hit
run, and then got logged out and redirected to a survey screen." Datadog
shows ~7,000 distinct users/day hit the survey-not-found 404 path on prod
ingest. The flag `onboarding_survey_enabled` is true in prod dynamic
config; combined with the catch-all `!response.ok` returning `false`, any
mid-session reload trips a redirect to /cloud/survey.

The user's stated requirement: rather miss showing the survey to a few
users than show it duplicately or interrupt working customers.

## Trade-off (worth Robin's review)

A genuinely brand-new user whose User.Settings JSON is empty returns 404
from `/api/settings/onboarding_survey` (the backend doesn't distinguish
"key absent" from "user not found"). With this change, that 404 is
treated as "completed", so the survey gate does not fire for them on the
strict 404 path. New users will still see the survey on the empty-200
path if signup pre-populates the Settings key with an empty object; if
not, the survey is missed on initial signup. Acceptable per product
requirement that false positives are worse than false negatives.

If we want to recover that signal cleanly, the proper fix is a backend
change to return 200 with `value: null` when the User row exists but the
key is absent — distinguishing "no survey saved" from "user not found".
Out of scope for this PR.

## Companion: Sentry breadcrumbs

Both files emit Sentry breadcrumbs at decision points (gate redirect,
ambiguous-response fallback) with `treated_as` and `initial_load` flags
so the next incident report can be diagnosed from session replay rather
than speculation.

## Test plan

- [ ] Logged-in user with completed survey navigates around — no redirect
- [ ] Logged-in user with no survey, fresh tab → redirected to /cloud/survey
- [ ] Logged-in user with no survey, after submitting → no redirect on next nav
- [ ] Simulate transient 5xx on `/api/settings/onboarding_survey` (DevTools
      throttling / blocking) → user stays on current page, no redirect
- [ ] Trigger `window.location.reload()` mid-session → no redirect to survey
- [ ] Sentry breadcrumbs show up on test events
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Updates getSurveyCompletedStatus() to treat non-OK HTTP responses and network errors as survey-completed (return true), adds an inline comment documenting this policy, and adds Vitest tests covering OK, empty, error-status, and network-failure paths.

Changes

Survey Completion Status Error-Handling Policy

Layer / File(s) Summary
Survey status logic and error-handling alignment
src/platform/cloud/onboarding/auth.ts
Adds inline documentation clarifying that ambiguous/non-OK responses should be treated as completed. Non-OK responses now return true, completion is computed only for OK responses via isEmpty(data.value), and the catch-block default return value changes from false to true.
Test suite for getSurveyCompletedStatus
src/platform/cloud/onboarding/auth.test.ts
Introduces Vitest mocks for fetchApi and Sentry, a mockResponse helper for test Response objects, and test cases validating completion for successful responses with non-empty values, false for empty/null payloads, true for non-auth error statuses (404, 500, 401, 403), and true when fetchApi rejects.

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested labels

size:S

Suggested reviewers

  • benceruleanlu

🐰 A rabbit hops through the survey gate,
No more false bounces—embrace a kinder fate,
Non-OK responses? They mean "done!"
Network hiccups won't spoil the fun,
Tests confirm: the policy's spun! 🎉


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore (reviewers only)

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
End-To-End Regression Coverage For Fixes ❌ Error PR title uses "fix" keyword and changes frontend code (src/platform/cloud/onboarding/) without browser_tests/ coverage. No explanation in PR description for omitting E2E test. Add Playwright regression test under browser_tests/ verifying survey gate behavior prevents mid-session bounces, OR document in PR description why E2E test is impractical.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main change: preventing users from being redirected to /cloud/survey during their work sessions due to transient errors or background reloads.
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.
Adr Compliance For Entity/Litegraph Changes ✅ Passed Check not applicable. Modified files (src/platform/cloud/onboarding/) are not under src/lib/litegraph/, src/ecs/, or graph entity paths. ADR compliance check only applies to those specific paths.
Description check ✅ Passed The PR description is comprehensive, following the template structure with Summary, Changes (implicit through the comparison table), Review Focus, and detailed context including rationale, trade-offs, test plan, and companion PR information.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch deep/fix-survey-gate-false-positives

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

🎭 Playwright: ✅ 1611 passed, 0 failed · 3 flaky

📊 Browser Reports
  • chromium: View Report (✅ 1590 / ❌ 0 / ⚠️ 3 / ⏭️ 5)
  • chromium-2x: View Report (✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • chromium-0.5x: View Report (✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • mobile-chrome: View Report (✅ 18 / ❌ 0 / ⚠️ 0 / ⏭️ 0)

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 05/18/2026, 10:01:59 PM UTC

Links

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

📦 Bundle: 5.38 MB gzip 🔴 +69 B

Details

Summary

  • Raw size: 24.8 MB baseline 24.8 MB — 🔴 +1 B
  • Gzip: 5.38 MB baseline 5.38 MB — 🔴 +69 B
  • Brotli: 4.16 MB baseline 4.16 MB — 🟢 -50 B
  • Bundles: 268 current • 268 baseline • 118 added / 118 removed

Category Glance
User & Accounts 🔴 +1 B (17.8 kB) · Vendor & Third-Party ⚪ 0 B (9.94 MB) · Other ⚪ 0 B (9.23 MB) · Data & Services ⚪ 0 B (3.16 MB) · Graph Workspace ⚪ 0 B (1.24 MB) · Panels & Settings ⚪ 0 B (527 kB) · + 5 more

App Entry Points — 26.1 kB (baseline 26.1 kB) • ⚪ 0 B

Main entry bundles and manifests

File Before After Δ Raw Δ Gzip Δ Brotli
assets/index-CPEauTAV.js (removed) 26.1 kB 🟢 -26.1 kB 🟢 -8.75 kB 🟢 -7.57 kB
assets/index-N0zGXMCS.js (new) 26.1 kB 🔴 +26.1 kB 🔴 +8.75 kB 🔴 +7.53 kB

Status: 1 added / 1 removed

Graph Workspace — 1.24 MB (baseline 1.24 MB) • ⚪ 0 B

Graph editor runtime, canvas, workflow orchestration

File Before After Δ Raw Δ Gzip Δ Brotli
assets/GraphView-C1a9-Ott.js (new) 1.24 MB 🔴 +1.24 MB 🔴 +264 kB 🔴 +199 kB
assets/GraphView-Cel7NaNd.js (removed) 1.24 MB 🟢 -1.24 MB 🟢 -264 kB 🟢 -199 kB

Status: 1 added / 1 removed

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

Top-level views, pages, and routed surfaces

File Before After Δ Raw Δ Gzip Δ Brotli
assets/CloudSurveyView-BDUNpyWl.js (new) 19.6 kB 🔴 +19.6 kB 🔴 +5.14 kB 🔴 +4.56 kB
assets/CloudSurveyView-BxidrKh0.js (removed) 19.6 kB 🟢 -19.6 kB 🟢 -5.14 kB 🟢 -4.58 kB
assets/CloudLoginView-B9xJJAPq.js (new) 12.5 kB 🔴 +12.5 kB 🔴 +3.55 kB 🔴 +3.14 kB
assets/CloudLoginView-CmJwj92e.js (removed) 12.5 kB 🟢 -12.5 kB 🟢 -3.55 kB 🟢 -3.13 kB
assets/CloudSignupView-Bh2_AFvU.js (new) 10.4 kB 🔴 +10.4 kB 🔴 +3.06 kB 🔴 +2.7 kB
assets/CloudSignupView-kAZn3Ziu.js (removed) 10.4 kB 🟢 -10.4 kB 🟢 -3.06 kB 🟢 -2.71 kB
assets/UserCheckView-g2dJYk1x.js (removed) 9.07 kB 🟢 -9.07 kB 🟢 -2.34 kB 🟢 -2.05 kB
assets/UserCheckView-vTEhqdAM.js (new) 9.07 kB 🔴 +9.07 kB 🔴 +2.34 kB 🔴 +2.05 kB
assets/CloudLayoutView-89fCxhuN.js (removed) 7.81 kB 🟢 -7.81 kB 🟢 -2.48 kB 🟢 -2.17 kB
assets/CloudLayoutView-BpKxOzlQ.js (new) 7.81 kB 🔴 +7.81 kB 🔴 +2.49 kB 🔴 +2.16 kB
assets/CloudForgotPasswordView-CLuYorlN.js (new) 6.22 kB 🔴 +6.22 kB 🔴 +2.21 kB 🔴 +1.95 kB
assets/CloudForgotPasswordView-CmZ9xUIs.js (removed) 6.22 kB 🟢 -6.22 kB 🟢 -2.21 kB 🟢 -1.94 kB
assets/CloudAuthTimeoutView-Bl-14uKb.js (removed) 5.58 kB 🟢 -5.58 kB 🟢 -2.05 kB 🟢 -1.79 kB
assets/CloudAuthTimeoutView-s1hsy1jj.js (new) 5.58 kB 🔴 +5.58 kB 🔴 +2.05 kB 🔴 +1.8 kB
assets/CloudSubscriptionRedirectView-Bqjhk-Op.js (removed) 5.36 kB 🟢 -5.36 kB 🟢 -2.03 kB 🟢 -1.79 kB
assets/CloudSubscriptionRedirectView-CzL3_hsA.js (new) 5.36 kB 🔴 +5.36 kB 🔴 +2.03 kB 🔴 +1.81 kB
assets/UserSelectView-Dnnpmk6e.js (new) 4.7 kB 🔴 +4.7 kB 🔴 +1.75 kB 🔴 +1.55 kB
assets/UserSelectView-DULAx-tZ.js (removed) 4.7 kB 🟢 -4.7 kB 🟢 -1.75 kB 🟢 -1.55 kB

Status: 9 added / 9 removed / 2 unchanged

Panels & Settings — 527 kB (baseline 527 kB) • ⚪ 0 B

Configuration panels, inspectors, and settings screens

File Before After Δ Raw Δ Gzip Δ Brotli
assets/KeybindingPanel-4ymUTHhq.js (new) 47.5 kB 🔴 +47.5 kB 🔴 +9.87 kB 🔴 +8.73 kB
assets/KeybindingPanel-DmI6S7M4.js (removed) 47.5 kB 🟢 -47.5 kB 🟢 -9.87 kB 🟢 -8.75 kB
assets/SecretsPanel-B546YL_9.js (new) 23.9 kB 🔴 +23.9 kB 🔴 +5.73 kB 🔴 +5.04 kB
assets/SecretsPanel-DjDttGDT.js (removed) 23.9 kB 🟢 -23.9 kB 🟢 -5.73 kB 🟢 -5.04 kB
assets/LegacyCreditsPanel-C78WenNp.js (new) 21.8 kB 🔴 +21.8 kB 🔴 +5.94 kB 🔴 +5.25 kB
assets/LegacyCreditsPanel-CLyL8bJR.js (removed) 21.8 kB 🟢 -21.8 kB 🟢 -5.94 kB 🟢 -5.25 kB
assets/SubscriptionPanel-D7LtaMky.js (new) 20.2 kB 🔴 +20.2 kB 🔴 +5.18 kB 🔴 +4.56 kB
assets/SubscriptionPanel-DDroNQul.js (removed) 20.2 kB 🟢 -20.2 kB 🟢 -5.18 kB 🟢 -4.55 kB
assets/AboutPanel-DPckE5wB.js (removed) 12 kB 🟢 -12 kB 🟢 -3.32 kB 🟢 -2.99 kB
assets/AboutPanel-DpCLZ10j.js (new) 12 kB 🔴 +12 kB 🔴 +3.32 kB 🔴 +2.98 kB
assets/ExtensionPanel-BKUpSa2L.js (new) 10.1 kB 🔴 +10.1 kB 🔴 +2.95 kB 🔴 +2.61 kB
assets/ExtensionPanel-BYmEoM0J.js (removed) 10.1 kB 🟢 -10.1 kB 🟢 -2.94 kB 🟢 -2.61 kB
assets/ServerConfigPanel-CxBDAXEg.js (new) 7.13 kB 🔴 +7.13 kB 🔴 +2.4 kB 🔴 +2.14 kB
assets/ServerConfigPanel-nAS-thLf.js (removed) 7.13 kB 🟢 -7.13 kB 🟢 -2.39 kB 🟢 -2.14 kB
assets/UserPanel-CRa9W9b7.js (removed) 6.84 kB 🟢 -6.84 kB 🟢 -2.27 kB 🟢 -2 kB
assets/UserPanel-DV0SqDXS.js (new) 6.84 kB 🔴 +6.84 kB 🔴 +2.27 kB 🔴 +2.01 kB
assets/cloudRemoteConfig-C9SjcMJX.js (removed) 2.13 kB 🟢 -2.13 kB 🟢 -1.02 kB 🟢 -880 B
assets/cloudRemoteConfig-CDMZ4Mhy.js (new) 2.13 kB 🔴 +2.13 kB 🔴 +1.02 kB 🔴 +881 B
assets/refreshRemoteConfig-DDrr8Zyl.js (new) 1.45 kB 🔴 +1.45 kB 🔴 +650 B 🔴 +550 B
assets/refreshRemoteConfig-mZgUcaWP.js (removed) 1.45 kB 🟢 -1.45 kB 🟢 -648 B 🟢 -555 B

Status: 10 added / 10 removed / 14 unchanged

User & Accounts — 17.8 kB (baseline 17.8 kB) • 🔴 +1 B

Authentication, profile, and account management bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/auth-DaGliDlp.js (new) 3.65 kB 🔴 +3.65 kB 🔴 +1.29 kB 🔴 +1.1 kB
assets/auth-D0jcgh2q.js (removed) 3.65 kB 🟢 -3.65 kB 🟢 -1.29 kB 🟢 -1.11 kB
assets/SignUpForm-BhsZyLC_.js (new) 3.19 kB 🔴 +3.19 kB 🔴 +1.29 kB 🔴 +1.15 kB
assets/SignUpForm-Bi_5tm4A.js (removed) 3.19 kB 🟢 -3.19 kB 🟢 -1.29 kB 🟢 -1.15 kB
assets/UpdatePasswordContent-Dt4-sozb.js (removed) 2.98 kB 🟢 -2.98 kB 🟢 -1.33 kB 🟢 -1.19 kB
assets/UpdatePasswordContent-yWAyZDBE.js (new) 2.98 kB 🔴 +2.98 kB 🔴 +1.33 kB 🔴 +1.19 kB
assets/authStore-B-DTfUkH.js (removed) 1.27 kB 🟢 -1.27 kB 🟢 -597 B 🟢 -535 B
assets/authStore-CTRRNdtM.js (new) 1.27 kB 🔴 +1.27 kB 🔴 +597 B 🔴 +534 B
assets/auth--4WZ5lgC.js (removed) 348 B 🟢 -348 B 🟢 -219 B 🟢 -192 B
assets/auth-DldFxF0i.js (new) 348 B 🔴 +348 B 🔴 +217 B 🔴 +210 B

Status: 5 added / 5 removed / 2 unchanged

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

Modals, dialogs, drawers, and in-app editors

File Before After Δ Raw Δ Gzip Δ Brotli
assets/ComfyHubPublishDialog-CjoaCz0m.js (removed) 85.8 kB 🟢 -85.8 kB 🟢 -18.6 kB 🟢 -15.9 kB
assets/ComfyHubPublishDialog-ZtBZ3olR.js (new) 85.8 kB 🔴 +85.8 kB 🔴 +18.6 kB 🔴 +15.9 kB
assets/useShareDialog-0d27CoHH.js (new) 23.9 kB 🔴 +23.9 kB 🔴 +5.81 kB 🔴 +5.15 kB
assets/useShareDialog-DoLTU15R.js (removed) 23.9 kB 🟢 -23.9 kB 🟢 -5.81 kB 🟢 -5.15 kB
assets/ComfyHubPublishDialog-C1Sw4rML.js (new) 1.43 kB 🔴 +1.43 kB 🔴 +658 B 🔴 +587 B
assets/ComfyHubPublishDialog-CT7uSYFn.js (removed) 1.43 kB 🟢 -1.43 kB 🟢 -657 B 🟢 -585 B
assets/useSubscriptionDialog-C8GDATCm.js (removed) 1.25 kB 🟢 -1.25 kB 🟢 -589 B 🟢 -520 B
assets/useSubscriptionDialog-CNvHPl61.js (new) 1.25 kB 🔴 +1.25 kB 🔴 +589 B 🔴 +520 B

Status: 4 added / 4 removed

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

Reusable component library chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/ComfyQueueButton-BtCcEhOB.js (new) 13.5 kB 🔴 +13.5 kB 🔴 +3.79 kB 🔴 +3.38 kB
assets/ComfyQueueButton-D6ylVNPp.js (removed) 13.5 kB 🟢 -13.5 kB 🟢 -3.79 kB 🟢 -3.38 kB
assets/useTerminalTabs-jMPVjgLf.js (new) 11.1 kB 🔴 +11.1 kB 🔴 +3.76 kB 🔴 +3.31 kB
assets/useTerminalTabs-mv2KVIBc.js (removed) 11.1 kB 🟢 -11.1 kB 🟢 -3.76 kB 🟢 -3.31 kB
assets/SubscribeButton-DR10Z97H.js (new) 2.42 kB 🔴 +2.42 kB 🔴 +1.05 kB 🔴 +918 B
assets/SubscribeButton-DRSwD9AF.js (removed) 2.42 kB 🟢 -2.42 kB 🟢 -1.05 kB 🟢 -921 B
assets/cloudFeedbackTopbarButton-CQWMnPpj.js (new) 1.94 kB 🔴 +1.94 kB 🔴 +966 B 🔴 +860 B
assets/cloudFeedbackTopbarButton-DDNPLpY9.js (removed) 1.94 kB 🟢 -1.94 kB 🟢 -965 B 🟢 -862 B
assets/ComfyQueueButton-CHZJUJZD.js (new) 1.35 kB 🔴 +1.35 kB 🔴 +625 B 🔴 +570 B
assets/ComfyQueueButton-DXevYOBU.js (removed) 1.35 kB 🟢 -1.35 kB 🟢 -626 B 🟢 -574 B

Status: 5 added / 5 removed / 8 unchanged

Data & Services — 3.16 MB (baseline 3.16 MB) • ⚪ 0 B

Stores, services, APIs, and repositories

File Before After Δ Raw Δ Gzip Δ Brotli
assets/dialogService-Bgn9bM1E.js (removed) 2.09 MB 🟢 -2.09 MB 🟢 -477 kB 🟢 -361 kB
assets/dialogService-DDgPiqlg.js (new) 2.09 MB 🔴 +2.09 MB 🔴 +477 kB 🔴 +361 kB
assets/api-CUB04mKD.js (new) 898 kB 🔴 +898 kB 🔴 +215 kB 🔴 +169 kB
assets/api-DJeX47MK.js (removed) 898 kB 🟢 -898 kB 🟢 -215 kB 🟢 -169 kB
assets/load3dService-D6G4rQDj.js (removed) 116 kB 🟢 -116 kB 🟢 -25.4 kB 🟢 -21.6 kB
assets/load3dService-DAiIWakl.js (new) 116 kB 🔴 +116 kB 🔴 +25.4 kB 🔴 +21.6 kB
assets/workflowShareService-Be-yJTJM.js (removed) 16.7 kB 🟢 -16.7 kB 🟢 -4.93 kB 🟢 -4.37 kB
assets/workflowShareService-DrWuaPpT.js (new) 16.7 kB 🔴 +16.7 kB 🔴 +4.92 kB 🔴 +4.36 kB
assets/keybindingService-B_lPLI1l.js (removed) 13.8 kB 🟢 -13.8 kB 🟢 -3.67 kB 🟢 -3.22 kB
assets/keybindingService-fv6KL29I.js (new) 13.8 kB 🔴 +13.8 kB 🔴 +3.67 kB 🔴 +3.22 kB
assets/releaseStore-BFcxXSxS.js (removed) 8.12 kB 🟢 -8.12 kB 🟢 -2.28 kB 🟢 -2 kB
assets/releaseStore-CFdsaiBw.js (new) 8.12 kB 🔴 +8.12 kB 🔴 +2.28 kB 🔴 +2 kB
assets/userStore-CASgJX_a.js (new) 2.42 kB 🔴 +2.42 kB 🔴 +933 B 🔴 +822 B
assets/userStore-CGcFcZur.js (removed) 2.42 kB 🟢 -2.42 kB 🟢 -934 B 🟢 -818 B
assets/audioService-Dv20noJn.js (new) 1.8 kB 🔴 +1.8 kB 🔴 +878 B 🔴 +759 B
assets/audioService-sW6JJun4.js (removed) 1.8 kB 🟢 -1.8 kB 🟢 -880 B 🟢 -761 B
assets/releaseStore-DCxWaRyi.js (removed) 1.27 kB 🟢 -1.27 kB 🟢 -593 B 🟢 -526 B
assets/releaseStore-kr1g6DKp.js (new) 1.27 kB 🔴 +1.27 kB 🔴 +594 B 🔴 +528 B
assets/workflowDraftStore-DXDYSX5Z.js (new) 1.25 kB 🔴 +1.25 kB 🔴 +590 B 🔴 +523 B
assets/workflowDraftStore-mpGm4LVE.js (removed) 1.25 kB 🟢 -1.25 kB 🟢 -589 B 🟢 -523 B
assets/dialogService-Dbw2G--H.js (removed) 1.24 kB 🟢 -1.24 kB 🟢 -582 B 🟢 -519 B
assets/dialogService-DQAzyqZE.js (new) 1.24 kB 🔴 +1.24 kB 🔴 +582 B 🔴 +523 B
assets/settingStore-DhkdOkFl.js (new) 1.23 kB 🔴 +1.23 kB 🔴 +585 B 🔴 +519 B
assets/settingStore-DvolVDY3.js (removed) 1.23 kB 🟢 -1.23 kB 🟢 -584 B 🟢 -518 B
assets/assetsStore-CDQSqKJT.js (new) 1.23 kB 🔴 +1.23 kB 🔴 +585 B 🔴 +517 B
assets/assetsStore-ErkG5gF2.js (removed) 1.23 kB 🟢 -1.23 kB 🟢 -584 B 🟢 -517 B

Status: 13 added / 13 removed / 4 unchanged

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

Helpers, composables, and utility bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/useConflictDetection-BmnlfKj2.js (new) 234 kB 🔴 +234 kB 🔴 +52.1 kB 🔴 +42.4 kB
assets/useConflictDetection-C7S8ikQw.js (removed) 234 kB 🟢 -234 kB 🟢 -52.1 kB 🟢 -42.4 kB
assets/useLoad3d-D4NBkkG5.js (removed) 23.9 kB 🟢 -23.9 kB 🟢 -5.42 kB 🟢 -4.79 kB
assets/useLoad3d-xeyB_1kf.js (new) 23.9 kB 🔴 +23.9 kB 🔴 +5.42 kB 🔴 +4.78 kB
assets/useLoad3dViewer-BE7_I20l.js (removed) 21 kB 🟢 -21 kB 🟢 -4.93 kB 🟢 -4.3 kB
assets/useLoad3dViewer-Laxl5sVC.js (new) 21 kB 🔴 +21 kB 🔴 +4.92 kB 🔴 +4.3 kB
assets/useFeatureFlags-BvDWFEOQ.js (new) 5.95 kB 🔴 +5.95 kB 🔴 +1.79 kB 🔴 +1.52 kB
assets/useFeatureFlags-CaUgvezp.js (removed) 5.95 kB 🟢 -5.95 kB 🟢 -1.79 kB 🟢 -1.52 kB
assets/useCopyToClipboard-C7SIoNi_.js (removed) 5.29 kB 🟢 -5.29 kB 🟢 -1.86 kB 🟢 -1.58 kB
assets/useCopyToClipboard-DxQmcprR.js (new) 5.29 kB 🔴 +5.29 kB 🔴 +1.86 kB 🔴 +1.57 kB
assets/useWorkspaceUI-Dmrs1-cH.js (removed) 3.34 kB 🟢 -3.34 kB 🟢 -981 B 🟢 -810 B
assets/useWorkspaceUI-DsYKlv18.js (new) 3.34 kB 🔴 +3.34 kB 🔴 +981 B 🔴 +811 B
assets/subscriptionCheckoutUtil-C5ex_1YL.js (new) 3.31 kB 🔴 +3.31 kB 🔴 +1.36 kB 🔴 +1.18 kB
assets/subscriptionCheckoutUtil-Co7tLz24.js (removed) 3.31 kB 🟢 -3.31 kB 🟢 -1.36 kB 🟢 -1.18 kB
assets/assetPreviewUtil-DH4ox6hn.js (removed) 2.43 kB 🟢 -2.43 kB 🟢 -1.01 kB 🟢 -882 B
assets/assetPreviewUtil-NXwboxl4.js (new) 2.43 kB 🔴 +2.43 kB 🔴 +1.01 kB 🔴 +883 B
assets/useUpstreamValue-2u9-XGk1.js (removed) 2.08 kB 🟢 -2.08 kB 🟢 -803 B 🟢 -716 B
assets/useUpstreamValue-D5rC8Wui.js (new) 2.08 kB 🔴 +2.08 kB 🔴 +803 B 🔴 +709 B
assets/useLoad3d-CmxILz17.js (removed) 1.41 kB 🟢 -1.41 kB 🟢 -652 B 🟢 -600 B
assets/useLoad3d-CxlA34W2.js (new) 1.41 kB 🔴 +1.41 kB 🔴 +654 B 🔴 +584 B
assets/useLoad3dViewer-A8pGKLo6.js (removed) 1.35 kB 🟢 -1.35 kB 🟢 -622 B 🟢 -573 B
assets/useLoad3dViewer-D4sqsPaf.js (new) 1.35 kB 🔴 +1.35 kB 🔴 +621 B 🔴 +570 B
assets/useCurrentUser-DkYamWyd.js (new) 1.23 kB 🔴 +1.23 kB 🔴 +585 B 🔴 +520 B
assets/useCurrentUser-DLyDRsO_.js (removed) 1.23 kB 🟢 -1.23 kB 🟢 -585 B 🟢 -519 B
assets/useWorkspaceSwitch-C7RIY4F1.js (new) 747 B 🔴 +747 B 🔴 +383 B 🔴 +334 B
assets/useWorkspaceSwitch-DJh1Dlfw.js (removed) 747 B 🟢 -747 B 🟢 -383 B 🟢 -327 B

Status: 13 added / 13 removed / 18 unchanged

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

External libraries and shared vendor chunks

Status: 16 unchanged

Other — 9.23 MB (baseline 9.23 MB) • ⚪ 0 B

Bundles that do not match a named category

File Before After Δ Raw Δ Gzip Δ Brotli
assets/WidgetSelect-DQExhqn5.js (removed) 78.7 kB 🟢 -78.7 kB 🟢 -17.4 kB 🟢 -15 kB
assets/WidgetSelect-nzzJTln-.js (new) 78.7 kB 🔴 +78.7 kB 🔴 +17.4 kB 🔴 +15 kB
assets/core-BTuuVWiL.js (removed) 78 kB 🟢 -78 kB 🟢 -20.2 kB 🟢 -17.2 kB
assets/core-CxhXt4Le.js (new) 78 kB 🔴 +78 kB 🔴 +20.2 kB 🔴 +17.2 kB
assets/groupNode-BmaIDv3L.js (new) 74.9 kB 🔴 +74.9 kB 🔴 +18.7 kB 🔴 +16.5 kB
assets/groupNode-BZX2I5u0.js (removed) 74.9 kB 🟢 -74.9 kB 🟢 -18.7 kB 🟢 -16.5 kB
assets/SubscriptionRequiredDialogContentWorkspace-Cso4IC2X.js (removed) 48.9 kB 🟢 -48.9 kB 🟢 -9.55 kB 🟢 -8.27 kB
assets/SubscriptionRequiredDialogContentWorkspace-KDMMFbI2.js (new) 48.9 kB 🔴 +48.9 kB 🔴 +9.56 kB 🔴 +8.24 kB
assets/Load3DControls-C23Gt5v9.js (removed) 46.1 kB 🟢 -46.1 kB 🟢 -7.5 kB 🟢 -6.55 kB
assets/Load3DControls-D9OqAqnA.js (new) 46.1 kB 🔴 +46.1 kB 🔴 +7.5 kB 🔴 +6.54 kB
assets/WorkspacePanelContent-CHjLtT34.js (removed) 34.3 kB 🟢 -34.3 kB 🟢 -7.44 kB 🟢 -6.58 kB
assets/WorkspacePanelContent-DpdckDww.js (new) 34.3 kB 🔴 +34.3 kB 🔴 +7.44 kB 🔴 +6.6 kB
assets/WidgetPainter-CIOKS9TJ.js (new) 33.5 kB 🔴 +33.5 kB 🔴 +8.29 kB 🔴 +7.35 kB
assets/WidgetPainter-Kp80qK8v.js (removed) 33.5 kB 🟢 -33.5 kB 🟢 -8.29 kB 🟢 -7.35 kB
assets/Load3dViewerContent-BUtWWWVo.js (removed) 30.6 kB 🟢 -30.6 kB 🟢 -6.19 kB 🟢 -5.37 kB
assets/Load3dViewerContent-DLguSjaR.js (new) 30.6 kB 🔴 +30.6 kB 🔴 +6.19 kB 🔴 +5.37 kB
assets/SubscriptionRequiredDialogContent-_zwaA3Sc.js (removed) 27.6 kB 🟢 -27.6 kB 🟢 -7.02 kB 🟢 -6.2 kB
assets/SubscriptionRequiredDialogContent-B_9CggmX.js (new) 27.6 kB 🔴 +27.6 kB 🔴 +7.02 kB 🔴 +6.2 kB
assets/WidgetImageCrop-BPD86y6J.js (new) 24.4 kB 🔴 +24.4 kB 🔴 +6.24 kB 🔴 +5.49 kB
assets/WidgetImageCrop-CS20OZtN.js (removed) 24.4 kB 🟢 -24.4 kB 🟢 -6.23 kB 🟢 -5.49 kB
assets/SubscriptionPanelContentWorkspace-CAeQmS9Q.js (removed) 22.2 kB 🟢 -22.2 kB 🟢 -5.17 kB 🟢 -4.56 kB
assets/SubscriptionPanelContentWorkspace-CkFM8-GY.js (new) 22.2 kB 🔴 +22.2 kB 🔴 +5.17 kB 🔴 +4.56 kB
assets/SignInContent-C4LAsUnF.js (new) 20.9 kB 🔴 +20.9 kB 🔴 +5.47 kB 🔴 +4.79 kB
assets/SignInContent-DCUDgNWp.js (removed) 20.9 kB 🟢 -20.9 kB 🟢 -5.47 kB 🟢 -4.79 kB
assets/CurrentUserPopoverWorkspace-Fh_sAN3-.js (removed) 20.9 kB 🟢 -20.9 kB 🟢 -4.99 kB 🟢 -4.46 kB
assets/CurrentUserPopoverWorkspace-NWLgBe4W.js (new) 20.9 kB 🔴 +20.9 kB 🔴 +4.99 kB 🔴 +4.48 kB
assets/WidgetInputNumber-CP_J5bVI.js (new) 19.1 kB 🔴 +19.1 kB 🔴 +4.84 kB 🔴 +4.3 kB
assets/WidgetInputNumber-n0arQAlf.js (removed) 19.1 kB 🟢 -19.1 kB 🟢 -4.84 kB 🟢 -4.29 kB
assets/Load3D-BP4xWv1G.js (new) 18.5 kB 🔴 +18.5 kB 🔴 +4.39 kB 🔴 +3.83 kB
assets/Load3D-Cp57N4aR.js (removed) 18.5 kB 🟢 -18.5 kB 🟢 -4.39 kB 🟢 -3.84 kB
assets/load3d-7jdx9e2O.js (removed) 17.7 kB 🟢 -17.7 kB 🟢 -4.92 kB 🟢 -4.27 kB
assets/load3d-BDOYqna4.js (new) 17.7 kB 🔴 +17.7 kB 🔴 +4.92 kB 🔴 +4.27 kB
assets/WidgetRecordAudio-CfJtxKWv.js (new) 17.6 kB 🔴 +17.6 kB 🔴 +5.07 kB 🔴 +4.52 kB
assets/WidgetRecordAudio-WCUwHZ0w.js (removed) 17.6 kB 🟢 -17.6 kB 🟢 -5.07 kB 🟢 -4.52 kB
assets/WidgetRange-B23u7DnQ.js (removed) 17.1 kB 🟢 -17.1 kB 🟢 -4.64 kB 🟢 -4.14 kB
assets/WidgetRange-BrPpD4Df.js (new) 17.1 kB 🔴 +17.1 kB 🔴 +4.64 kB 🔴 +4.14 kB
assets/WaveAudioPlayer-BsYBY1DR.js (removed) 13.4 kB 🟢 -13.4 kB 🟢 -3.68 kB 🟢 -3.22 kB
assets/WaveAudioPlayer-YCh2P2n3.js (new) 13.4 kB 🔴 +13.4 kB 🔴 +3.68 kB 🔴 +3.23 kB
assets/WidgetCurve-C72PEtGs.js (removed) 12.3 kB 🟢 -12.3 kB 🟢 -3.98 kB 🟢 -3.6 kB
assets/WidgetCurve-h2Vrk4gc.js (new) 12.3 kB 🔴 +12.3 kB 🔴 +3.98 kB 🔴 +3.6 kB
assets/TeamWorkspacesDialogContent-BQNnqaeW.js (new) 11.4 kB 🔴 +11.4 kB 🔴 +3.46 kB 🔴 +3.08 kB
assets/TeamWorkspacesDialogContent-CGXv5M5a.js (removed) 11.4 kB 🟢 -11.4 kB 🟢 -3.46 kB 🟢 -3.08 kB
assets/nodeTemplates-Brx1dkq9.js (new) 9.92 kB 🔴 +9.92 kB 🔴 +3.51 kB 🔴 +3.11 kB
assets/nodeTemplates-MKkTZ84w.js (removed) 9.92 kB 🟢 -9.92 kB 🟢 -3.51 kB 🟢 -3.11 kB
assets/NightlySurveyController-CAfpsHU_.js (removed) 9.05 kB 🟢 -9.05 kB 🟢 -3.18 kB 🟢 -2.8 kB
assets/NightlySurveyController-Cp8wnta0.js (new) 9.05 kB 🔴 +9.05 kB 🔴 +3.19 kB 🔴 +2.8 kB
assets/Load3DConfiguration-CrcjQp9X.js (new) 8.77 kB 🔴 +8.77 kB 🔴 +2.61 kB 🔴 +2.29 kB
assets/Load3DConfiguration-De08SjAN.js (removed) 8.77 kB 🟢 -8.77 kB 🟢 -2.61 kB 🟢 -2.29 kB
assets/InviteMemberDialogContent-5NO2bVxa.js (new) 8.02 kB 🔴 +8.02 kB 🔴 +2.56 kB 🔴 +2.25 kB
assets/InviteMemberDialogContent-T5gimTlW.js (removed) 8.02 kB 🟢 -8.02 kB 🟢 -2.56 kB 🟢 -2.25 kB
assets/onboardingCloudRoutes-BK50H7TK.js (new) 6.94 kB 🔴 +6.94 kB 🔴 +2.17 kB 🔴 +1.86 kB
assets/onboardingCloudRoutes-DQEWXlLh.js (removed) 6.94 kB 🟢 -6.94 kB 🟢 -2.18 kB 🟢 -1.86 kB
assets/CreateWorkspaceDialogContent-BrYujwbP.js (removed) 6.23 kB 🟢 -6.23 kB 🟢 -2.27 kB 🟢 -1.99 kB
assets/CreateWorkspaceDialogContent-CKjeXRZx.js (new) 6.23 kB 🔴 +6.23 kB 🔴 +2.27 kB 🔴 +1.99 kB
assets/saveMesh-s1RdKmeJ.js (new) 6.21 kB 🔴 +6.21 kB 🔴 +2.15 kB 🔴 +1.9 kB
assets/saveMesh-wwWGssbP.js (removed) 6.21 kB 🟢 -6.21 kB 🟢 -2.14 kB 🟢 -1.9 kB
assets/WidgetWithControl-AosQQeBY.js (new) 6.2 kB 🔴 +6.2 kB 🔴 +2.5 kB 🔴 +2.21 kB
assets/WidgetWithControl-oj58kygr.js (removed) 6.2 kB 🟢 -6.2 kB 🟢 -2.5 kB 🟢 -2.22 kB
assets/FreeTierDialogContent-D-fUt3TN.js (removed) 6.09 kB 🟢 -6.09 kB 🟢 -2.17 kB 🟢 -1.93 kB
assets/FreeTierDialogContent-Du0AdpzX.js (new) 6.09 kB 🔴 +6.09 kB 🔴 +2.17 kB 🔴 +1.93 kB
assets/EditWorkspaceDialogContent-C66gFc17.js (new) 6.03 kB 🔴 +6.03 kB 🔴 +2.23 kB 🔴 +1.96 kB
assets/EditWorkspaceDialogContent-C739kvcH.js (removed) 6.03 kB 🟢 -6.03 kB 🟢 -2.23 kB 🟢 -1.95 kB
assets/WidgetTextarea-CCHOnsHj.js (new) 5.84 kB 🔴 +5.84 kB 🔴 +2.3 kB 🔴 +2.03 kB
assets/WidgetTextarea-CmhRua26.js (removed) 5.84 kB 🟢 -5.84 kB 🟢 -2.3 kB 🟢 -2.04 kB
assets/Preview3d-BLKN0qgn.js (new) 5.81 kB 🔴 +5.81 kB 🔴 +1.96 kB 🔴 +1.71 kB
assets/Preview3d-C4eBfLAw.js (removed) 5.81 kB 🟢 -5.81 kB 🟢 -1.96 kB 🟢 -1.7 kB
assets/ValueControlPopover-BsnZVJiy.js (removed) 5.61 kB 🟢 -5.61 kB 🟢 -2.05 kB 🟢 -1.85 kB
assets/ValueControlPopover-CdEQT4Xh.js (new) 5.61 kB 🔴 +5.61 kB 🔴 +2.05 kB 🔴 +1.83 kB
assets/CancelSubscriptionDialogContent-8v3ta6JX.js (removed) 5.53 kB 🟢 -5.53 kB 🟢 -2.07 kB 🟢 -1.83 kB
assets/CancelSubscriptionDialogContent-CWgpTegk.js (new) 5.53 kB 🔴 +5.53 kB 🔴 +2.07 kB 🔴 +1.82 kB
assets/DeleteWorkspaceDialogContent-BtKfqJ9_.js (new) 4.93 kB 🔴 +4.93 kB 🔴 +1.91 kB 🔴 +1.66 kB
assets/DeleteWorkspaceDialogContent-DRoVKBWE.js (removed) 4.93 kB 🟢 -4.93 kB 🟢 -1.91 kB 🟢 -1.66 kB
assets/LeaveWorkspaceDialogContent-C7x0oOff.js (new) 4.76 kB 🔴 +4.76 kB 🔴 +1.86 kB 🔴 +1.62 kB
assets/LeaveWorkspaceDialogContent-Dk-FhQDd.js (removed) 4.76 kB 🟢 -4.76 kB 🟢 -1.85 kB 🟢 -1.62 kB
assets/RemoveMemberDialogContent-gs0QSyR4.js (removed) 4.74 kB 🟢 -4.74 kB 🟢 -1.81 kB 🟢 -1.58 kB
assets/RemoveMemberDialogContent-tDysO0ud.js (new) 4.74 kB 🔴 +4.74 kB 🔴 +1.81 kB 🔴 +1.58 kB
assets/RevokeInviteDialogContent-BhA02JjU.js (new) 4.65 kB 🔴 +4.65 kB 🔴 +1.82 kB 🔴 +1.59 kB
assets/RevokeInviteDialogContent-DzPQNZDV.js (removed) 4.65 kB 🟢 -4.65 kB 🟢 -1.82 kB 🟢 -1.59 kB
assets/InviteMemberUpsellDialogContent-BVRt031V.js (new) 4.55 kB 🔴 +4.55 kB 🔴 +1.69 kB 🔴 +1.48 kB
assets/InviteMemberUpsellDialogContent-qtw8Y8Db.js (removed) 4.55 kB 🟢 -4.55 kB 🟢 -1.68 kB 🟢 -1.48 kB
assets/tierBenefits-cmvRJ25d.js (new) 4.45 kB 🔴 +4.45 kB 🔴 +1.58 kB 🔴 +1.36 kB
assets/tierBenefits-DmtT3YlG.js (removed) 4.45 kB 🟢 -4.45 kB 🟢 -1.58 kB 🟢 -1.36 kB
assets/Media3DTop-DEuZfV85.js (new) 4.43 kB 🔴 +4.43 kB 🔴 +1.81 kB 🔴 +1.6 kB
assets/Media3DTop-fEQMOIij.js (removed) 4.43 kB 🟢 -4.43 kB 🟢 -1.81 kB 🟢 -1.6 kB
assets/cloudSessionCookie-BUcisPkB.js (new) 4.39 kB 🔴 +4.39 kB 🔴 +1.61 kB 🔴 +1.4 kB
assets/cloudSessionCookie-oVOQxabz.js (removed) 4.39 kB 🟢 -4.39 kB 🟢 -1.6 kB 🟢 -1.4 kB
assets/GlobalToast-ClMHwPVt.js (removed) 3.05 kB 🟢 -3.05 kB 🟢 -1.26 kB 🟢 -1.12 kB
assets/GlobalToast-DVSGsIby.js (new) 3.05 kB 🔴 +3.05 kB 🔴 +1.26 kB 🔴 +1.08 kB
assets/CloudRunButtonWrapper-DTF-Dnud.js (removed) 2.31 kB 🟢 -2.31 kB 🟢 -1.05 kB 🟢 -940 B
assets/CloudRunButtonWrapper-ufuUXTcG.js (new) 2.31 kB 🔴 +2.31 kB 🔴 +1.05 kB 🔴 +944 B
assets/SubscribeToRun-BNu4tfFY.js (new) 2.13 kB 🔴 +2.13 kB 🔴 +982 B 🔴 +874 B
assets/SubscribeToRun-yykAOEx_.js (removed) 2.13 kB 🟢 -2.13 kB 🟢 -983 B 🟢 -872 B
assets/MediaAudioTop-CxbmMdlt.js (removed) 2.08 kB 🟢 -2.08 kB 🟢 -1 kB 🟢 -863 B
assets/MediaAudioTop-r2cpETQM.js (new) 2.08 kB 🔴 +2.08 kB 🔴 +1 kB 🔴 +857 B
assets/cloudBadges-ClGt315_.js (new) 2.04 kB 🔴 +2.04 kB 🔴 +1.01 kB 🔴 +876 B
assets/cloudBadges-cPQp2H43.js (removed) 2.04 kB 🟢 -2.04 kB 🟢 -1.01 kB 🟢 -881 B
assets/cloudSubscription-DurnD7fq.js (removed) 1.96 kB 🟢 -1.96 kB 🟢 -929 B 🟢 -806 B
assets/cloudSubscription-RYZfrgBx.js (new) 1.96 kB 🔴 +1.96 kB 🔴 +932 B 🔴 +809 B
assets/graphHasMissingNodes-B8WR4dtX.js (new) 1.84 kB 🔴 +1.84 kB 🔴 +862 B 🔴 +758 B
assets/graphHasMissingNodes-CTyQLWAF.js (removed) 1.84 kB 🟢 -1.84 kB 🟢 -864 B 🟢 -764 B
assets/Load3D-C7nnOf3X.js (removed) 1.66 kB 🟢 -1.66 kB 🟢 -741 B 🟢 -662 B
assets/Load3D-Dvs1XxqR.js (new) 1.66 kB 🔴 +1.66 kB 🔴 +739 B 🔴 +656 B
assets/nightlyBadges-C7daf0jn.js (new) 1.57 kB 🔴 +1.57 kB 🔴 +779 B 🔴 +696 B
assets/nightlyBadges-C7ZTu5oK.js (removed) 1.57 kB 🟢 -1.57 kB 🟢 -777 B 🟢 -693 B
assets/Load3dViewerContent-BO4ns7rF.js (removed) 1.54 kB 🟢 -1.54 kB 🟢 -696 B 🟢 -622 B
assets/Load3dViewerContent-sf9oHrka.js (new) 1.54 kB 🔴 +1.54 kB 🔴 +697 B 🔴 +619 B
assets/previousFullPath-Cbq1HzRZ.js (removed) 1.53 kB 🟢 -1.53 kB 🟢 -693 B 🟢 -601 B
assets/previousFullPath-DiADRmJF.js (new) 1.53 kB 🔴 +1.53 kB 🔴 +692 B 🔴 +599 B
assets/SubscriptionPanelContentWorkspace-IuOoxmQo.js (removed) 1.43 kB 🟢 -1.43 kB 🟢 -648 B 🟢 -578 B
assets/SubscriptionPanelContentWorkspace-m0bsBTD6.js (new) 1.43 kB 🔴 +1.43 kB 🔴 +648 B 🔴 +581 B
assets/WidgetLegacy-BrWh_rUc.js (new) 1.25 kB 🔴 +1.25 kB 🔴 +597 B 🔴 +531 B
assets/WidgetLegacy-D0gCsXnE.js (removed) 1.25 kB 🟢 -1.25 kB 🟢 -596 B 🟢 -527 B
assets/changeTracker-BlMxvwmZ.js (removed) 1.23 kB 🟢 -1.23 kB 🟢 -584 B 🟢 -517 B
assets/changeTracker-CZXvkA0r.js (new) 1.23 kB 🔴 +1.23 kB 🔴 +586 B 🔴 +519 B

Status: 57 added / 57 removed / 86 unchanged

⚡ Performance Report

canvas-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 62.7 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 49.8 MB heap
canvas-zoom-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 58.4 MB heap
dom-widget-clipping: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 55.5 MB heap
large-graph-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 76.4 MB heap
large-graph-pan: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 78.0 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 83.9 MB heap
minimap-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 82.3 MB heap
subgraph-dom-widget-clipping: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 54.5 MB heap
subgraph-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 66.7 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 59.8 MB heap
subgraph-transition-enter: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 160ms TBT · 91.3 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 74.4 MB heap
vue-large-graph-idle: · 56.3 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 262.9 MB heap
vue-large-graph-pan: · 56.2 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 158.1 MB heap
workflow-execution: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 54.0 MB heap

⚠️ 5 regressions detected

Metric Baseline PR (median) Δ Sig
canvas-mouse-sweep: task duration 799ms 989ms +24% ⚠️ z=2.1
large-graph-idle: task duration 651ms 656ms +1% ⚠️ z=2.1
large-graph-pan: style recalc duration 20ms 20ms -0% ⚠️ z=3.7
large-graph-pan: task duration 1161ms 1267ms +9% ⚠️ z=4.3
minimap-idle: task duration 598ms 645ms +8% ⚠️ z=2.5
All metrics
Metric Baseline PR (median) Δ Sig
canvas-idle: avg frame time 17ms 17ms +0% z=-0.1
canvas-idle: p95 frame time 17ms 17ms +0%
canvas-idle: layout duration 0ms 0ms +0%
canvas-idle: style recalc duration 8ms 11ms +33% z=0.3
canvas-idle: layout count 0 0 +0%
canvas-idle: style recalc count 9 12 +28% z=0.6
canvas-idle: task duration 527ms 428ms -19% z=1.1
canvas-idle: script duration 25ms 26ms +4% z=0.2
canvas-idle: TBT 0ms 0ms +0%
canvas-idle: heap used 52.8 MB 62.7 MB +19%
canvas-idle: DOM nodes -265 -118 -55% z=-110.2
canvas-idle: event listeners -131 -65 -51% z=-16.1
canvas-mouse-sweep: avg frame time 17ms 17ms -0% z=-0.9
canvas-mouse-sweep: p95 frame time 17ms 17ms -0%
canvas-mouse-sweep: layout duration 4ms 4ms +6% z=1.6
canvas-mouse-sweep: style recalc duration 40ms 46ms +15% z=0.9
canvas-mouse-sweep: layout count 12 12 +0%
canvas-mouse-sweep: style recalc count 74 78 +5% z=-0.5
canvas-mouse-sweep: task duration 799ms 989ms +24% ⚠️ z=2.1
canvas-mouse-sweep: script duration 127ms 144ms +14% z=1.3
canvas-mouse-sweep: TBT 0ms 0ms +0%
canvas-mouse-sweep: heap used 48.7 MB 49.8 MB +2%
canvas-mouse-sweep: DOM nodes -260 -261 +0% z=-124.8
canvas-mouse-sweep: event listeners -129 -133 +3% z=-33.9
canvas-zoom-sweep: avg frame time 17ms 17ms -0% z=-0.3
canvas-zoom-sweep: p95 frame time 17ms 17ms -0%
canvas-zoom-sweep: layout duration 1ms 1ms +3% z=-0.1
canvas-zoom-sweep: style recalc duration 17ms 19ms +11% z=-0.3
canvas-zoom-sweep: layout count 6 6 +0%
canvas-zoom-sweep: style recalc count 31 31 +0% z=-0.6
canvas-zoom-sweep: task duration 367ms 338ms -8% z=0.5
canvas-zoom-sweep: script duration 20ms 22ms +10% z=-1.6
canvas-zoom-sweep: TBT 0ms 0ms +0%
canvas-zoom-sweep: heap used 42.7 MB 58.4 MB +37%
canvas-zoom-sweep: DOM nodes -212 77 -136% z=-2.8
canvas-zoom-sweep: event listeners -118 19 -116% z=-0.9
dom-widget-clipping: avg frame time 17ms 17ms +0% z=0.1
dom-widget-clipping: p95 frame time 17ms 17ms +0%
dom-widget-clipping: layout duration 0ms 0ms +0%
dom-widget-clipping: style recalc duration 8ms 7ms -11% z=-3.3
dom-widget-clipping: layout count 0 0 +0%
dom-widget-clipping: style recalc count 11 10 -9% z=-6.2
dom-widget-clipping: task duration 334ms 350ms +5% z=-0.9
dom-widget-clipping: script duration 57ms 62ms +9% z=-1.7
dom-widget-clipping: TBT 0ms 0ms +0%
dom-widget-clipping: heap used 54.8 MB 55.5 MB +1%
dom-widget-clipping: DOM nodes 18 16 -11% z=-4.3
dom-widget-clipping: event listeners 2 2 +0% variance too high
large-graph-idle: avg frame time 17ms 17ms -0% z=-0.6
large-graph-idle: p95 frame time 17ms 17ms +0%
large-graph-idle: layout duration 0ms 0ms +0%
large-graph-idle: style recalc duration 9ms 10ms +15% z=-1.9
large-graph-idle: layout count 0 0 +0%
large-graph-idle: style recalc count 9 9 +0% z=-8.3
large-graph-idle: task duration 651ms 656ms +1% ⚠️ z=2.1
large-graph-idle: script duration 97ms 113ms +16% z=1.0
large-graph-idle: TBT 0ms 0ms +0%
large-graph-idle: heap used 63.8 MB 76.4 MB +20%
large-graph-idle: DOM nodes -262 -261 -1% z=-315.5
large-graph-idle: event listeners -129 -130 +1% z=-25.6
large-graph-pan: avg frame time 17ms 17ms +0% z=0.3
large-graph-pan: p95 frame time 17ms 17ms +0%
large-graph-pan: layout duration 0ms 0ms +0%
large-graph-pan: style recalc duration 20ms 20ms -0% ⚠️ z=3.7
large-graph-pan: layout count 0 0 +0%
large-graph-pan: style recalc count 69 69 +0% z=-0.9
large-graph-pan: task duration 1161ms 1267ms +9% ⚠️ z=4.3
large-graph-pan: script duration 407ms 442ms +9% z=1.7
large-graph-pan: TBT 0ms 0ms +0%
large-graph-pan: heap used 61.0 MB 78.0 MB +28%
large-graph-pan: DOM nodes -263 -266 +1% z=-172.4
large-graph-pan: event listeners -127 -143 +13% z=-178.6
large-graph-zoom: avg frame time 17ms 17ms +0%
large-graph-zoom: p95 frame time 17ms 17ms +0%
large-graph-zoom: layout duration 8ms 8ms -7%
large-graph-zoom: style recalc duration 20ms 19ms -6%
large-graph-zoom: layout count 60 60 +0%
large-graph-zoom: style recalc count 66 65 -2%
large-graph-zoom: task duration 1410ms 1429ms +1%
large-graph-zoom: script duration 499ms 508ms +2%
large-graph-zoom: TBT 0ms 0ms +0%
large-graph-zoom: heap used 103.2 MB 83.9 MB -19%
large-graph-zoom: DOM nodes -268 -267 -0%
large-graph-zoom: event listeners -127 -126 -1%
minimap-idle: avg frame time 17ms 17ms +0% z=0.1
minimap-idle: p95 frame time 17ms 17ms +0%
minimap-idle: layout duration 0ms 0ms +0%
minimap-idle: style recalc duration 8ms 8ms +4% z=-1.7
minimap-idle: layout count 0 0 +0%
minimap-idle: style recalc count 8 7 -13% z=-3.8
minimap-idle: task duration 598ms 645ms +8% ⚠️ z=2.5
minimap-idle: script duration 95ms 107ms +13% z=0.9
minimap-idle: TBT 0ms 0ms +0%
minimap-idle: heap used 68.5 MB 82.3 MB +20%
minimap-idle: DOM nodes -265 -264 -0% z=-207.0
minimap-idle: event listeners -129 -128 -1% z=-200.8
subgraph-dom-widget-clipping: avg frame time 17ms 17ms -0% z=-0.4
subgraph-dom-widget-clipping: p95 frame time 17ms 17ms +1%
subgraph-dom-widget-clipping: layout duration 0ms 0ms +0%
subgraph-dom-widget-clipping: style recalc duration 12ms 12ms +6% z=-0.2
subgraph-dom-widget-clipping: layout count 0 0 +0%
subgraph-dom-widget-clipping: style recalc count 47 48 +1% z=-0.8
subgraph-dom-widget-clipping: task duration 373ms 406ms +9% z=1.5
subgraph-dom-widget-clipping: script duration 126ms 127ms +0% z=-0.2
subgraph-dom-widget-clipping: TBT 0ms 0ms +0%
subgraph-dom-widget-clipping: heap used 61.7 MB 54.5 MB -12%
subgraph-dom-widget-clipping: DOM nodes 20 -109 -643% z=-116.8
subgraph-dom-widget-clipping: event listeners 8 -61 -856% z=-13.2
subgraph-idle: avg frame time 17ms 17ms +0% z=0.4
subgraph-idle: p95 frame time 17ms 17ms +0%
subgraph-idle: layout duration 0ms 0ms +0%
subgraph-idle: style recalc duration 9ms 10ms +12% z=-0.7
subgraph-idle: layout count 0 0 +0%
subgraph-idle: style recalc count 10 10 +0% z=-1.4
subgraph-idle: task duration 361ms 351ms -3% z=-0.6
subgraph-idle: script duration 15ms 17ms +7% z=-1.4
subgraph-idle: TBT 0ms 0ms +0%
subgraph-idle: heap used 68.6 MB 66.7 MB -3%
subgraph-idle: DOM nodes 19 20 +5% z=-1.2
subgraph-idle: event listeners 6 5 -17% variance too high
subgraph-mouse-sweep: avg frame time 17ms 17ms -0% z=0.4
subgraph-mouse-sweep: p95 frame time 17ms 17ms +1%
subgraph-mouse-sweep: layout duration 5ms 5ms +4% z=0.8
subgraph-mouse-sweep: style recalc duration 43ms 44ms +4% z=0.5
subgraph-mouse-sweep: layout count 16 16 +0%
subgraph-mouse-sweep: style recalc count 78 81 +4% z=0.5
subgraph-mouse-sweep: task duration 693ms 861ms +24% z=1.4
subgraph-mouse-sweep: script duration 99ms 106ms +7% z=0.9
subgraph-mouse-sweep: TBT 0ms 0ms +0%
subgraph-mouse-sweep: heap used 61.6 MB 59.8 MB -3%
subgraph-mouse-sweep: DOM nodes 65 -94 -244% z=-71.8
subgraph-mouse-sweep: event listeners 6 -64 -1158% variance too high
subgraph-transition-enter: avg frame time 17ms 17ms +0%
subgraph-transition-enter: p95 frame time 17ms 17ms -1%
subgraph-transition-enter: layout duration 12ms 13ms +4%
subgraph-transition-enter: style recalc duration 27ms 27ms -3%
subgraph-transition-enter: layout count 4 4 +0%
subgraph-transition-enter: style recalc count 16 15 -6%
subgraph-transition-enter: task duration 911ms 753ms -17%
subgraph-transition-enter: script duration 34ms 28ms -17%
subgraph-transition-enter: TBT 149ms 160ms +7%
subgraph-transition-enter: heap used 112.9 MB 91.3 MB -19%
subgraph-transition-enter: DOM nodes 12627 13513 +7%
subgraph-transition-enter: event listeners 1639 2529 +54%
viewport-pan-sweep: avg frame time 17ms 17ms -0%
viewport-pan-sweep: p95 frame time 17ms 17ms +0%
viewport-pan-sweep: layout duration 0ms 0ms +0%
viewport-pan-sweep: style recalc duration 58ms 54ms -6%
viewport-pan-sweep: layout count 0 0 +0%
viewport-pan-sweep: style recalc count 250 250 +0%
viewport-pan-sweep: task duration 4002ms 3905ms -2%
viewport-pan-sweep: script duration 1192ms 1305ms +10%
viewport-pan-sweep: TBT 0ms 0ms +0%
viewport-pan-sweep: heap used 122.1 MB 74.4 MB -39%
viewport-pan-sweep: DOM nodes -263 -122 -54%
viewport-pan-sweep: event listeners -125 -47 -63%
vue-large-graph-idle: avg frame time 18ms 18ms +0%
vue-large-graph-idle: p95 frame time 17ms 17ms +0%
vue-large-graph-idle: layout duration 0ms 0ms +0%
vue-large-graph-idle: style recalc duration 0ms 0ms +0%
vue-large-graph-idle: layout count 0 0 +0%
vue-large-graph-idle: style recalc count 0 0 +0%
vue-large-graph-idle: task duration 16197ms 14336ms -11%
vue-large-graph-idle: script duration 672ms 668ms -1%
vue-large-graph-idle: TBT 0ms 0ms +0%
vue-large-graph-idle: heap used 242.7 MB 262.9 MB +8%
vue-large-graph-idle: DOM nodes -8335 -5819 -30%
vue-large-graph-idle: event listeners -16486 -16472 -0%
vue-large-graph-pan: avg frame time 18ms 18ms +0%
vue-large-graph-pan: p95 frame time 17ms 17ms -0%
vue-large-graph-pan: layout duration 0ms 0ms +0%
vue-large-graph-pan: style recalc duration 25ms 19ms -27%
vue-large-graph-pan: layout count 0 0 +0%
vue-large-graph-pan: style recalc count 128 68 -47%
vue-large-graph-pan: task duration 18814ms 14794ms -21%
vue-large-graph-pan: script duration 985ms 890ms -10%
vue-large-graph-pan: TBT 59ms 0ms -100%
vue-large-graph-pan: heap used 252.3 MB 158.1 MB -37%
vue-large-graph-pan: DOM nodes -8329 -8331 +0%
vue-large-graph-pan: event listeners -16484 -16462 -0%
workflow-execution: avg frame time 17ms 17ms +0% z=1.1
workflow-execution: p95 frame time 17ms 17ms +0%
workflow-execution: layout duration 2ms 1ms -21% z=-1.8
workflow-execution: style recalc duration 23ms 24ms +4% z=-0.2
workflow-execution: layout count 5 5 -10% z=-0.8
workflow-execution: style recalc count 16 17 +6% z=-0.4
workflow-execution: task duration 119ms 125ms +5% z=0.2
workflow-execution: script duration 23ms 27ms +15% z=-0.8
workflow-execution: TBT 0ms 0ms +0%
workflow-execution: heap used 52.9 MB 54.0 MB +2%
workflow-execution: DOM nodes 155 163 +5% z=0.2
workflow-execution: event listeners 71 69 -3% z=3.9
Historical variance (last 15 runs)
Metric μ σ CV
canvas-idle: avg frame time 17ms 0ms 0.0%
canvas-idle: layout duration 0ms 0ms 0.0%
canvas-idle: style recalc duration 11ms 1ms 8.2%
canvas-idle: layout count 0 0 0.0%
canvas-idle: style recalc count 11 1 5.0%
canvas-idle: task duration 395ms 31ms 7.9%
canvas-idle: script duration 25ms 2ms 8.8%
canvas-idle: TBT 0ms 0ms 0.0%
canvas-idle: DOM nodes 23 1 5.6%
canvas-idle: event listeners 12 5 40.9%
canvas-mouse-sweep: avg frame time 17ms 0ms 0.0%
canvas-mouse-sweep: layout duration 4ms 0ms 5.4%
canvas-mouse-sweep: style recalc duration 43ms 3ms 7.4%
canvas-mouse-sweep: layout count 12 0 0.0%
canvas-mouse-sweep: style recalc count 79 2 3.0%
canvas-mouse-sweep: task duration 865ms 58ms 6.7%
canvas-mouse-sweep: script duration 136ms 6ms 4.8%
canvas-mouse-sweep: TBT 0ms 0ms 0.0%
canvas-mouse-sweep: DOM nodes 62 3 4.2%
canvas-mouse-sweep: event listeners 8 4 49.4%
canvas-zoom-sweep: avg frame time 17ms 0ms 0.0%
canvas-zoom-sweep: layout duration 1ms 0ms 7.0%
canvas-zoom-sweep: style recalc duration 19ms 2ms 8.0%
canvas-zoom-sweep: layout count 6 0 0.0%
canvas-zoom-sweep: style recalc count 31 0 1.5%
canvas-zoom-sweep: task duration 327ms 23ms 7.1%
canvas-zoom-sweep: script duration 27ms 3ms 11.1%
canvas-zoom-sweep: TBT 0ms 0ms 0.0%
canvas-zoom-sweep: DOM nodes 79 1 1.0%
canvas-zoom-sweep: event listeners 24 5 21.8%
dom-widget-clipping: avg frame time 17ms 0ms 0.0%
dom-widget-clipping: layout duration 0ms 0ms 0.0%
dom-widget-clipping: style recalc duration 10ms 1ms 8.0%
dom-widget-clipping: layout count 0 0 0.0%
dom-widget-clipping: style recalc count 13 0 3.8%
dom-widget-clipping: task duration 365ms 16ms 4.5%
dom-widget-clipping: script duration 68ms 3ms 4.8%
dom-widget-clipping: TBT 0ms 0ms 0.0%
dom-widget-clipping: DOM nodes 22 1 6.4%
dom-widget-clipping: event listeners 8 6 81.2%
large-graph-idle: avg frame time 17ms 0ms 0.0%
large-graph-idle: layout duration 0ms 0ms 0.0%
large-graph-idle: style recalc duration 12ms 1ms 8.6%
large-graph-idle: layout count 0 0 0.0%
large-graph-idle: style recalc count 12 0 2.7%
large-graph-idle: task duration 542ms 54ms 10.0%
large-graph-idle: script duration 102ms 11ms 10.3%
large-graph-idle: TBT 0ms 0ms 0.0%
large-graph-idle: DOM nodes 25 1 3.7%
large-graph-idle: event listeners 26 6 23.2%
large-graph-pan: avg frame time 17ms 0ms 0.0%
large-graph-pan: layout duration 0ms 0ms 0.0%
large-graph-pan: style recalc duration 17ms 1ms 4.6%
large-graph-pan: layout count 0 0 0.0%
large-graph-pan: style recalc count 70 1 0.9%
large-graph-pan: task duration 1082ms 43ms 4.0%
large-graph-pan: script duration 408ms 20ms 4.8%
large-graph-pan: TBT 0ms 0ms 0.0%
large-graph-pan: DOM nodes 19 2 8.7%
large-graph-pan: event listeners 5 1 16.8%
minimap-idle: avg frame time 17ms 0ms 0.0%
minimap-idle: layout duration 0ms 0ms 0.0%
minimap-idle: style recalc duration 10ms 1ms 8.6%
minimap-idle: layout count 0 0 0.0%
minimap-idle: style recalc count 10 1 7.1%
minimap-idle: task duration 527ms 47ms 9.0%
minimap-idle: script duration 98ms 10ms 10.1%
minimap-idle: TBT 0ms 0ms 0.0%
minimap-idle: DOM nodes 19 1 7.1%
minimap-idle: event listeners 5 1 14.4%
subgraph-dom-widget-clipping: avg frame time 17ms 0ms 0.0%
subgraph-dom-widget-clipping: layout duration 0ms 0ms 0.0%
subgraph-dom-widget-clipping: style recalc duration 13ms 1ms 7.4%
subgraph-dom-widget-clipping: layout count 0 0 0.0%
subgraph-dom-widget-clipping: style recalc count 48 1 1.2%
subgraph-dom-widget-clipping: task duration 378ms 18ms 4.9%
subgraph-dom-widget-clipping: script duration 128ms 6ms 4.9%
subgraph-dom-widget-clipping: TBT 0ms 0ms 0.0%
subgraph-dom-widget-clipping: DOM nodes 22 1 5.0%
subgraph-dom-widget-clipping: event listeners 16 6 36.0%
subgraph-idle: avg frame time 17ms 0ms 0.0%
subgraph-idle: layout duration 0ms 0ms 0.0%
subgraph-idle: style recalc duration 10ms 1ms 7.5%
subgraph-idle: layout count 0 0 0.0%
subgraph-idle: style recalc count 11 1 6.0%
subgraph-idle: task duration 370ms 31ms 8.5%
subgraph-idle: script duration 20ms 3ms 13.2%
subgraph-idle: TBT 0ms 0ms 0.0%
subgraph-idle: DOM nodes 22 1 6.9%
subgraph-idle: event listeners 10 7 64.5%
subgraph-mouse-sweep: avg frame time 17ms 0ms 0.0%
subgraph-mouse-sweep: layout duration 5ms 0ms 6.8%
subgraph-mouse-sweep: style recalc duration 42ms 3ms 7.8%
subgraph-mouse-sweep: layout count 16 0 0.0%
subgraph-mouse-sweep: style recalc count 80 2 2.4%
subgraph-mouse-sweep: task duration 766ms 69ms 9.0%
subgraph-mouse-sweep: script duration 101ms 7ms 6.5%
subgraph-mouse-sweep: TBT 0ms 0ms 0.0%
subgraph-mouse-sweep: DOM nodes 67 2 3.3%
subgraph-mouse-sweep: event listeners 8 4 52.6%
workflow-execution: avg frame time 17ms 0ms 0.0%
workflow-execution: layout duration 2ms 0ms 9.4%
workflow-execution: style recalc duration 24ms 2ms 9.1%
workflow-execution: layout count 5 1 11.0%
workflow-execution: style recalc count 18 2 11.5%
workflow-execution: task duration 123ms 11ms 8.8%
workflow-execution: script duration 29ms 3ms 10.2%
workflow-execution: TBT 0ms 0ms 0.0%
workflow-execution: DOM nodes 161 7 4.4%
workflow-execution: event listeners 52 4 8.4%
Trend (last 15 commits on main)
Metric Trend Dir Latest
canvas-idle: avg frame time ▆▃▆▁▆▃▆█▆▆▄▃▃▄▃ ➡️ 17ms
canvas-idle: p95 frame time ➡️ NaNms
canvas-idle: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
canvas-idle: style recalc duration ▇▇▆▆▃█▄▃▄▃▇▄▁▆▇ ➡️ 11ms
canvas-idle: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
canvas-idle: style recalc count █▃▅▂▅▆▃▁▂▁▂▅▆▅▆ ➡️ 12
canvas-idle: task duration ▃▃▃▆▂▃▃▅▆▂█▃▁▃▃ ➡️ 391ms
canvas-idle: script duration ▄▃▅▇▂▅▃▆▇▅█▄▁▅▆ ➡️ 27ms
canvas-idle: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
canvas-idle: heap used ➡️ NaN MB
canvas-idle: DOM nodes █▇▆▅▃▇▃▁▂▂▅▆▆▆▇ ➡️ 24
canvas-idle: event listeners ▅█▅▄▁▅▁▁▁▄▅▅▁▅▄ 📉 11
canvas-mouse-sweep: avg frame time ▆█▆▃▁▃▁▆▆▁▃▆▆▃▃ ➡️ 17ms
canvas-mouse-sweep: p95 frame time ➡️ NaNms
canvas-mouse-sweep: layout duration ▁▃▂▄▁▂▁▃▆▂█▇▆▄▃ ➡️ 4ms
canvas-mouse-sweep: style recalc duration ▄▄▂▄▁▂▃▃▅▄█▆▂▄▄ ➡️ 43ms
canvas-mouse-sweep: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 12
canvas-mouse-sweep: style recalc count █▅▄▃▂▂▁▄▄▅▆▅▂▇▄ ➡️ 79
canvas-mouse-sweep: task duration █▆▄▂▂▃▂▄▄▅█▆▁▆▄ ➡️ 868ms
canvas-mouse-sweep: script duration ▄▅▄▆▄▆▆▆▅▅█▆▁▅▆ ➡️ 139ms
canvas-mouse-sweep: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
canvas-mouse-sweep: heap used ➡️ NaN MB
canvas-mouse-sweep: DOM nodes █▅▃▃▁▂▂▃▂▄▆▅▃▅▅ ➡️ 64
canvas-mouse-sweep: event listeners █▁▁▁▁▁▇▁▁▁██▇▁█ 📈 13
canvas-zoom-sweep: avg frame time ▅▅█▄▅▁▁▁▅▁▁▅▄▅▁ ➡️ 17ms
canvas-zoom-sweep: p95 frame time ➡️ NaNms
canvas-zoom-sweep: layout duration ▆▅▅▄▁▁█▅▃▅▇▆▁▂▆ ➡️ 1ms
canvas-zoom-sweep: style recalc duration ▆▅▄▆▅▃█▆▇▅▇▄▁▃▅ ➡️ 20ms
canvas-zoom-sweep: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 6
canvas-zoom-sweep: style recalc count ▁▁▃▄▆▃▆█▄▄▆▁▆▁▆ ➡️ 32
canvas-zoom-sweep: task duration ▄▂▁▇▂▂▄▅▆▃█▄▁▁▅ ➡️ 338ms
canvas-zoom-sweep: script duration ▃▃▂▇▂▂▅▇▆▅█▄▁▂▆ ➡️ 30ms
canvas-zoom-sweep: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
canvas-zoom-sweep: heap used ➡️ NaN MB
canvas-zoom-sweep: DOM nodes ▄▃▁▅█▁▃▆▄▅▅▃▃▄▃ ➡️ 79
canvas-zoom-sweep: event listeners ▁▁▂▅█▂▁▅▁▅▅▄▁▅▁ ➡️ 19
dom-widget-clipping: avg frame time ▂▄▅▅▂▄█▇▅▇▇▅▅▁▇ ➡️ 17ms
dom-widget-clipping: p95 frame time ➡️ NaNms
dom-widget-clipping: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
dom-widget-clipping: style recalc duration ▆▆▂▆▄▃██▄▁▆▇▆▃▅ ➡️ 10ms
dom-widget-clipping: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
dom-widget-clipping: style recalc count ▇█▅█▅▄█▇▇▁▇▄▇▂▅ ➡️ 13
dom-widget-clipping: task duration ▃▃▁▅▄▃▅▆▅▂▇█▁▅▅ ➡️ 371ms
dom-widget-clipping: script duration ▅▄▄▆▆▅▇▇▆▃█▇▁▇▇ ➡️ 71ms
dom-widget-clipping: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
dom-widget-clipping: heap used ➡️ NaN MB
dom-widget-clipping: DOM nodes ▇▇▄▇▅▄█▇▅▁▅▄▇▃▄ ➡️ 21
dom-widget-clipping: event listeners ▅▅▅▅▁▅██▁▁▁▁█▁▁ 📉 2
large-graph-idle: avg frame time ▅▅▅▅▅▂▁▂▄▅▄▂▂▅█ ➡️ 17ms
large-graph-idle: p95 frame time ➡️ NaNms
large-graph-idle: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
large-graph-idle: style recalc duration ▅▅▅▆▄▅▃▄▅▅▆█▁▄▆ ➡️ 13ms
large-graph-idle: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
large-graph-idle: style recalc count █▆█▃▃▁▃▆▃▆▆▃▆██ ➡️ 12
large-graph-idle: task duration ▂▃▂▆▂▃▃▇▅▃██▁▂▅ ➡️ 569ms
large-graph-idle: script duration ▄▅▄▆▄▅▅▇▆▅█▆▁▃▆ ➡️ 110ms
large-graph-idle: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
large-graph-idle: heap used ➡️ NaN MB
large-graph-idle: DOM nodes ▆█▅▂▅▃▁▂▃▅▅▆▂▆▅ ➡️ 25
large-graph-idle: event listeners ███▇██▄▁▄▇▇█▂█▇ ➡️ 29
large-graph-pan: avg frame time ▆▃▃▆█▃▁█▆▆▆▆█▁▆ ➡️ 17ms
large-graph-pan: p95 frame time ➡️ NaNms
large-graph-pan: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
large-graph-pan: style recalc duration ▃▂▄▄▁▅▂▂▁▄▄█▃▁▂ ➡️ 17ms
large-graph-pan: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
large-graph-pan: style recalc count ▆▃█▂▃▂▂▂▁▇▅▃█▆▃ ➡️ 69
large-graph-pan: task duration ▄▃▄▆▄▄▄▆▄▄█▆▁▂▅ ➡️ 1100ms
large-graph-pan: script duration ▅▄▅▆▆▅▄▆▄▅█▄▁▄▅ ➡️ 413ms
large-graph-pan: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
large-graph-pan: heap used ➡️ NaN MB
large-graph-pan: DOM nodes ▅▃▆▂▄▁▃▁▁▅▁▂█▅▂ ➡️ 18
large-graph-pan: event listeners █▆█▁▁▆▁▁▃▆▁▃██▃ ➡️ 5
minimap-idle: avg frame time ▃▆▆▃█▁█▆▆▃▃▆█▆█ ➡️ 17ms
minimap-idle: p95 frame time ➡️ NaNms
minimap-idle: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
minimap-idle: style recalc duration ▄█▁█▅▅█▅▅▃▅▁▁▄▆ ➡️ 10ms
minimap-idle: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
minimap-idle: style recalc count ▃▅▂▄█▃▆▁▂▅▂▁▅▆▃ ➡️ 9
minimap-idle: task duration ▃▄▁▅▁▃▄▅▇▃█▅▁▁▅ ➡️ 547ms
minimap-idle: script duration ▄▆▃▇▃▅▆▆▇▅█▅▁▃▆ ➡️ 106ms
minimap-idle: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
minimap-idle: heap used ➡️ NaN MB
minimap-idle: DOM nodes ▃▅▂▄█▃▆▁▂▅▂▁▅▆▃ ➡️ 19
minimap-idle: event listeners ▃▃▆▁▁▁▃▁▁▆▁▃█▆▁ ➡️ 4
subgraph-dom-widget-clipping: avg frame time ▅▄▄▄▄▄█▄▄▄▃▁▆▃▃ ➡️ 17ms
subgraph-dom-widget-clipping: p95 frame time ➡️ NaNms
subgraph-dom-widget-clipping: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-dom-widget-clipping: style recalc duration ▂▄▃▅▅▃▂▅▇▃▄█▁▄▆ ➡️ 14ms
subgraph-dom-widget-clipping: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
subgraph-dom-widget-clipping: style recalc count ▇█▆▃▆▃▁▆█▇▃▆▇█▅ ➡️ 48
subgraph-dom-widget-clipping: task duration ▂▃▃▆▅▅▂▅█▂▆█▁▂▇ ➡️ 398ms
subgraph-dom-widget-clipping: script duration ▃▃▃▄▅▅▂▄█▂▅▇▁▂▅ ➡️ 131ms
subgraph-dom-widget-clipping: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-dom-widget-clipping: heap used ➡️ NaN MB
subgraph-dom-widget-clipping: DOM nodes ▅▇▅▂▅▂▁▅▅▅▁▇▅█▄ ➡️ 22
subgraph-dom-widget-clipping: event listeners ▅▅▅▂▅▁▅██▁▁█▅█▅ 📈 16
subgraph-idle: avg frame time ▆▆█▁▆▃▆▆▆▃▆▁▃▆█ ➡️ 17ms
subgraph-idle: p95 frame time ➡️ NaNms
subgraph-idle: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-idle: style recalc duration ▁▇▃▆▂▄▂▃▃▆▆▄▃▇█ ➡️ 12ms
subgraph-idle: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
subgraph-idle: style recalc count ▃▆▃▃▂▅▁▂▁▆▃▃██▇ ➡️ 12
subgraph-idle: task duration ▁▃▁▇▁▁▃▆▅▂█▅▁▁▄ ➡️ 378ms
subgraph-idle: script duration ▁▃▂▇▁▂▃▇▆▂█▅▂▁▅ ➡️ 22ms
subgraph-idle: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-idle: heap used ➡️ NaN MB
subgraph-idle: DOM nodes ▃▅▃▂▁▄▁▂▁▅▃▂▇█▇ ➡️ 24
subgraph-idle: event listeners ▁▅▁▁▁▁▁▁▁▅▄▁███ 📈 21
subgraph-mouse-sweep: avg frame time ▅▄▁▃▃▄▆▄▆▃▃█▁▃▃ ➡️ 17ms
subgraph-mouse-sweep: p95 frame time ➡️ NaNms
subgraph-mouse-sweep: layout duration ▁▄▄▄▃▃▅▅▅▂█▇▂▃▆ ➡️ 5ms
subgraph-mouse-sweep: style recalc duration ▃▂▄▅▂▃▄▅█▃█▆▁▂▅ ➡️ 43ms
subgraph-mouse-sweep: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 16
subgraph-mouse-sweep: style recalc count ▅▂▅▅▁▄▃▅█▅▆▄▂▄▅ ➡️ 81
subgraph-mouse-sweep: task duration ▃▂▄▅▂▄▄▅▇▄█▆▁▃▅ ➡️ 785ms
subgraph-mouse-sweep: script duration ▄▅▄▇▅▅▆▇▆▅██▁▄▆ ➡️ 105ms
subgraph-mouse-sweep: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-mouse-sweep: heap used ➡️ NaN MB
subgraph-mouse-sweep: DOM nodes ▅▁▄▅▁▄▃▃█▅▅▄▂▅▃ ➡️ 66
subgraph-mouse-sweep: event listeners ▇▁▂▇▁▂▂▂█▇▂▂▇▇▂ 📈 5
workflow-execution: avg frame time ▆▆▆▄▆▆▃▄▁▄█▆▅▄▆ ➡️ 17ms
workflow-execution: p95 frame time ➡️ NaNms
workflow-execution: layout duration ▁▆▁▃▂▄▃▂▃▃▅█▄▂▅ ➡️ 2ms
workflow-execution: style recalc duration ▃▇▅▇▁▅▆▇█▁██▂▄▆ ➡️ 25ms
workflow-execution: layout count ▁█▂▃▂▃▃▁▃▃▄▃▂▃▂ ➡️ 5
workflow-execution: style recalc count ▃█▅▇▁▄▅▆▅▅▅▅▄▄▂ ➡️ 15
workflow-execution: task duration ▂▅▄▅▁▄▆▆▆▁▇█▁▃▃ ➡️ 120ms
workflow-execution: script duration ▄▃▄▄▃▅▄▅▆▂▇█▁▃▄ ➡️ 29ms
workflow-execution: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
workflow-execution: heap used ➡️ NaN MB
workflow-execution: DOM nodes ▂█▃▆▁▄▃▅▃█▃▃▄▃▁ ➡️ 152
workflow-execution: event listeners ▅███▁▅███▁██▅█▅ ➡️ 49
Raw data
{
  "timestamp": "2026-05-18T22:11:29.981Z",
  "gitSha": "f243e390baad71731ae9b3ea142c1f1e4a36571c",
  "branch": "deep/fix-survey-gate-false-positives",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2030.4360000000088,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 10.661,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 365.359,
      "heapDeltaBytes": 24307884,
      "heapUsedBytes": 73484032,
      "domNodes": 22,
      "jsHeapTotalBytes": 15466496,
      "scriptDurationMs": 24.651000000000003,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-idle",
      "durationMs": 2035.8019999999897,
      "styleRecalcs": 12,
      "styleRecalcDurationMs": 11.531,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 489.762,
      "heapDeltaBytes": -10404768,
      "heapUsedBytes": 58030948,
      "domNodes": -258,
      "jsHeapTotalBytes": 19877888,
      "scriptDurationMs": 26.817999999999994,
      "eventListeners": -133,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1896.016999999972,
      "styleRecalcs": 74,
      "styleRecalcDurationMs": 43.03999999999999,
      "layouts": 12,
      "layoutDurationMs": 3.92,
      "taskDurationMs": 879.9,
      "heapDeltaBytes": 1765920,
      "heapUsedBytes": 49761208,
      "domNodes": -261,
      "jsHeapTotalBytes": 14544896,
      "scriptDurationMs": 144.59199999999998,
      "eventListeners": -133,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2074.3780000000243,
      "styleRecalcs": 81,
      "styleRecalcDurationMs": 48.12000000000001,
      "layouts": 12,
      "layoutDurationMs": 3.9190000000000005,
      "taskDurationMs": 1098.8829999999998,
      "heapDeltaBytes": -13760780,
      "heapUsedBytes": 54610256,
      "domNodes": -260,
      "jsHeapTotalBytes": 25382912,
      "scriptDurationMs": 142.892,
      "eventListeners": -133,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1750.9769999999776,
      "styleRecalcs": 32,
      "styleRecalcDurationMs": 20.258,
      "layouts": 6,
      "layoutDurationMs": 0.7370000000000002,
      "taskDurationMs": 351.126,
      "heapDeltaBytes": 522600,
      "heapUsedBytes": 49132184,
      "domNodes": 79,
      "jsHeapTotalBytes": 14942208,
      "scriptDurationMs": 25.275,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1716.3560000000189,
      "styleRecalcs": 30,
      "styleRecalcDurationMs": 17.226,
      "layouts": 6,
      "layoutDurationMs": 0.5409999999999998,
      "taskDurationMs": 325.519,
      "heapDeltaBytes": 23069072,
      "heapUsedBytes": 73319736,
      "domNodes": 75,
      "jsHeapTotalBytes": 24117248,
      "scriptDurationMs": 19.196,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 589.5329999999603,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 6.2810000000000015,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 360.27299999999997,
      "heapDeltaBytes": 9182620,
      "heapUsedBytes": 57678104,
      "domNodes": 12,
      "jsHeapTotalBytes": 15204352,
      "scriptDurationMs": 62.114,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 546.5020000000322,
      "styleRecalcs": 12,
      "styleRecalcDurationMs": 8.21,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 340.25300000000004,
      "heapDeltaBytes": -9279764,
      "heapUsedBytes": 58738724,
      "domNodes": 20,
      "jsHeapTotalBytes": 19136512,
      "scriptDurationMs": 62.586000000000006,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2032.9940000000306,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 9.489999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 589.062,
      "heapDeltaBytes": -2002540,
      "heapUsedBytes": 57634088,
      "domNodes": -263,
      "jsHeapTotalBytes": 5324800,
      "scriptDurationMs": 102.04199999999999,
      "eventListeners": -131,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2067.877000000067,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 10.965,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 722.3459999999999,
      "heapDeltaBytes": 45359348,
      "heapUsedBytes": 102603068,
      "domNodes": -258,
      "jsHeapTotalBytes": 33579008,
      "scriptDurationMs": 123.232,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2166.9899999999984,
      "styleRecalcs": 69,
      "styleRecalcDurationMs": 19.998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1267.1650000000002,
      "heapDeltaBytes": -5880296,
      "heapUsedBytes": 53989136,
      "domNodes": -263,
      "jsHeapTotalBytes": 6316032,
      "scriptDurationMs": 455.897,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2169.563000000039,
      "styleRecalcs": 69,
      "styleRecalcDurationMs": 20.442,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1265.987,
      "heapDeltaBytes": 50023284,
      "heapUsedBytes": 109613260,
      "domNodes": -268,
      "jsHeapTotalBytes": 40599552,
      "scriptDurationMs": 427.954,
      "eventListeners": -159,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3246.8110000000365,
      "styleRecalcs": 64,
      "styleRecalcDurationMs": 18.687999999999995,
      "layouts": 60,
      "layoutDurationMs": 7.755999999999999,
      "taskDurationMs": 1464.48,
      "heapDeltaBytes": 45559164,
      "heapUsedBytes": 106830112,
      "domNodes": -269,
      "jsHeapTotalBytes": 43016192,
      "scriptDurationMs": 511.861,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3136.12999999998,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 18.922000000000004,
      "layouts": 60,
      "layoutDurationMs": 7.622,
      "taskDurationMs": 1393.577,
      "heapDeltaBytes": 7892560,
      "heapUsedBytes": 69017684,
      "domNodes": -265,
      "jsHeapTotalBytes": 6316032,
      "scriptDurationMs": 503.7010000000001,
      "eventListeners": -125,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "minimap-idle",
      "durationMs": 2079.7580000000266,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 8.41,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 740.0179999999998,
      "heapDeltaBytes": 39660060,
      "heapUsedBytes": 100854988,
      "domNodes": -263,
      "jsHeapTotalBytes": 32792576,
      "scriptDurationMs": 116.727,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "minimap-idle",
      "durationMs": 2016.845000000103,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 7.889999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 549.969,
      "heapDeltaBytes": 10966732,
      "heapUsedBytes": 71770784,
      "domNodes": -265,
      "jsHeapTotalBytes": 286720,
      "scriptDurationMs": 96.78499999999998,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 579.2059999999992,
      "styleRecalcs": 47,
      "styleRecalcDurationMs": 12.328000000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 391.166,
      "heapDeltaBytes": -9599252,
      "heapUsedBytes": 59044988,
      "domNodes": 20,
      "jsHeapTotalBytes": 19398656,
      "scriptDurationMs": 128.963,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.663333333333338,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 604.5950000000175,
      "styleRecalcs": 48,
      "styleRecalcDurationMs": 12.622000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 421.783,
      "heapDeltaBytes": -13233588,
      "heapUsedBytes": 55242244,
      "domNodes": -237,
      "jsHeapTotalBytes": 4411392,
      "scriptDurationMs": 124.64800000000001,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-idle",
      "durationMs": 1990.1249999999777,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 11.49,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 381.856,
      "heapDeltaBytes": 1553688,
      "heapUsedBytes": 67940628,
      "domNodes": 22,
      "jsHeapTotalBytes": 19488768,
      "scriptDurationMs": 21.679,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "subgraph-idle",
      "durationMs": 1989.3680000000131,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 8.293999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 320.639,
      "heapDeltaBytes": 23044944,
      "heapUsedBytes": 71968796,
      "domNodes": 18,
      "jsHeapTotalBytes": 14680064,
      "scriptDurationMs": 11.347000000000001,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1967.7760000000148,
      "styleRecalcs": 85,
      "styleRecalcDurationMs": 50.615,
      "layouts": 16,
      "layoutDurationMs": 5.3740000000000006,
      "taskDurationMs": 950.216,
      "heapDeltaBytes": 14495620,
      "heapUsedBytes": 62931956,
      "domNodes": 74,
      "jsHeapTotalBytes": 15204352,
      "scriptDurationMs": 110.03500000000001,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1750.9129999999686,
      "styleRecalcs": 77,
      "styleRecalcDurationMs": 37.648999999999994,
      "layouts": 16,
      "layoutDurationMs": 4.5120000000000005,
      "taskDurationMs": 770.8170000000001,
      "heapDeltaBytes": 13196296,
      "heapUsedBytes": 62458480,
      "domNodes": -261,
      "jsHeapTotalBytes": 19263488,
      "scriptDurationMs": 102.87,
      "eventListeners": -131,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-transition-enter",
      "durationMs": 932.7190000000201,
      "styleRecalcs": 15,
      "styleRecalcDurationMs": 26.528999999999996,
      "layouts": 4,
      "layoutDurationMs": 12.851999999999999,
      "taskDurationMs": 752.6780000000002,
      "heapDeltaBytes": 30310952,
      "heapUsedBytes": 95723852,
      "domNodes": 13513,
      "jsHeapTotalBytes": 16777216,
      "scriptDurationMs": 27.90100000000001,
      "eventListeners": 2529,
      "totalBlockingTimeMs": 160,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8174.472999999978,
      "styleRecalcs": 249,
      "styleRecalcDurationMs": 53.114,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 3962.893,
      "heapDeltaBytes": 13658208,
      "heapUsedBytes": 71772864,
      "domNodes": -264,
      "jsHeapTotalBytes": 6782976,
      "scriptDurationMs": 1286.689,
      "eventListeners": -113,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8143.6089999999695,
      "styleRecalcs": 251,
      "styleRecalcDurationMs": 54.855000000000004,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 3847.9780000000005,
      "heapDeltaBytes": 21094088,
      "heapUsedBytes": 84355128,
      "domNodes": 20,
      "jsHeapTotalBytes": 22077440,
      "scriptDurationMs": 1322.906,
      "eventListeners": 20,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.80000000000109
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 12975.528999999995,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 12960.427,
      "heapDeltaBytes": -36283292,
      "heapUsedBytes": 170512384,
      "domNodes": -8331,
      "jsHeapTotalBytes": 21032960,
      "scriptDurationMs": 626.986,
      "eventListeners": -16464,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 15751.892999999996,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 15712.486000000003,
      "heapDeltaBytes": 140982468,
      "heapUsedBytes": 380855328,
      "domNodes": -3306,
      "jsHeapTotalBytes": 154374144,
      "scriptDurationMs": 709.021,
      "eventListeners": -16480,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 15115.257000000041,
      "styleRecalcs": 71,
      "styleRecalcDurationMs": 19.517000000000007,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 15091.710000000003,
      "heapDeltaBytes": -25753736,
      "heapUsedBytes": 169424116,
      "domNodes": -8331,
      "jsHeapTotalBytes": -1511424,
      "scriptDurationMs": 896.7829999999999,
      "eventListeners": -16460,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 18.333333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 14525.679999999966,
      "styleRecalcs": 65,
      "styleRecalcDurationMs": 17.533999999999995,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 14495.566,
      "heapDeltaBytes": -40934152,
      "heapUsedBytes": 162153872,
      "domNodes": -8331,
      "jsHeapTotalBytes": 22605824,
      "scriptDurationMs": 883.931,
      "eventListeners": -16464,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.223333333333358,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "workflow-execution",
      "durationMs": 445.1930000000175,
      "styleRecalcs": 17,
      "styleRecalcDurationMs": 23.011999999999997,
      "layouts": 5,
      "layoutDurationMs": 1.44,
      "taskDurationMs": 123.88800000000002,
      "heapDeltaBytes": 5420368,
      "heapUsedBytes": 57051204,
      "domNodes": 168,
      "jsHeapTotalBytes": 0,
      "scriptDurationMs": 28.178,
      "eventListeners": 69,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "workflow-execution",
      "durationMs": 462.9520000000866,
      "styleRecalcs": 17,
      "styleRecalcDurationMs": 24.491000000000003,
      "layouts": 4,
      "layoutDurationMs": 1.139,
      "taskDurationMs": 126.64600000000002,
      "heapDeltaBytes": 5072956,
      "heapUsedBytes": 56289964,
      "domNodes": 157,
      "jsHeapTotalBytes": 524288,
      "scriptDurationMs": 24.985,
      "eventListeners": 69,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.669999999999998,
      "p95FrameDurationMs": 16.700000000000273
    }
  ]
}

Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
src/platform/cloud/onboarding/auth.ts (1)

144-166: ⚡ Quick win

Consider extracting the error prefix to a constant to avoid silent breakage.

The error detection at line 149 relies on a string prefix that must match line 126 exactly. If someone edits one without the other, auth errors will be swallowed instead of re-thrown.

♻️ Proposed refactor
+const SURVEY_AUTH_ERROR_PREFIX = 'Survey status auth error:'
+
 export async function getSurveyCompletedStatus(): Promise<boolean> {
   try {
     ...
     if (response.status === 401 || response.status === 403) {
       throw new Error(
-        `Survey status auth error: ${response.status} ${response.statusText}`
+        `${SURVEY_AUTH_ERROR_PREFIX} ${response.status} ${response.statusText}`
       )
     }
     ...
   } catch (error) {
     if (
       error instanceof Error &&
-      error.message.startsWith('Survey status auth error:')
+      error.message.startsWith(SURVEY_AUTH_ERROR_PREFIX)
     ) {
       throw error
     }
     ...
   }
 }
🤖 Prompt for 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.

In `@src/platform/cloud/onboarding/auth.ts` around lines 144 - 166, The string
literal used to detect auth errors in the catch block
(error.message.startsWith('Survey status auth error:')) is brittle; extract that
prefix into a shared constant (e.g., SURVEY_STATUS_AUTH_ERROR_PREFIX) declared
alongside the function that throws the error so both the throw site and the
catch site reference the same symbol, then replace the literal in the catch with
that constant and update the throw to use it as well to prevent silent breakage
when the message changes.
🤖 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.

Nitpick comments:
In `@src/platform/cloud/onboarding/auth.ts`:
- Around line 144-166: The string literal used to detect auth errors in the
catch block (error.message.startsWith('Survey status auth error:')) is brittle;
extract that prefix into a shared constant (e.g.,
SURVEY_STATUS_AUTH_ERROR_PREFIX) declared alongside the function that throws the
error so both the throw site and the catch site reference the same symbol, then
replace the literal in the catch with that constant and update the throw to use
it as well to prevent silent breakage when the message changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 213245c1-f1c0-4f37-b06b-46418ba8f819

📥 Commits

Reviewing files that changed from the base of the PR and between 7160a9e and c7abc90.

📒 Files selected for processing (2)
  • src/platform/cloud/onboarding/auth.ts
  • src/router.ts

@codecov
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

@@             Coverage Diff             @@
##             main   #12301       +/-   ##
===========================================
- Coverage   74.10%   59.69%   -14.41%     
===========================================
  Files        1522     1412      -110     
  Lines       88026    72107    -15919     
  Branches    24660    20010     -4650     
===========================================
- Hits        65230    43045    -22185     
- Misses      21972    28589     +6617     
+ Partials      824      473      -351     
Flag Coverage Δ
e2e ?
unit 59.69% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/platform/cloud/onboarding/auth.ts 24.44% <100.00%> (+24.44%) ⬆️

... and 1015 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@deepme987
Copy link
Copy Markdown
Contributor Author

PR Review (cloud-leaning eyes)

Overall Assessment

Approve — pure frontend change with no auth/middleware/API-contract drift. A couple of cloud-side observations worth filing as follow-ups, plus one nuance in the README trade-off table that's tighter than the actual backend contract.

1. Cloud backend contract — the 404 trade-off is narrower than the PR description suggests

The PR description frames the 404 case as covering both "key absent for existing user" and "user has no settings yet" / brand-new user. Looking at the live handler (cloud/services/ingest/server/implementation/settings.go::GetSettingByKey), the contract is actually:

// User row not found:
if database.IsNotFound(err) {
    return ingest.GetSettingByKey401JSONResponse{Code: "UNAUTHORIZED", ...}
}
// DB error:
if err != nil {
    return ingest.GetSettingByKey401JSONResponse{Code: "SERVER_ERROR", ...}
}
// Key not present in Settings JSON, and no default registered:
return ingest.GetSettingByKey404JSONResponse{Code: "NOT_FOUND", ...}

So 404 only ever fires when the User row exists but the onboarding_survey key isn't in their Settings JSON. A brand-new user who just signed up still has a User row, so they will hit the 404 path. The trade-off is real but it's strictly the "existing user, no survey key" case — not "user not found". Worth tightening the wording for posterity, but no code change.

2. The 401 path is wider than expected — SERVER_ERROR from DB hiccups returns 401

Same handler: an unrelated DB failure returns 401 with Code: "SERVER_ERROR". With this PR's new resolution rules, that surfaces as a thrown auth error inside getSurveyCompletedStatus. The router catch branch handles it safely (sets the session flag and proceeds), so this is not a bug — but it does mean a DB blip on the user lookup will reach the new throw path, not the "treat as completed via 5xx" path. The behavior is still correct (user proceeds, no bounce), but the code comment in auth.ts saying "401/403 → auth issue" is slightly imprecise. Cheap follow-up: have the cloud handler return 500 on the DB-error branch and reserve 401 for auth-only failures. I'd file that as a backend ticket rather than block this PR.

3. Auth middleware sanity check — no behavioral drift

Verified the endpoint is on apiWaitlisted (Protected() only — no requireEmailVerified, no requireWhitelist), unchanged. The PR doesn't touch any auth group membership. Survey responses are intentionally accessible to waitlisted users since onboarding writes there. Nothing to flag.

4. Backend follow-up (the PR already mentions this)

The clean recovery for the new-user signal — return 200 with value: null when the User row exists but the key is absent — is the right call and matches how every other consumer expects "key absent" to look. Happy to take that as a cloud ticket. With that in place, the frontend could move back to a stricter "only treat 200-empty as 'not completed'" and drop the 404 ambiguity entirely.

Minor

  • The new sessionStorage flag is a clean fit for this use case — survives window.location.reload() (token-refresh recovery, GraphCanvas 401 path) but resets per tab. Good call over localStorage, which would have stuck across sign-outs.
  • The catch branch no longer bouncing to /cloud-user-check is the right move — that view loads UserCheckView.vue which calls back into the same endpoints, exactly the redirect loop the PR description calls out.
  • The Sentry breadcrumb with initial_load: _from.name === undefined is going to be useful for next-incident triage.

@deepme987
Copy link
Copy Markdown
Contributor Author

Review: fix(cloud): stop bouncing working users to /cloud/survey mid-session

The logic in getSurveyCompletedStatus is the heart of this fix — it now has five distinct exit paths (200+value, 200+empty, 401/403 throw, 404/5xx true, network true). None of them are tested.

Must Fix

  • src/platform/cloud/onboarding/auth.ts -- No tests for getSurveyCompletedStatus. Add auth.test.ts covering the five response branches. Minimum set:
// 200 non-empty → true
mockFetch({ ok: true, status: 200, json: () => ({ value: { q1: 'a' } }) })
expect(await getSurveyCompletedStatus()).toBe(true)

// 200 empty value → false
mockFetch({ ok: true, status: 200, json: () => ({ value: {} }) })
expect(await getSurveyCompletedStatus()).toBe(false)

// 404 → true (not a redirect)
mockFetch({ ok: false, status: 404 })
expect(await getSurveyCompletedStatus()).toBe(true)

// 5xx → true (not a redirect)
mockFetch({ ok: false, status: 503 })
expect(await getSurveyCompletedStatus()).toBe(true)

// 401 → throws (not swallowed as true)
mockFetch({ ok: false, status: 401 })
await expect(getSurveyCompletedStatus()).rejects.toThrow()

// network error → true
mockFetch(() => { throw new Error('Failed to fetch') })
expect(await getSurveyCompletedStatus()).toBe(true)

The 401-throws path is the most dangerous one to ship untested — if the string-prefix catch in the catch block is ever off by a character, a 401 silently returns true and the auth layer never handles it.

  • src/platform/cloud/onboarding/auth.ts lines 126 + 149 -- The catch-block re-throw check uses error.message.startsWith('Survey status auth error:') matching a string literal at the throw site. Extract to a module-level const:
const SURVEY_AUTH_ERROR_PREFIX = 'Survey status auth error:'

Use it at both the throw and the startsWith check. This is load-bearing for correctness (CodeRabbit flagged it too) and would be caught immediately by the 401 test above.

Note on sessionStorage gate

The SURVEY_GATE_SESSION_KEY logic in router.ts is correct conceptually but untestable at the unit level without a full router harness. Fine to leave without unit tests if the manual test plan covers it — but document that explicitly in the PR so the next reader doesn't wonder why router.ts has no spec.

The fix itself is the right call. Ship it once auth.test.ts exists.

@deepme987
Copy link
Copy Markdown
Contributor Author

Drive-by from a cloud lens. The auth/billing/contract surface is fine — no subscription/credits/usage touchpoints, the 5xx-as-completed tradeoff is documented and reasonable, and propagating 401/403 instead of masquerading it as ''no survey'' is strictly better.

One real concern:

sessionStorage is not cleared on logout, so the gate skip leaks across users in the same tab. useFirebaseAuthActions.logout ends with window.location.href = '/cloud/login' — a same-tab nav, which preserves sessionStorage. If user A completes (or is bounced through) the gate, logs out, and user B logs in in the same tab, user B's first nav to / will hit sessionStorage.getItem(SURVEY_GATE_SESSION_KEY) and short-circuit next() without ever checking B's survey status. New-user-on-shared-machine is the obvious case; the team-plan account-switching case is the more likely one in practice.

Two ways out:

  • sessionStorage.removeItem(SURVEY_GATE_SESSION_KEY) in the logout path (cheapest)
  • key the flag by uid: comfy.survey_gate_evaluated:<uid> so a different user gets a fresh evaluation automatically

The second is more robust to logout paths we forget about.

Smaller note: UserCheckView.vue calls getSurveyCompletedStatus() inside useAsyncState + wrapWithErrorHandlingAsync. Previously a 401/403 returned false and silently routed to /cloud/survey; now it throws and surfaces the ''Unable to check account status'' error UI with a Retry button. That's better behavior, but it is a user-visible change for the auth-expired case — worth a line in the test plan to confirm the retry UX is what you want there.

@deepme987
Copy link
Copy Markdown
Contributor Author

Drive-by review from cloud side — focused on data correctness / null safety / guard edges. The mid-session bounce fix is the right call; flagging a few edges that look load-bearing.

Must Fix

  • router.ts:226SURVEY_GATE_SESSION_KEY is not scoped to user. logout does window.location.href = '/cloud/login' (same-tab nav), so sessionStorage survives logout. If User A completes the gate and signs out, User B signing in on the same tab will skip the survey gate forever (until tab close). The flag should either be cleared on logout or keyed by user id (e.g. comfy.survey_gate_evaluated:<userId>). Given the team-plan / shared-machine context, this seems likely to bite.

  • auth.ts:155 — caller-divergence on CloudSurveyView.vue. CloudSurveyView.onMounted also calls getSurveyCompletedStatus(), and on true does router.replace({ name: 'cloud-user-check' }). With the new "ambiguous → true" policy, a 5xx hiccup while the user is on /cloud/survey now silently boots them off the survey screen to user-check. Combined with the router gate having already set the session flag, a transient backend blip during initial signup means the user never sees the survey at all (gate skipped + survey view self-redirects). Either:

    • Have CloudSurveyView distinguish "definitely completed" from "ambiguous" (e.g. return a tri-state from getSurveyCompletedStatus, or have the survey view hit a different endpoint / accept the ambiguous case as "stay here"), or
    • At minimum, don't auto-redirect away from /cloud/survey unless the response was an unambiguous 200-with-non-empty-value.

Should Fix

  • auth.ts:148 — auth error detected by message-string prefix. error.message.startsWith('Survey status auth error:') couples the throw site to the catch site by stringly-typed contract. A trivial rename of the error message in 6 months silently flips behavior from "propagate auth error" to "swallow + treat as completed". A tiny class SurveyAuthError extends Error (or an error.cause discriminant) makes the intent obvious and survives refactors.

  • router.ts:232sessionStorage can throw. Strict cookie/storage policies and some embedded webviews throw SecurityError on sessionStorage.getItem/setItem. Right now those throws escape beforeEach and reject the navigation entirely. Wrap in a try/catch — on failure, fall through to the old behavior (re-evaluate every nav) rather than breaking routing.

Notes

  • The PR description's brand-new-user trade-off is clear and the right call given the 7k-users/day Datadog signal. The follow-up backend fix (200 with value: null to distinguish absent-key from absent-user) is the clean answer — happy to take that on the cloud side if you file the ticket.

  • isEmpty(data.value) from es-toolkit/compat returns true for 0, false, '', {}, [], null, undefined. If the survey payload could ever be a scalar (e.g. backend returns { value: false } to mean "declined"), that'd read as "not completed" and re-bounce. Worth a comment pinning the expected shape, or a stricter check like typeof data.value === 'object' && data.value !== null && Object.keys(data.value).length > 0.

  • Nit: the treated_as: 'completed' breadcrumb in the catch block fires for all non-auth errors including JSON parse failures. Useful, but worth tagging error_kind so you can tell parse-failure from network-failure when this fires in prod.

@deepme987
Copy link
Copy Markdown
Contributor Author

Review: fix(cloud): stop bouncing working users to /cloud/survey mid-session

Overall the policy change is sound and the trade-off is documented well. A few correctness/cleanliness issues before this can come out of draft.

Must Fix

  • src/platform/cloud/onboarding/auth.ts:131-138 — Magic-string error matching with error.message.startsWith('Survey status auth error:') to re-throw is fragile. Any future copy-edit to the thrown message silently breaks the auth-propagation contract. There is already a helper for exactly this in this same file (line 47): isHttpError(error, prefix). Use it, or better, throw a tagged error class (class SurveyAuthError extends Error) and instanceof-check it. The string-prefix coupling between the throw site (line 116) and the catch site (line 137) is precisely the kind of thing that breaks during refactors.

  • src/platform/cloud/onboarding/UserCheckView.vue:61-64 and src/platform/cloud/onboarding/CloudSurveyView.vue:255 — These two call sites also consume getSurveyCompletedStatus() and assumed the old contract (returns false on failure, never throws on HTTP errors). Under the new contract:

    • UserCheckView's Promise.all will now reject on 401/403 and surface an error to the user with the retry button. Previously it resolved with surveyStatus=false and redirected to /cloud/survey. Verify this is the desired UX for stale-token recovery — particularly because /cloud-user-check is itself the page we render after token refresh.
    • CloudSurveyView on-mount already wraps in try/catch and just logs, so it tolerates the new throw, but the user lands on the survey form with no signal of an auth failure. Worth a Sentry breadcrumb at minimum.

    At a minimum, please add inline tests covering all five branches (200/empty, 200/value, 404, 401, 5xx) of getSurveyCompletedStatus — there are currently no tests for auth.ts and you're changing a five-way decision matrix that gates billing-bearing customers.

  • src/router.ts:5 — The SURVEY_GATE_SESSION_KEY is never cleared. firebaseAuthStore.ts:115-121 clears all workspace sessionStorage keys on logout, but not this one. Result: if a logged-in-and-surveyed user logs out and a different user logs in on the same tab (without closing the tab), the new user's survey gate is skipped for the rest of the tab session. Add sessionStorage.removeItem(SURVEY_GATE_SESSION_KEY) to the logout cleanup block in firebaseAuthStore.ts:115-121, and export the key from a constants module so it's not a stringly-typed orphan.

Should Fix

  • src/router.ts:5-9 — The constant lives in router.ts but the cleanup site is stores/firebaseAuthStore.ts. Either co-locate with WORKSPACE_STORAGE_KEYS in src/platform/workspace/workspaceConstants.ts (similar sessionStorage lifecycle), or create src/platform/cloud/onboarding/surveyGateStorage.ts with markGateEvaluated()/hasGateBeenEvaluated()/clearGateState() helpers. Either way, the literal 'comfy.survey_gate_evaluated_this_session' should not be a free-floating string. Bonus: easier to mock in tests.

  • src/router.ts:222-227 — The catch branch now silently console.errors and falls through to next() (since there's no return after the setItem). That's the right behavior per the PR description, but the implicit fall-through to the bottom return next() is non-obvious. Add an explicit return next() inside the catch, or at least a comment that says "intentional fall-through to default next() below."

  • src/platform/cloud/onboarding/auth.ts:155-156 — The treated_as: 'completed' extra is added inside extra: {} but the existing route_template/route_actual extras are already there. The breadcrumb at lines 134-147 also has treated_as: 'completed'. Consider extracting to a small helper or constant so the two decision points share one definition.

  • src/router.ts:248-250 — Setting the sessionStorage flag in the catch branch is right, but the user is now allowed to navigate through / with an unknown survey state. If the auth layer's re-auth on the next API call succeeds and the survey actually was unfilled, the user has effectively bypassed the gate until they open a new tab. Acceptable per the documented trade-off, but worth an explicit Sentry breadcrumb at the catch site (currently only console.error), parallel to the breadcrumb at line 246. Datadog won't pick up the console-only path.

Already Exists

  • isHttpError helper at line 47 of the same auth.ts file is the pattern this PR should be using for the auth-error re-throw (see Must Fix Bump braces from 3.0.2 to 3.0.3 in /tests-ui #1).
  • The codebase already uses useStorage from @vueuse/core for reactive sessionStorage (workspaceAuthStore.ts, useFeatureUsageTracker.ts, versionCompatibilityStore.ts). For a one-shot router-guard check raw sessionStorage is reasonable, so not blocking — but if you wrap in a helper module per Should Fix Bump braces from 3.0.2 to 3.0.3 in /tests-ui #1, consider useStorage for consistency with the rest of the platform.

Nits

  • src/platform/cloud/onboarding/auth.ts:114-115 — The block comment about the 404 case is excellent. Consider extracting the policy table from the PR description into a JSDoc @returns table on the function — that information is high-value for the next person debugging an incident and PR descriptions are not greppable.
  • The hardcoded session key prefix comfy. (lowercase) is inconsistent with the rest of the codebase which uses Comfy.Workspace.* (PascalCase + namespace). Pick one.

Verdict

Directionally correct and the documented trade-off is reasonable. Holding on the magic-string error matching, the unmocked behavior change in the two other callers, and the logout-cleanup gap before this is ready to merge. Tests for the five-branch resolution matrix are non-negotiable given this gates paying customers.

@deepme987
Copy link
Copy Markdown
Contributor Author

Review: fix(cloud): stop bouncing working users to /cloud/survey mid-session

Pulling on this carefully because it touches the auth/onboarding hot path and the semantic of getSurveyCompletedStatus() changed for three call sites, not just the router gate. The intent is right and the user-facing trade-off is clearly documented, but the policy change isn't fully wired through.

Issues (must fix)

  • src/router.ts:264-271 — issue: the catch swallows the very auth error that auth.ts was just changed to throw. getSurveyCompletedStatus() now only throws on 401/403 (everything else resolves to a boolean). So this catch branch fires almost exclusively on auth failures, then (a) marks the session as evaluated for the entire tab and (b) calls next() at line 230. That means a single transient 401 at gate time silently disables the survey gate for the whole session and lets the user proceed past /. Per the auth.ts comment ("propagate so the auth layer can handle"), this should redirect to cloud-login (or at minimum not set SURVEY_GATE_SESSION_KEY, so the gate re-evaluates once the token refresh lands). As written, an expired token at the wrong moment is indistinguishable from a clean pass.

  • src/platform/cloud/onboarding/UserCheckView.vue:61-78 — issue: this caller was not updated for the new throw semantics. It runs Promise.all([getUserCloudStatus(), getSurveyCompletedStatus()]), and a 401 from the survey endpoint will now reject the whole Promise.all and surface as error in useAsyncState. Previously the same 401 returned false and routed to cloud-survey. Worse, on a transient 5xx the old behavior also routed to cloud-survey (which is one of the symptoms this PR is fixing); the new behavior returns true and falls through to globalThis.location.href = '/' — which then re-enters the router gate. That gate now sees SURVEY_GATE_SESSION_KEY unset (UserCheckView never set it), re-checks, and we're back where we started. The policy change should be applied consistently or this caller needs to opt into the old false-on-error semantics.

  • src/platform/cloud/onboarding/CloudSurveyView.vue:254-267 — issue: same caller-not-updated bug, lower blast radius. The onMounted survey check used to return false on any non-200 and proceed to render the survey form. With this PR, a 401 throws (caught and logged, OK) but a transient 5xx returns true and routes the user away from the survey to cloud-user-check — even when they genuinely haven't completed it. The user landed on /cloud/survey to fill it out and gets bounced back to user-check on a backend hiccup.

  • src/platform/cloud/onboarding/auth.ts:127-133 — issue: branding the auth error via error.message.startsWith('Survey status auth error:') is a stringly-typed sentinel that breaks under any wrapping (e.g., a future cause:, a sentry beforeSend mutation, i18n). Use a discriminator: either a custom class SurveyAuthError extends Error (callers instanceof-check), or re-throw a typed object { kind: 'auth', status }. The current pattern silently downgrades to "completed" if anyone ever changes that message string, which is exactly the false-positive class this PR is trying to prevent.

  • src/router.ts:8-9 — issue: SURVEY_GATE_SESSION_KEY = 'comfy.survey_gate_evaluated_this_session' doesn't follow the workspace convention. src/platform/workspace/workspaceConstants.ts uses Comfy.Workspace.* PascalCase. New sessionStorage keys should live in a shared constants file (or at minimum match the casing), otherwise we'll end up with two competing conventions and no single place to audit storage keys.

Suggestions (non-blocking)

  • No tests added. suggestion: this is the second incident-class fix (~7K users/day per the PR description) on a guard that has no unit coverage. At minimum, add a auth.test.ts covering the new resolution table (200/empty, 200/non-empty, 401, 403, 404, 500, network throw) — the resolution rules in the JSDoc are the spec, they should be enforceable. The router gate is harder to unit-test, but at least sessionStorage set/check could be covered with a thin wrapper. A regression here will be invisible until the next incident.

  • src/router.ts:209-211 — suggestion: the dynamic-import-to-avoid-circular-dep comment is preserved, but with the Sentry.addBreadcrumb at the top of router.ts you now eagerly import @sentry/vue at module load. Verify the circular dep wasn't transitively via Sentry — if auth.ts (now imported lazily) was the cycle source and Sentry is fine to load eagerly, you might be able to drop the dynamic import. If Sentry was the cycle, this PR may have re-introduced it; worth pnpm typecheck && pnpm build locally to confirm.

  • src/router.ts:230 — suggestion: the silent fall-through after the catch (no log of "proceeding past gate due to error") is hard to debug from Sentry. Either log a breadcrumb in the catch (you already log in the success path), or surface this as a tagged Sentry event so we can quantify how often the gate is being bypassed by errors versus deliberately. Otherwise we'll be guessing again on the next incident.

  • src/router.ts:255-260 — suggestion: initial_load: _from.name === undefined is a fragile heuristic. Vue Router's initial navigation has _from.name === undefined and _from.matched.length === 0. The latter is more robust; name === undefined can also be true for routes you forgot to name.

  • src/platform/cloud/onboarding/auth.ts:147-151 — suggestion: the catch-block now both throws (for auth) and resolves to true (for everything else) with no breadcrumb on the non-auth path — you only captureException. That's inconsistent with the 404/5xx in-band path which logs a breadcrumb with treated_as: 'completed'. Add the same breadcrumb here so all "treated as completed" paths are uniformly observable.

Nits

  • src/platform/cloud/onboarding/auth.ts:99 — nit: existing JSDoc resolution rule says "5xx / network error → true" but a 404 is grouped under "5xx / other" in the implementation. Worth pulling 404 into its own bullet in the body — "key absent" semantically isn't a 5xx, and the comment at line 117–119 already calls it out as the most important and most ambiguous case. Right now the JSDoc and the body comment overlap awkwardly.

  • src/router.ts:222 — nit: console.error('Failed to check user status:', error) was previously the only signal that we hit this path; now it's still console-only. Consider Sentry.captureException(error, { tags: { area: 'survey_gate' }, level: 'warning' })console.error is invisible in prod.

  • src/router.ts:258 — nit: en-dash arrow in breadcrumb message 'survey gate → /cloud/survey'. Project writing style says no em/en dashes; use ASCII -> for log strings to keep them grep-friendly.

Test Coverage Assessment

  • Zero tests added. The PR changes the contract of a function with three call sites and a documented resolution table — that table is exactly the kind of thing that wants a describe('getSurveyCompletedStatus resolution rules', ...) block. Without it, the next refactor (say, someone normalizing the error sentinel string in Configure vite to copy from src to dist #2 above) silently regresses.
  • The manual test plan in the PR description covers the happy path and the 5xx case for the router gate, but does not cover (a) the new 401 throw path from UserCheckView, (b) the 5xx-now-returns-true path in CloudSurveyView.vue (user lands on survey, hits transient 5xx in onMounted, gets bounced to user-check), or (c) the gate's behavior when getSurveyCompletedStatus throws — which is now the only error path it has.
  • Recommend at least: unit test for the resolution table, and a Playwright case that loads /cloud/survey directly with a mocked 500 on /settings/onboarding_survey to catch the regression in CloudSurveyView's onMounted.

Verdict

Don't merge as-is. The auth.ts policy is sound, but the policy change wasn't applied consistently to the other two callers of getSurveyCompletedStatus, and the router's catch branch now bypasses the gate on the exact errors auth.ts was changed to throw — which silently re-enables the original incident class for any 401 at gate time. Fix the catch to redirect/login (or to not poison the session flag on throws), update the two other callers to the new contract, and please add a unit test for the resolution table before this lands.

@deepme987
Copy link
Copy Markdown
Contributor Author

Review: fix(cloud): stop bouncing working users to /cloud/survey mid-session

The intent here is correct and well-motivated. The 7,000 daily false-positive bounces are a real problem, the response-code triage table is the right mental model, and the sessionStorage gate addresses the background-reload trigger properly. A few issues worth resolving before merge, one of which is a user-facing correctness bug.


Race Conditions

[MUST FIX] src/router.ts — sessionStorage flag set before redirect creates a window where the gate is already latched but the user is mid-survey

The flag is written, then next({ name: 'cloud-survey' }) is returned on the same synchronous tick. If the user opens a second tab during the survey (same origin, same sessionStorage partition — sessionStorage is per-tab, not per-origin, so this is actually safe) this is fine. However, there is a subtlety: sessionStorage.setItem is called at line 245 before the redirect fires. If the redirect itself is somehow cancelled by a downstream beforeEach guard, the gate is permanently latched for the session even though the survey was never shown. This is unlikely in the current guard setup but fragile if a future guard is added to cloud-survey. Low severity on its own, but worth documenting with a comment that the flag write is intentionally pessimistic (fail-open) rather than an oversight.

[MUST FIX] src/stores/firebaseAuthStore.ts:114 — logout does not clear SURVEY_GATE_SESSION_KEY

This is a concrete user-visible bug. onAuthStateChanged clears workspace-scoped sessionStorage keys on logout, but comfy.survey_gate_evaluated_this_session is never removed. If user A logs out and user B logs in within the same tab (team-shared machine, or a user who switches accounts), user B's survey gate will be skipped entirely for that tab session because the flag is already set from user A's evaluation. Since the flag is stored against the tab, not the Firebase UID, it silently suppresses the survey for the second user. Fix: add sessionStorage.removeItem('comfy.survey_gate_evaluated_this_session') inside the user === null branch of onAuthStateChanged, alongside the existing workspace key removals. Ideally import SURVEY_GATE_SESSION_KEY from router.ts or move it to a shared constants file so the two sites stay in sync.


Browser API Edge Cases

[MUST FIX] src/router.ts — bare sessionStorage access is not guarded against SecurityError

The workspaceAuthStore wraps all its sessionStorage calls in try/catch (see src/stores/firebaseAuthStore.ts:119) with a comment "Ignore sessionStorage errors (e.g., in private browsing mode)". The new sessionStorage.getItem(SURVEY_GATE_SESSION_KEY) and sessionStorage.setItem(...) at lines 230 and 245 have no equivalent guard. In a sandboxed iframe (e.g., the workflow embed use-case) or a privacy-hardened browser, sessionStorage access throws SecurityError. Since the gate's intent is to fail-open (treat ambiguous situations as "completed"), a thrown SecurityError here would propagate up through the beforeEach and leave navigation in an undefined state. Wrap both accesses in try/catch and treat a SecurityError as if the key is absent (i.e., proceed to evaluate the gate) — consistent with the rest of the codebase's pattern of failing open on storage errors.


State Management in Callers

[SHOULD FIX] src/platform/cloud/onboarding/UserCheckView.vue:61 — getSurveyCompletedStatus now throws on 401/403, but the Promise.all caller shows a generic error UI

UserCheckView calls getSurveyCompletedStatus inside Promise.all([getUserCloudStatus(), getSurveyCompletedStatus()]). With the old code, a 401 returned false (silently). With the new code, it throws Survey status auth error: 401 .... That error propagates to wrapWithErrorHandlingAsync, which shows a toast and surfaces error.value in the component. The errorMessage computed at line 87 does not match the new error message prefix ('Survey status auth error:''), so it falls through to the generic "Unable to check account status. Please try again." string. The retry button will re-call the same endpoint and throw again. The correct handling here is to redirect to cloud-login on a 401/403 from getSurveyCompletedStatus, same as getUserCloudStatus already does when it throws. Either catch the auth error specifically in UserCheckView and redirect, or have getSurveyCompletedStatus attach a distinguishing property to the thrown error so callers can branch without string-prefix matching.

[SHOULD FIX] src/platform/cloud/onboarding/CloudSurveyView.vue:265 — onMounted catch swallows the new auth error silently

The onMounted in CloudSurveyView calls getSurveyCompletedStatus and catches errors with console.error only. Under the old code this was fine (nothing threw). Under the new code, a 401 during the mount check will print to console but not redirect. The user lands on the survey page with an expired token, fills it out, and submitSurvey will also 401. That failure is handled (it throws), but the user experience is confusing — they see a survey they cannot submit. Since this route is already authenticated by the router guard, a 401 here is most likely a race with token expiry. Redirecting to cloud-login in the catch is the right call, consistent with how the router guard handles auth errors.


Telemetry Correctness

[INFORMATIONAL] src/router.ts — Sentry breadcrumb fires after sessionStorage.setItem but before next()

The ordering is: evaluate, set flag, emit breadcrumb, call next(). This is fine for Sentry (breadcrumbs are buffered and attached to the next event, not sent immediately). No action required, but worth noting for future readers that the breadcrumb intentionally precedes the navigation call.

[INFORMATIONAL] src/platform/cloud/onboarding/auth.ts — error re-identification via string prefix

The catch block uses error.message.startsWith('Survey status auth error:') to distinguish re-thrown auth errors from network errors. This pattern is used consistently in this file (e.g., the isHttpError helper uses the same prefix match). It works, but it is fragile to message string changes. A typed error class (e.g., class SurveyAuthError extends Error {}) would be more robust and make the instanceof check in callers self-documenting. Low priority but worth a follow-up.


Good

  • Freezing the response-code decision table in the JSDoc block is exactly right — it makes the policy auditable and testable without reading control flow.
  • Setting the session flag regardless of the outcome (line 245) before the redirect is the correct fail-open sequencing for the suppression use-case.
  • The Sentry breadcrumb on the gate redirect includes initial_load: _from.name === undefined which is a useful signal for distinguishing first-paint redirects from mid-session ones.
  • Breaking the loop from catch → cloud-user-check → getSurveyCompletedStatus → throw → catch ... by letting the user proceed in the catch branch is the right call.
  • The trade-off documented in the PR description (new-user 404 treated as completed) is an honest product trade-off, and flagging the clean backend fix as a follow-up is the right scope decision.

Summary of blockers before merge:

  1. onAuthStateChanged logout path must clear SURVEY_GATE_SESSION_KEY (user-switching bug).
  2. sessionStorage accesses in router.ts must be guarded against SecurityError (fail-open, consistent with existing patterns).
  3. UserCheckView and CloudSurveyView callers need to handle the newly-thrown 401/403 errors rather than showing a generic retry UI or swallowing them silently.

deepme987 and others added 2 commits May 15, 2026 15:06
- Replace string-prefix error sentinel with a typed SurveyAuthError class
  so router/CloudSurveyView can branch on `instanceof` instead of matching
  a hardcoded message.
- Clear the session-scoped survey-gate flag on logout. Without this, a
  same-tab account switch (logout uses `window.location.href`, which
  preserves sessionStorage) silently skipped the gate for the next user.
- Stop marking the gate as evaluated when the auth check itself throws
  SurveyAuthError. The previous catch branch swallowed 401/403 and set
  the flag, disabling the gate for the whole tab session on a single
  transient auth blip. Non-auth errors still mark the flag (those are
  expected to keep failing in the short term and we don't want to hammer
  the API).
- Wrap sessionStorage reads/writes in try/catch to match the codebase
  convention for SecurityError in sandboxed iframes / private browsing.
- Move the gate's sessionStorage key into WORKSPACE_STORAGE_KEYS to
  follow the existing `Comfy.Workspace.*` naming.
- Drop CloudSurveyView's redundant `getSurveyCompletedStatus()` re-check
  in onMounted. The router gate is the source of truth; a redundant
  check would treat a transient 5xx during signup as "already completed"
  and bounce a new user away from the survey page — the same false-
  positive class this PR is meant to fix.
- Remove the Sentry breadcrumb/captureException calls added inside the
  survey-gate code paths. Sentry is deprecated in this org per CLAUDE.md.
  Pre-existing Sentry usage in getUserCloudStatus and submitSurvey is
  untouched.
- Add auth.test.ts covering the five resolution branches plus the
  SurveyAuthError shape.
coderabbitai[bot]
coderabbitai Bot previously requested changes May 15, 2026
Copy link
Copy Markdown
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

🧹 Nitpick comments (1)
src/router.ts (1)

273-286: Add structured telemetry for fail-open survey checks.

At Line 285, this path only logs to console.error. Consider emitting a telemetry event/counter (non-PII) when the guard bypasses on error so you can monitor how often survey gating is skipped in production.

🤖 Prompt for 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.

In `@src/router.ts` around lines 273 - 286, In the catch block that currently
calls writeSurveyGateFlag() and console.error (the block referencing
SurveyAuthError and writeSurveyGateFlag), emit a non-PII telemetry event or
increment a counter whenever you execute the fail-open path (i.e., when error is
not an instance of SurveyAuthError and you call writeSurveyGateFlag); implement
this by calling the existing telemetry/metrics API in the codebase (e.g.,
telemetry.increment or telemetry.trackEvent) with a clear metric name like
"survey_gate_bypassed" and include only non-identifying context (error
type/category, environment, and boolean flags) before calling
writeSurveyGateFlag and logging the error.
🤖 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 `@src/composables/auth/useAuthActions.ts`:
- Around line 85-95: Wrap the call to authStore.logout() in a try/finally so
sessionStorage.removeItem(WORKSPACE_STORAGE_KEYS.SURVEY_GATE_EVALUATED) always
runs even if logout throws; move the existing sessionStorage removal into the
finally block (preserving the existing catch for storage access) and keep the
subsequent navigation via window.location.href unchanged so the survey-gate
cleanup executes on both success and failure of authStore.logout().

---

Nitpick comments:
In `@src/router.ts`:
- Around line 273-286: In the catch block that currently calls
writeSurveyGateFlag() and console.error (the block referencing SurveyAuthError
and writeSurveyGateFlag), emit a non-PII telemetry event or increment a counter
whenever you execute the fail-open path (i.e., when error is not an instance of
SurveyAuthError and you call writeSurveyGateFlag); implement this by calling the
existing telemetry/metrics API in the codebase (e.g., telemetry.increment or
telemetry.trackEvent) with a clear metric name like "survey_gate_bypassed" and
include only non-identifying context (error type/category, environment, and
boolean flags) before calling writeSurveyGateFlag and logging the error.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8220c17d-b640-4dcf-b820-b2a11eaa75b7

📥 Commits

Reviewing files that changed from the base of the PR and between c7abc90 and c717616.

📒 Files selected for processing (6)
  • src/composables/auth/useAuthActions.ts
  • src/platform/cloud/onboarding/CloudSurveyView.vue
  • src/platform/cloud/onboarding/auth.test.ts
  • src/platform/cloud/onboarding/auth.ts
  • src/platform/workspace/workspaceConstants.ts
  • src/router.ts
✅ Files skipped from review due to trivial changes (1)
  • src/platform/workspace/workspaceConstants.ts

Comment thread src/composables/auth/useAuthActions.ts Outdated
Comment on lines +85 to +95

// Clear the survey gate so the next user signing in on the same tab
// gets a fresh evaluation. `window.location.href` below preserves
// sessionStorage, so without this an account switch silently skips the
// survey gate for the new user.
try {
sessionStorage.removeItem(WORKSPACE_STORAGE_KEYS.SURVEY_GATE_EVALUATED)
} catch {
// sessionStorage unavailable (sandboxed iframe / strict private mode);
// the tab is being navigated away anyway.
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 15, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Ensure survey-gate session cleanup runs even if logout throws.

If authStore.logout() fails, Lines 90-95 are never reached, so the stale gate key can survive in the same tab. Wrap logout with try/finally so removal always executes.

Suggested fix
-    await authStore.logout()
-
-    // Clear the survey gate so the next user signing in on the same tab
-    // gets a fresh evaluation. `window.location.href` below preserves
-    // sessionStorage, so without this an account switch silently skips the
-    // survey gate for the new user.
-    try {
-      sessionStorage.removeItem(WORKSPACE_STORAGE_KEYS.SURVEY_GATE_EVALUATED)
-    } catch {
-      // sessionStorage unavailable (sandboxed iframe / strict private mode);
-      // the tab is being navigated away anyway.
-    }
+    try {
+      await authStore.logout()
+    } finally {
+      try {
+        sessionStorage.removeItem(WORKSPACE_STORAGE_KEYS.SURVEY_GATE_EVALUATED)
+      } catch {
+        // sessionStorage unavailable (sandboxed iframe / strict private mode)
+      }
+    }

As per coding guidelines: "Implement proper error handling in all code".

🤖 Prompt for 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.

In `@src/composables/auth/useAuthActions.ts` around lines 85 - 95, Wrap the call
to authStore.logout() in a try/finally so
sessionStorage.removeItem(WORKSPACE_STORAGE_KEYS.SURVEY_GATE_EVALUATED) always
runs even if logout throws; move the existing sessionStorage removal into the
finally block (preserving the existing catch for storage access) and keep the
subsequent navigation via window.location.href unchanged so the survey-gate
cleanup executes on both success and failure of authStore.logout().

Copy link
Copy Markdown
Contributor Author

@deepme987 deepme987 May 15, 2026

Choose a reason for hiding this comment

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

Stale — useAuthActions.ts was reverted to origin/main in the simplify commit (6915d83). The PR no longer touches this file; gate-cleanup-on-logout was dropped along with the rest of the per-tab gate logic.

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.

🧩 Analysis chain

Thanks for the heads-up, @deepme987. Acknowledged — since useAuthActions.ts was reverted to origin/main and the per-tab gate logic was dropped entirely, this comment is no longer relevant. I'll resolve it now.


🏁 Script executed:

#!/bin/bash
# List unresolved review comments by coderabbitai on PR `#12301` to find the one to resolve
gh api graphql -f query='
{
  repository(owner: "Comfy-Org", name: "ComfyUI_frontend") {
    pullRequest(number: 12301) {
      reviewThreads(first: 50) {
        nodes {
          id
          isResolved
          comments(first: 1) {
            nodes {
              author { login }
              body
              url
            }
          }
        }
      }
    }
  }
}'

Length of output: 76


Got it, @deepme987 — the comment is stale and no longer applicable since the per-tab gate logic (including the SURVEY_GATE_EVALUATED sessionStorage cleanup on logout) was dropped in the simplify commit. I wasn't able to resolve the thread programmatically due to a missing auth token in the sandbox, but you or a maintainer can dismiss it directly in the GitHub UI.


🧠 Learnings used
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7169
File: src/platform/remote/comfyui/jobs/jobTypes.ts:1-107
Timestamp: 2025-12-09T03:39:54.501Z
Learning: In the ComfyUI_frontend project, Zod is on v3.x. Do not suggest Zod v4 standalone validators (z.uuid, z.ulid, z.cuid2, z.nanoid) until an upgrade to Zod 4 is performed. When reviewing TypeScript files (e.g., src/platform/remote/comfyui/jobs/jobTypes.ts) validate against Zod 3 capabilities and avoid introducing v4-specific features; flag any proposal to upgrade or incorporate v4-only validators and propose staying with compatible 3.x patterns.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7416
File: src/stores/imagePreviewStore.ts:5-7
Timestamp: 2025-12-13T11:03:11.264Z
Learning: In the ComfyUI_frontend repository, lint rules require keeping 'import type' statements separate from non-type imports, even if importing from the same module. Do not suggest consolidating them into a single import statement. Ensure type imports remain on their own line (import type { ... } from 'module') and regular imports stay on separate lines.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.stories.ts:45-55
Timestamp: 2025-12-17T00:40:09.635Z
Learning: Prefer pure function declarations over function expressions (e.g., use function foo() { ... } instead of const foo = () => { ... }) for pure functions in the repository. Function declarations are more functional-leaning, offer better hoisting clarity, and can improve readability and tooling consistency. Apply this guideline across TypeScript files in Comfy-Org/ComfyUI_frontend, including story and UI component code, except where a function expression is semantically required (e.g., callbacks, higher-order functions with closures).

Learnt from: kaili-yang
Repo: Comfy-Org/ComfyUI_frontend PR: 7805
File: src/composables/useCoreCommands.ts:439-439
Timestamp: 2025-12-30T22:22:33.836Z
Learning: When accessing reactive properties from Pinia stores in TypeScript files, avoid using .value on direct property access (e.g., useStore().isOverlayExpanded). Pinia auto-wraps refs when accessed directly, returning the primitive value. The .value accessor is only needed when destructuring store properties or when using storeToRefs().

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8248
File: src/platform/assets/schemas/assetSchema.ts:109-118
Timestamp: 2026-01-22T23:44:59.888Z
Learning: Do not export declarations (e.g., schemas, constants, types) unless they are actually used elsewhere in the codebase. Keep the public API surface minimal by exporting only items with concrete usage. This reduces coupling and keeps imports explicit. In src/platform/assets/schemas/assetSchema.ts, ensure only necessary exports are exposed; remove unused exports or switch to non-exported/internal where appropriate.

Learnt from: JemiloII
Repo: Comfy-Org/ComfyUI_frontend PR: 8282
File: src/composables/usePaste.test.ts:372-383
Timestamp: 2026-01-23T21:06:15.240Z
Learning: In paste/clipboard handling code, do not rely on ephemeral DataTransfer data across async boundaries. If an operation awaits async tasks (such as node creation) during a paste, clone the DataTransfer (e.g., using cloneDataTransfer) to persist files and string data before the paste event completes. This ensures clipboard contents remain available for subsequent processing.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8722
File: src/composables/maskeditor/useBrushDrawing.ts:5-5
Timestamp: 2026-02-08T03:19:12.009Z
Learning: Treat unresolved import warnings caused by pnpm workspace catalog mappings (pnpm-workspace.yaml) and unplugin-typegpu in vite.config.mts as false positives. Do not flag these in PR reviews; they are expected in sandboxes without node_modules and are not indicative of actual missing dependencies in the TS codebase.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/load3d.ts:427-430
Timestamp: 2026-02-19T02:06:23.468Z
Learning: In TypeScript, you can use typeof SomeClass in type annotation positions (e.g., param: typeof LGraphNode) even when SomeClass is imported with import type. This is a type query that only exists at compile time and is erased in runtime, so it is safe to combine with type-only imports. Apply this pattern to TS files broadly when you need a type that references the shape of a class or constructor function without importing the value at runtime.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/uploadAudio.ts:91-94
Timestamp: 2026-02-19T02:06:38.395Z
Learning: In TypeScript files, you can use a type annotation like 'nodeType: typeof MyClass' even if MyClass is imported via 'import type'. Both the type-only import and 'typeof' operate at the type level and are erased at compile time. This pattern is commonly used for constructor types (e.g., 'nodeType: typeof LGraphNode'). Apply this pattern across TypeScript files in the repository (src/**/*.ts) as appropriate, ensuring the imported symbol is a type-only import when possible for clarity and to avoid runtime imports.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8992
File: src/lib/litegraph/src/widgets/GradientSliderWidget.ts:18-18
Timestamp: 2026-02-20T21:08:19.814Z
Learning: When drawing with CanvasRenderingContext2D in TypeScript/JavaScript, wrap the drawing logic with ctx.save() at the start and ctx.restore() at the end to preserve and restore the canvas state. Do not manually destructure and restore individual properties (e.g., fillStyle, strokeStyle); rely on save/restore to manage state changes in a scoped manner. This should be applied to all TS files that perform canvas drawing.

Learnt from: dante01yoon
Repo: Comfy-Org/ComfyUI_frontend PR: 9075
File: src/scripts/api.featureFlags.test.ts:237-268
Timestamp: 2026-02-22T04:27:33.379Z
Learning: In Vite/Vitest, import.meta.env.DEV is true for any mode that is not 'production' (i.e., DEV is the opposite of PROD, and can be true in 'test', 'development', etc.). Do not assume DEV implies only 'development' mode. When reviewing code and tests, treat DEV as a non-production flag and verify environment-specific logic accordingly. Reference: https://vite.dev/guide/env-and-mode#modes

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 11420
File: src/composables/useCanvasScheduler.ts:20-20
Timestamp: 2026-04-19T22:12:36.981Z
Learning: In the ComfyUI frontend Vue/TypeScript codebase, when implementing singleton composables inside `src/composables/**`, prefer `createSharedComposable` from VueUse over module-scoped `let` variables. Module-scoped `let` can cause SSR cross-request state leakage, unpredictable HMR behavior, and requires manual lifecycle/cleanup management for watchers/computeds/effects. `createSharedComposable` ties the singleton to Vue lifecycle: it disposes watchers/computeds and effect-scope reactivity automatically when the last consumer unmounts, improving testability. Only use the module-scoped `let` pattern if the singleton must intentionally outlive all Vue component trees or if there is a clearly documented reason why `createSharedComposable` is inferior for that specific composable.

Learnt from: kaili-yang
Repo: Comfy-Org/ComfyUI_frontend PR: 11531
File: src/composables/maskeditor/brushDrawingUtils.ts:14-14
Timestamp: 2026-04-22T04:06:01.353Z
Learning: In the Comfy-Org/ComfyUI_frontend codebase, `knip` is used to detect unused exports. When reviewing TypeScript/TSX code, avoid recommending or introducing exported types/functions/constants that have no concrete external consumers (e.g., they’re only used within the same module). Only recommend exporting when there is an actual external usage outside the module. If something might be needed externally later, keep it non-exported for now and defer exporting until an external consumer is added.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 12197
File: src/renderer/extensions/linearMode/AppInput.vue:23-23
Timestamp: 2026-05-14T21:10:45.382Z
Learning: In the ComfyUI_frontend repo, `WidgetEntityId` is a branded template-literal type (e.g., `${UUID}:${NodeId}:${string}`) and should be treated as structurally non-empty when valid—so an empty string is invalid and must be treated as “absent”. When handling optional `entityId?: WidgetEntityId` values (e.g., for Vue props or TS function parameters), guard missing/invalid IDs with a truthy check (e.g., `if (!entityId) return`) rather than `if (entityId === undefined)`. Rationale: `=== undefined` would allow empty-string values through into stores such as `appModeStore.selectedInputs`, creating rows that can’t resolve back to a widget.

Drop the per-tab sessionStorage gate, SurveyAuthError class, logout-side
cleanup, CloudSurveyView change, and workspace key — none of those are
required to fix the mid-session bounce. With getSurveyCompletedStatus
returning true on every ambiguous response (404 / 5xx / network), no
transient error can produce a false "not completed" signal, so the
existing single-evaluation router gate is correct as-is.

Cumulative PR diff vs main is now: two `return false` -> `return true`
in auth.ts (with comments updated), plus a new auth.test.ts covering
the six resolution branches.
coderabbitai[bot]
coderabbitai Bot previously requested changes May 15, 2026
Copy link
Copy Markdown
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/platform/cloud/onboarding/auth.ts (1)

98-112: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate auth failures instead of failing open on 401/403.

This now returns true for all non-OK responses and all caught errors, so 401/403 are treated as “completed” instead of surfacing auth problems. That breaks the intended contract and can permanently bypass the survey gate after auth blips.

Use explicit branching for auth statuses and rethrow in catch (or use a typed sentinel error) so only ambiguous failures fail-open.

Suggested fix
 export async function getSurveyCompletedStatus(): Promise<boolean> {
   try {
     const response = await api.fetchApi(`/settings/${ONBOARDING_SURVEY_KEY}`, {
       method: 'GET',
       headers: {
         'Content-Type': 'application/json'
       }
     })
     if (!response.ok) {
+      if (response.status === 401 || response.status === 403) {
+        throw new Error(`Survey auth failure: ${response.status}`)
+      }
       Sentry.addBreadcrumb({
         category: 'auth',
         message: 'Survey status check returned non-ok response',
         level: 'info',
         data: {
           status: response.status,
           endpoint: `/settings/${ONBOARDING_SURVEY_KEY}`
         }
       })
       return true
     }
     const data = await response.json()
     return !isEmpty(data.value)
   } catch (error) {
+    if (
+      error instanceof Error &&
+      error.message.startsWith('Survey auth failure:')
+    ) {
+      throw error
+    }
     Sentry.captureException(error, {
       tags: {
         api_endpoint: '/settings/{key}',
         error_type: 'network_error'
       },
       extra: {
         route_template: '/settings/{key}',
         route_actual: `/settings/${ONBOARDING_SURVEY_KEY}`
       },
       level: 'warning'
     })
     return true
   }
 }

Also applies to: 116-129

🤖 Prompt for 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.

In `@src/platform/cloud/onboarding/auth.ts` around lines 98 - 112, The current
logic treats any non-OK HTTP response (and all caught errors) as "completed"
which masks auth failures; change the non-ok handling so you explicitly check
response.status and propagate auth errors: if response.status is 401 or 403,
throw or rethrow an authentication error (do not return true), otherwise
continue to add the Sentry breadcrumb and return true for ambiguous statuses;
also update the corresponding catch block so it rethrows authentication-related
errors (or a typed sentinel) while only swallowing/returning true for non-auth
transient errors—use the same symbols shown (response.status,
ONBOARDING_SURVEY_KEY, Sentry.addBreadcrumb) to locate the code to modify.
🤖 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 `@src/platform/cloud/onboarding/auth.test.ts`:
- Around line 40-74: Add two tests to src/platform/cloud/onboarding/auth.test.ts
that assert getSurveyCompletedStatus rejects when the backend returns 401 or
403: mimic the existing style using
fetchApi.mockResolvedValueOnce(mockResponse({ ok: false, status: 401 })) and
fetchApi.mockResolvedValueOnce(mockResponse({ ok: false, status: 403 })), and
use await expect(getSurveyCompletedStatus()).rejects.toThrow() (or similar
rejects matcher) to lock the auth-failure contract; reuse the mockResponse
helper and the same test structure as the other cases so the suite remains
consistent.

---

Outside diff comments:
In `@src/platform/cloud/onboarding/auth.ts`:
- Around line 98-112: The current logic treats any non-OK HTTP response (and all
caught errors) as "completed" which masks auth failures; change the non-ok
handling so you explicitly check response.status and propagate auth errors: if
response.status is 401 or 403, throw or rethrow an authentication error (do not
return true), otherwise continue to add the Sentry breadcrumb and return true
for ambiguous statuses; also update the corresponding catch block so it rethrows
authentication-related errors (or a typed sentinel) while only
swallowing/returning true for non-auth transient errors—use the same symbols
shown (response.status, ONBOARDING_SURVEY_KEY, Sentry.addBreadcrumb) to locate
the code to modify.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 29a9d844-2610-4ada-933f-08930b086782

📥 Commits

Reviewing files that changed from the base of the PR and between e88a7ae and 6915d83.

📒 Files selected for processing (2)
  • src/platform/cloud/onboarding/auth.test.ts
  • src/platform/cloud/onboarding/auth.ts

Comment thread src/platform/cloud/onboarding/auth.test.ts
401 and 403 are treated the same as any other non-OK response — return
true (completed) — so a transient auth failure can't bounce a working
user to /cloud/survey. The dedicated auth layer handles re-auth on the
next API call. Add tests so the policy can't silently drift back to
throwing on auth errors.
@deepme987 deepme987 marked this pull request as ready for review May 15, 2026 23:46
@deepme987 deepme987 requested a review from a team May 15, 2026 23:46
@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label May 15, 2026
@DrJKL DrJKL added needs-backport Fix/change that needs to be cherry-picked to the current feature freeze branch cloud/1.44 Backport PRs for cloud 1.44 labels May 18, 2026
DrJKL
DrJKL previously approved these changes May 18, 2026
Copy link
Copy Markdown
Contributor

@DrJKL DrJKL left a comment

Choose a reason for hiding this comment

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

🙌 LGTM — thank you for protecting working users from the survey hammer

I need to start with full disclosure: I am professionally tired of being yanked out of a flow state to rate something, fill in another two-question onboarding survey, or confirm that yes, I still want to use the product I am actively using. Every single one of these is a paper cut, and at scale (the ~7k users/day in your Datadog screenshot) it is not a paper cut — it is a daily, recurring betrayal of the user's attention. So thank you, @deepme987, for fixing this. Sincerely.

What I like, beyond the user impact:

  • Discipline in scoping. The simplification commit (6915d833 refactor: simplify PR to the minimum policy change) is the kind of commit I wish I saw more often. You built a more elaborate fix — SurveyAuthError, sessionStorage gate, workspace key plumbing, CloudSurveyView edits — realized that two return falsereturn true plus comments and tests covers the whole failure mode, and threw the rest away. The PR is now ~10 lines of meaningful change for an outage-class fix. That is a real-deal YAGNI win.
  • The policy-locking tests are the right shape. You did not write change-detector tests; you wrote one test per resolution branch and added an explanatory comment specifically locking the 401/403 → true decision so a future refactor cannot quietly drift back to a throw branch. That comment is doing real work.
  • Comments that explain why. The replacement for the // Not an error case comment now tells the next reader the user-facing reason for the choice ("bouncing working customers to /cloud/survey on transient hiccups"). This is the standard the AGENTS.md guidance asks for.
  • The trade-off is named, not hidden. The PR description explicitly calls out the new-user-on-404 false negative and frames it against product's stated preference. That is the right level of honesty for a behavior change in a flow that touches paying customers.

Approving. One blocker-adjacent ask on the PR description (left as an inline note on the diff so it's adjacent to the code it describes), plus three minor polish comments inline that you can take or leave. None of them block the merge from my side — I would rather this ship and protect users today than block on stylistic notes.

Thank you again. This is a great fix.

}
})
return false
return true
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.

⚠️ PR description is now stale — please update before merge.

The PR body's resolution table still says:

| 401 / 403 | false → bounced | throws (auth issue, not survey signal) |

…but the final commit (9dfb501c test(auth): lock 401/403 behavior under the ambiguous-response policy) and the new tests at L71–80 explicitly lock 401/403 → true (not throws). Every non-OK response now flows through this single return true; the function has no auth-error branch at all.

This is the only thing I would call out as actually important — reviewers / approvers / future spelunkers will read the PR body and sign off on a different contract than the one being shipped. Please update the table to show 401/403 → true and drop the "throws" row. (Bonus: the simplification story gets even stronger when the table reflects what actually shipped.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the PR description — dropped the throws row, all non-OK / network now show true. Thanks for catching it.

})
if (!response.ok) {
// Not an error case - survey not completed is a valid state
// Ambiguous response (404/5xx/etc). Treat as completed to avoid
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.

👍 Nit-positive: this comment is doing what the AGENTS.md guidance asks for — it explains the user-facing why (transient hiccups bouncing working customers), not the what. Future reader will know why this looks like a swallow and won't "helpfully" revert it. Keep this style.

Comment thread src/platform/cloud/onboarding/auth.ts Outdated
return true
}
const data = await response.json()
// Check if data exists and is not empty
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.

Minor: this comment restates the code (!isEmpty(data.value)). Project guidance prefers dropping redundant comments. Non-blocking.

Suggested change
// Check if data exists and is not empty
return !isEmpty(data.value)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dropped in 1e9a6f5.

@@ -124,7 +126,7 @@ export async function getSurveyCompletedStatus(): Promise<boolean> {
},
level: 'warning'
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.

Minor consistency nit: the non-OK HTTP branch above breadcrumbs at level: 'info', but this catch branch logs at level: 'warning'. Under the new policy both are the same event class — "ambiguous response, deliberately resolved as completed." Picking one (probably 'warning' for both, so the suppression is visible in Sentry filters) would make incident triage easier next time someone digs through the breadcrumbs. Non-blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Bumped the breadcrumb to warning in 1e9a6f5 so both ambiguous-response paths log at the same level.

Comment on lines +71 to +80
// 401/403 fall under the same "ambiguous => treat as completed" policy.
// The dedicated auth layer handles re-authentication on the next API
// call; this function deliberately does not try to disambiguate auth
// failures from other non-OK responses. Locking with tests so the
// policy can't drift back to a "throw on auth error" branch.
test('401 → true (auth layer handles re-auth on next call)', async () => {
fetchApi.mockResolvedValueOnce(mockResponse({ ok: false, status: 401 }))
await expect(getSurveyCompletedStatus()).resolves.toBe(true)
})

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.

👍 Nit-positive: this is the right way to write a regression test. The 401/403 cases carry an explanatory comment that pins the policy decision to a reasonauth layer handles re-auth on the next API call — so a future refactor that re-introduces a throw branch will trip a red test with a comment explaining why the test is red. Great use of test-as-spec.

Comment on lines +18 to +31
function mockResponse({
ok,
status,
body
}: {
ok: boolean
status: number
body?: unknown
}): Response {
return {
ok,
status,
statusText: '',
json: async () => body
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.

Minor: body?: unknown + json: async () => body + as unknown as Response is the kind of double-cast the project's type-safety guidance gently warns against. Acceptable for a test scaffold and I would not block on it, but a typed Partial<Response> helper (or body: { value: unknown } since that's all the SUT reads) would keep the test more honest about the shape it depends on. Non-blocking.

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.

Correction on myself — this codebase has @total-typescript/shoehorn already in dev deps and ~70 test files use it for exactly this. fromPartial<Response>(...) keeps the same partial-shape style without the as unknown as Response double cast (add import { fromPartial } from '@total-typescript/shoehorn' at the top):

Suggested change
function mockResponse({
ok,
status,
body
}: {
ok: boolean
status: number
body?: unknown
}): Response {
return {
ok,
status,
statusText: '',
json: async () => body
function mockResponse({
ok,
status,
body
}: {
ok: boolean
status: number
body?: unknown
}): Response {
return fromPartial<Response>({
ok,
status,
statusText: '',
json: async () => body
})
}

Still non-blocking, just pointing at the local convention. Sorry for the noise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Switched to fromPartial in 1e9a6f5. Thanks for the pointer to the local convention.

@DrJKL DrJKL assigned deepme987 and unassigned DrJKL May 18, 2026
- Bump the non-OK breadcrumb to `level: 'warning'` so both ambiguous-
  response paths (HTTP non-OK and network/parse failure) log at the
  same level. Both are the same event class — "deliberately resolved
  as completed" — and aligning the levels makes Sentry triage easier.
- Drop the comment that restated `!isEmpty(data.value)`.
- Use `fromPartial<Response>` from `@total-typescript/shoehorn` instead
  of the `as unknown as Response` double-cast in the test mock helper.
  Matches the existing repo convention (~150 call sites).
@deepme987 deepme987 dismissed stale reviews from coderabbitai[bot] and coderabbitai[bot] May 18, 2026 22:27

Stale — addressed by the simplify commit (6915d83). The router.ts changes flagged here were dropped entirely; the auth-failure suggestion is intentionally rejected per the simplified policy and locked with explicit 401/403 tests in 9dfb501.

@deepme987 deepme987 added this pull request to the merge queue May 18, 2026
Merged via the queue into main with commit cf267ac May 18, 2026
49 checks passed
@deepme987 deepme987 deleted the deep/fix-survey-gate-false-positives branch May 18, 2026 23:51
@comfy-pr-bot
Copy link
Copy Markdown
Member

@deepme987 Successfully backported to #12320

@github-actions github-actions Bot removed the needs-backport Fix/change that needs to be cherry-picked to the current feature freeze branch label May 18, 2026
AustinMroz pushed a commit that referenced this pull request May 19, 2026
…ud/survey mid-session (#12320)

Backport of #12301 to `cloud/1.44`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12320-backport-cloud-1-44-fix-cloud-stop-bouncing-working-users-to-cloud-survey-mid-sessi-3646d73d3650815185cecaa2babdf3d1)
by [Unito](https://www.unito.io)

Co-authored-by: Deep Mehta <42841935+deepme987@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cloud/1.44 Backport PRs for cloud 1.44 size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants