Skip to content

feat(website): implement sales contact form submission via HubSpot#11710

Closed
DrJKL wants to merge 6 commits intomainfrom
glary/implement-sales-form-submission
Closed

feat(website): implement sales contact form submission via HubSpot#11710
DrJKL wants to merge 6 commits intomainfrom
glary/implement-sales-form-submission

Conversation

@DrJKL
Copy link
Copy Markdown
Contributor

@DrJKL DrJKL commented Apr 28, 2026

PR Created by the Glary-Bot Agent


Summary

The /contact form's handleSubmit() was a stub (// TODO: implement form submission). Every "Contact Sales" submission has been silently dropped — including, per the team thread, an enterprise lead from Bloomberg who reported the form as broken.

This PR wires the form to HubSpot's Forms Submissions API v3 (the unauthenticated, CORS-enabled endpoint) so submissions land in the existing HubSpot contact-sales form (244637579 / 94e05eab-1373-47f7-ab5e-d84f9e6aa262). I pulled the live HubSpot form definition while building this and aligned every field accordingly.

Fixes the broken form behavior reported in the project Slack thread.

What changed

Form structure (now matches the HubSpot form definition exactly)

The HubSpot form expects a different field set than the website form was rendering. The mismatches would have produced FIELD_NOT_IN_FORM_DEFINITION and REQUIRED_FIELD errors on every submission even after wiring up the API call.

Field Before After HubSpot internal name
First Name ✓ (required) firstname
Last Name ✓ (required) lastname
Company ✓ (placeholder jane@acme.org) dropped (not in HubSpot form)
Work Email ✓ (required, type=email) email
Phone ✓ (optional) phone
Package (Individual/Teams/Enterprise) submitted as packageIndividual etc. submitted as No/Teams/Yes to_give_you_ann_idea_of_pricing_upfront__while_…
Comfy usage submitted as usingYesProduction etc. submitted as Yes, in production etc. are_youyour_team_currently_using_comfy
Who primarily builds workflows? ✓ (multi-checkbox, required) who_primarily_builds_workflows
What are you looking for? optional required comfy_intake_notes

Multi-select checkbox values are joined with ; per HubSpot's enumeration convention. Every field is sent with objectTypeId: '0-1' per HubSpot's documented schema.

Submission utility (src/utils/submitHubspotForm.ts)

  • Pure helper with fetch dependency injection, AbortController-based timeout, and a typed HubspotSubmissionError that carries HubSpot's per-field errors[] array
  • Reads the visitor's hubspotutk cookie and forwards it as context.hutk so submissions tie back to HubSpot's session tracking when the tracking script is loaded
  • The component surfaces the per-field HubSpot error message to the user (e.g. "Email is required.") instead of a generic "Something went wrong"

Configuration

  • Default Portal ID 244637579 and Form GUID 94e05eab-1373-47f7-ab5e-d84f9e6aa262 are baked into the component and documented in .env_example. These IDs are public — they appear in HubSpot's own embed code on every site that uses the form — and intentionally inlined into the static client bundle (consistent with HubSpot's official embed). They are not secrets.
  • Per-environment overrides via PUBLIC_HUBSPOT_PORTAL_ID, PUBLIC_HUBSPOT_FORM_ID_CONTACT_SALES, and PUBLIC_HUBSPOT_REGION (na1 default; only set to eu1 if the HubSpot account is hosted in the EU region).

Tests

  • 15 utility test cases (submitHubspotForm.test.ts): payload shape including objectTypeId, NA1/EU1 region switching, empty-value pruning, context handling and hubspotutk cookie reading, success body parse, HubspotSubmissionError on 400 with errors, unparseable error bodies, unconfigured-form guard, timeout/abort
  • 3 component test cases (FormSection.test.ts) using @testing-library/vue + @testing-library/user-event: end-to-end payload assertion, success state + form reset, and HubSpot per-field error surfacing
  • Wired @vitejs/plugin-vue into vitest.config.ts so .vue components can be unit-tested with happy-dom

Verification

  • pnpm typecheck (astro check): 0 errors / 0 warnings / 0 hints
  • pnpm lint: 0 errors (1 pre-existing warning in unrelated billing test)
  • pnpm format: clean
  • pnpm knip: clean
  • pnpm test:unit: 38 passed (35 pre-existing + 3 new component, 15 of which are HubSpot util)
  • Manually verified end-to-end via Playwright against the local Astro dev server with the HubSpot endpoint mocked: success path, form reset, HubSpot validation error surfacing
  • Independently confirmed the wire format with a curl POST to the real HubSpot endpoint — it returned {"redirectUri": "https://244637579.hs-sites-na2.com/intake?..."} with all expected query parameters echoed back, including the multi-value who_primarily_builds_workflows=One+dedicated+technical+owner;Small+group+of+power+users

Notes for reviewers

  • The HubSpot form has reCAPTCHA disabled — required for this API submission path. If reCAPTCHA is added back, every submission will return FORM_HAS_RECAPTCHA_ENABLED and we'll need to switch to a different integration path (HubSpot's embed JS or a server proxy that solves the challenge).

  • I did not address the broader UX bug where the visible custom-styled radio/checkbox <span> indicators sit alongside sr-only inputs — that's pre-existing and a separate piece of work.

  • Fixes the broken contact form reported in #project-new-website-brand-refresh

Screenshots

Updated /contact form: First/Last name, Work Email, Phone, package radios (Individual/Teams/Enterprise), Comfy usage radios, new Who-primarily-builds-workflows multi-checkbox group, and required What-are-you-looking-for textarea

Form after a successful HubSpot submission: form values cleared and Thanks message in yellow brand color below the submit button

Form after a HubSpot 400 validation error: per-field HubSpot message Email is required surfaced in red, field values preserved so the user can correct and retry

┆Issue is synchronized with this Notion page by Unito

The /contact form's handleSubmit was a stub, silently dropping enterprise
leads. Submit form data to HubSpot's Forms Submissions API v3
(unauthenticated, CORS-enabled) so leads land in the configured HubSpot
contact-sales form.

Form structure now matches the HubSpot form definition exactly:
- Drop the 'Company' field (not present in HubSpot definition)
- Add a required 'Work Email' field (HubSpot rejects without it)
- Add a required 'Who primarily builds workflows?' multi-checkbox group
- Mark the 'What are you looking for?' textarea as required
- Map package values to HubSpot's enumeration (Individual=No,
  Teams=Teams, Enterprise=Yes) and submit them under the form's actual
  internal property name
- Submit each field with objectTypeId='0-1' per HubSpot's schema

Submission utility:
- Add submitHubspotForm with fetch DI, abort/timeout, and a typed
  HubspotSubmissionError that carries HubSpot's per-field error array
- Read the visitor's hubspotutk tracking cookie so submissions tie back
  to HubSpot's session tracking
- Surface HubSpot's per-field validation messages to the user instead
  of a generic 'something went wrong'

Tests:
- Vitest coverage for the utility (15 cases): payload shape including
  objectTypeId, region switching, empty-value pruning, context handling,
  success body parse, HubspotSubmissionError on 400, unparseable error
  bodies, unconfigured guard, timeout/abort, and the hubspotutk cookie
  reader
- Component tests (3 cases) using @testing-library/vue + user-event:
  payload shape end-to-end, success state + form reset, and HubSpot
  error surfacing

Configuration:
- Default Portal ID and Form GUID baked into the component (and
  documented in .env_example) — these are public IDs that appear in
  HubSpot's own embed code, not secrets
- PUBLIC_HUBSPOT_PORTAL_ID, PUBLIC_HUBSPOT_FORM_ID_CONTACT_SALES, and
  PUBLIC_HUBSPOT_REGION env vars override the defaults per environment
- Wire @vitejs/plugin-vue into vitest.config so .vue components can be
  tested with happy-dom
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

🌐 Website E2E

Caution

Some tests failed.

Status ❌ Failed
Report View Report
Screenshot diffs 1
Other failures 0
  • Update website screenshots (1 screenshot diff)

🔗 Website Preview

Website Preview: https://comfy-website-preview-pr-11710.vercel.app

This commit: https://website-frontend-kztcotpgp-comfyui.vercel.app

Last updated: 2026-04-28T02:32:09Z for 294d90f

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

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

Adds a HubSpot Forms v3 client and integrates HubSpot-backed submission into the website contact form with region-aware endpoints, client-side validation, tracking cookie capture, timeoutable requests, updated translations and .env_example, tests for utilities and the form, and Vue transforms for Vitest.

Changes

Cohort / File(s) Summary
HubSpot submission utils & tests
apps/website/src/utils/submitHubspotForm.ts, apps/website/src/utils/submitHubspotForm.test.ts
New HubSpot Forms v3 client: region resolver, endpoint builder, submitHubspotForm with AbortController timeout, payload filtering, JSON response parsing, HubspotSubmissionError, readHubspotTrackingCookie, and comprehensive unit tests for success, error, timeout, and cookie parsing.
Contact form component & tests
apps/website/src/components/contact/FormSection.vue, apps/website/src/components/contact/FormSection.test.ts
Replaces placeholder submit with real HubSpot flow: env-based config resolution (with production fallbacks), trimmed required-field validation (email, lookingFor, buildsWorkflows), field mapping to HubSpot names, submit state handling, success reset and error aggregation, plus tests covering happy path, validation, HubSpot errors, and disabled-submit scenarios.
Localization & env example
.env_example, apps/website/src/i18n/translations.ts
Extends .env_example with commented PUBLIC_HUBSPOT_PORTAL_ID, PUBLIC_HUBSPOT_FORM_ID_CONTACT_SALES, and PUBLIC_HUBSPOT_REGION notes; replaces company field with email, adds required multi-option "builds workflows" question and related validation strings, adds submitting/success/error and privacy disclosure strings.
Test runner config
apps/website/vitest.config.ts
Adds Vue plugin import and plugins: [vue()] to Vitest config to enable Vue transforms during tests.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Form as FormSection.vue
    participant Utils as submitHubspotForm.ts
    participant HubSpot as HubSpot API

    User->>Form: Fill form & submit
    Form->>Form: Trim inputs & validate required fields (email, lookingFor, buildsWorkflows)
    Form->>Form: Set status = "submitting"
    Form->>Utils: submitHubspotForm({ config, context, fields })
    Utils->>Utils: resolve region, build endpoint, filter fields, add submittedAt
    Utils->>HubSpot: POST JSON payload (fields, submittedAt, context) with timeout
    alt success (2xx)
        HubSpot->>Utils: Return JSON { inlineMessage?, redirectUri? }
        Utils->>Form: Resolve with response
        Form->>Form: Set status = "success" and reset form
    else error (4xx/5xx)
        HubSpot->>Utils: Return error body (maybe JSON)
        Utils->>Form: Throw HubspotSubmissionError(status, errors)
        Form->>Form: Set status = "error" and render errorDetail
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped from field to endpoint bright,

Trimmed each line, kept errors light,
Gathered hutk and privacy lore,
Sent a POST, then asked for more,
Returned a success — carrot night delight.


Important

Pre-merge checks failed

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

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
End-To-End Regression Coverage For Fixes ❓ Inconclusive Cannot verify PR claims without access to git repository data, PR title, commit subjects, changed files list, and PR description. Provide the git log output, list of changed files, PR title, and PR description to complete verification.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title directly and concisely summarizes the main change: implementing sales contact form submission via HubSpot. It is specific, clear, and matches the primary objective of the changeset.
Description check ✅ Passed The PR description is comprehensive and well-structured. It includes a clear summary, detailed breakdown of what changed (form structure, submission utility, configuration, tests), verification steps, and notes for reviewers. All required template sections are present and thoroughly completed.
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 PR modifies only website contact form and HubSpot utilities; no changes to src/lib/litegraph/, src/ecs/, or graph entity paths.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch glary/implement-sales-form-submission

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 04/28/2026, 02:32:30 AM UTC

Links

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

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

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

@glary-bot
Copy link
Copy Markdown

glary-bot Bot commented Apr 28, 2026

Saw the "review in progress" notification — standing by for the actual review. Will address whatever findings come back.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

📦 Bundle: 5.23 MB gzip 🔴 +67 B

Details

Summary

  • Raw size: 24.1 MB baseline 24.1 MB — ⚪ 0 B
  • Gzip: 5.23 MB baseline 5.23 MB — 🔴 +67 B
  • Brotli: 4.05 MB baseline 4.05 MB — 🔴 +130 B
  • Bundles: 257 current • 257 baseline • 118 added / 118 removed

Category Glance
Vendor & Third-Party ⚪ 0 B (9.88 MB) · Other ⚪ 0 B (8.83 MB) · Data & Services ⚪ 0 B (3.04 MB) · Graph Workspace ⚪ 0 B (1.23 MB) · Panels & Settings ⚪ 0 B (484 kB) · Utilities & Hooks ⚪ 0 B (363 kB) · + 5 more

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

Main entry bundles and manifests

File Before After Δ Raw Δ Gzip Δ Brotli
assets/index-Be514glO.js (removed) 22.5 kB 🟢 -22.5 kB 🟢 -7.98 kB 🟢 -6.84 kB
assets/index-DwlQhEQx.js (new) 22.5 kB 🔴 +22.5 kB 🔴 +7.99 kB 🔴 +6.89 kB

Status: 1 added / 1 removed

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

Graph editor runtime, canvas, workflow orchestration

File Before After Δ Raw Δ Gzip Δ Brotli
assets/GraphView-BmNQHIUq.js (removed) 1.23 MB 🟢 -1.23 MB 🟢 -264 kB 🟢 -199 kB
assets/GraphView-Df9OP5bd.js (new) 1.23 MB 🔴 +1.23 MB 🔴 +264 kB 🔴 +199 kB

Status: 1 added / 1 removed

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

Top-level views, pages, and routed surfaces

File Before After Δ Raw Δ Gzip Δ Brotli
assets/CloudSurveyView-BUCCPwLJ.js (removed) 15.7 kB 🟢 -15.7 kB 🟢 -3.41 kB 🟢 -2.92 kB
assets/CloudSurveyView-jZxySCCx.js (new) 15.7 kB 🔴 +15.7 kB 🔴 +3.41 kB 🔴 +2.92 kB
assets/CloudLoginView-BcSQeQwe.js (removed) 12.2 kB 🟢 -12.2 kB 🟢 -3.43 kB 🟢 -3.04 kB
assets/CloudLoginView-DekCg4wB.js (new) 12.2 kB 🔴 +12.2 kB 🔴 +3.43 kB 🔴 +3.04 kB
assets/CloudSignupView-C_dlvXmg.js (removed) 9.94 kB 🟢 -9.94 kB 🟢 -2.93 kB 🟢 -2.57 kB
assets/CloudSignupView-LjCTakPB.js (new) 9.94 kB 🔴 +9.94 kB 🔴 +2.93 kB 🔴 +2.58 kB
assets/UserCheckView-CIIaV-2K.js (removed) 9.07 kB 🟢 -9.07 kB 🟢 -2.33 kB 🟢 -2.05 kB
assets/UserCheckView-DawBH_KN.js (new) 9.07 kB 🔴 +9.07 kB 🔴 +2.34 kB 🔴 +2.04 kB
assets/CloudLayoutView-aQiVa7C8.js (removed) 7.7 kB 🟢 -7.7 kB 🟢 -2.44 kB 🟢 -2.12 kB
assets/CloudLayoutView-ChXUuTIF.js (new) 7.7 kB 🔴 +7.7 kB 🔴 +2.44 kB 🔴 +2.1 kB
assets/CloudForgotPasswordView-DClQXZX9.js (new) 6.11 kB 🔴 +6.11 kB 🔴 +2.17 kB 🔴 +1.91 kB
assets/CloudForgotPasswordView-DoRD7DMV.js (removed) 6.11 kB 🟢 -6.11 kB 🟢 -2.17 kB 🟢 -1.9 kB
assets/CloudAuthTimeoutView-CmKANBVN.js (removed) 5.47 kB 🟢 -5.47 kB 🟢 -2 kB 🟢 -1.75 kB
assets/CloudAuthTimeoutView-DNqiJSbg.js (new) 5.47 kB 🔴 +5.47 kB 🔴 +2.01 kB 🔴 +1.75 kB
assets/CloudSubscriptionRedirectView-BS1G6WYX.js (removed) 5.25 kB 🟢 -5.25 kB 🟢 -1.98 kB 🟢 -1.75 kB
assets/CloudSubscriptionRedirectView-JS543eYb.js (new) 5.25 kB 🔴 +5.25 kB 🔴 +1.99 kB 🔴 +1.76 kB
assets/UserSelectView-B8RedoEI.js (new) 4.73 kB 🔴 +4.73 kB 🔴 +1.75 kB 🔴 +1.55 kB
assets/UserSelectView-Ba0SutbC.js (removed) 4.73 kB 🟢 -4.73 kB 🟢 -1.75 kB 🟢 -1.55 kB

Status: 9 added / 9 removed / 2 unchanged

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

Configuration panels, inspectors, and settings screens

File Before After Δ Raw Δ Gzip Δ Brotli
assets/KeybindingPanel-jqLYk0VS.js (new) 46.7 kB 🔴 +46.7 kB 🔴 +9.61 kB 🔴 +8.54 kB
assets/KeybindingPanel-k4EKa6oz.js (removed) 46.7 kB 🟢 -46.7 kB 🟢 -9.61 kB 🟢 -8.53 kB
assets/SecretsPanel-C1lXOZTd.js (removed) 22.9 kB 🟢 -22.9 kB 🟢 -5.54 kB 🟢 -4.87 kB
assets/SecretsPanel-E98N61Ro.js (new) 22.9 kB 🔴 +22.9 kB 🔴 +5.54 kB 🔴 +4.87 kB
assets/LegacyCreditsPanel-D1Yc8LHR.js (removed) 21.7 kB 🟢 -21.7 kB 🟢 -5.89 kB 🟢 -5.2 kB
assets/LegacyCreditsPanel-DxFYT9j0.js (new) 21.7 kB 🔴 +21.7 kB 🔴 +5.89 kB 🔴 +5.2 kB
assets/SubscriptionPanel-B8MQ4_h9.js (new) 19.9 kB 🔴 +19.9 kB 🔴 +5.06 kB 🔴 +4.47 kB
assets/SubscriptionPanel-cMzxyLd7.js (removed) 19.9 kB 🟢 -19.9 kB 🟢 -5.06 kB 🟢 -4.46 kB
assets/AboutPanel-50hd-pTt.js (new) 12 kB 🔴 +12 kB 🔴 +3.33 kB 🔴 +2.99 kB
assets/AboutPanel-DQJvQ0g3.js (removed) 12 kB 🟢 -12 kB 🟢 -3.33 kB 🟢 -2.99 kB
assets/ExtensionPanel-BCQGboJa.js (removed) 9.94 kB 🟢 -9.94 kB 🟢 -2.9 kB 🟢 -2.56 kB
assets/ExtensionPanel-CN4XUfXu.js (new) 9.94 kB 🔴 +9.94 kB 🔴 +2.9 kB 🔴 +2.58 kB
assets/ServerConfigPanel-CwM8UmYw.js (removed) 7.02 kB 🟢 -7.02 kB 🟢 -2.35 kB 🟢 -2.1 kB
assets/ServerConfigPanel-DwHO7ncU.js (new) 7.02 kB 🔴 +7.02 kB 🔴 +2.35 kB 🔴 +2.1 kB
assets/UserPanel-BpuVUOAA.js (new) 6.73 kB 🔴 +6.73 kB 🔴 +2.23 kB 🔴 +1.96 kB
assets/UserPanel-DcYQ62dB.js (removed) 6.73 kB 🟢 -6.73 kB 🟢 -2.23 kB 🟢 -1.95 kB
assets/cloudRemoteConfig-C3855nD2.js (new) 2.02 kB 🔴 +2.02 kB 🔴 +975 B 🔴 +835 B
assets/cloudRemoteConfig-D5j8J_I6.js (removed) 2.02 kB 🟢 -2.02 kB 🟢 -977 B 🟢 -836 B
assets/refreshRemoteConfig-DrfFZXIp.js (new) 1.45 kB 🔴 +1.45 kB 🔴 +649 B 🔴 +557 B
assets/refreshRemoteConfig-Dz-CqpjK.js (removed) 1.45 kB 🟢 -1.45 kB 🟢 -647 B 🟢 -549 B

Status: 10 added / 10 removed / 11 unchanged

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

Authentication, profile, and account management bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/auth-B_JuoesV.js (removed) 3.57 kB 🟢 -3.57 kB 🟢 -1.26 kB 🟢 -1.07 kB
assets/auth-JwjI2uxR.js (new) 3.57 kB 🔴 +3.57 kB 🔴 +1.26 kB 🔴 +1.07 kB
assets/SignUpForm-C0Rl9K3v.js (new) 3.16 kB 🔴 +3.16 kB 🔴 +1.29 kB 🔴 +1.14 kB
assets/SignUpForm-DWM45pV_.js (removed) 3.16 kB 🟢 -3.16 kB 🟢 -1.29 kB 🟢 -1.16 kB
assets/UpdatePasswordContent-aEyWTG12.js (new) 2.87 kB 🔴 +2.87 kB 🔴 +1.29 kB 🔴 +1.14 kB
assets/UpdatePasswordContent-CQar0S0m.js (removed) 2.87 kB 🟢 -2.87 kB 🟢 -1.29 kB 🟢 -1.15 kB
assets/authStore-B2Ekd_XS.js (removed) 1.16 kB 🟢 -1.16 kB 🟢 -554 B 🟢 -497 B
assets/authStore-Cw3TePDh.js (new) 1.16 kB 🔴 +1.16 kB 🔴 +554 B 🔴 +496 B
assets/auth-BeP2P5Vt.js (removed) 348 B 🟢 -348 B 🟢 -216 B 🟢 -192 B
assets/auth-uJ33-VO8.js (new) 348 B 🔴 +348 B 🔴 +218 B 🔴 +189 B

Status: 5 added / 5 removed / 2 unchanged

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

Modals, dialogs, drawers, and in-app editors

File Before After Δ Raw Δ Gzip Δ Brotli
assets/ComfyHubPublishDialog-Cl9eFccC.js (removed) 86.9 kB 🟢 -86.9 kB 🟢 -18.8 kB 🟢 -16.1 kB
assets/ComfyHubPublishDialog-DZOVcZyi.js (new) 86.9 kB 🔴 +86.9 kB 🔴 +18.8 kB 🔴 +16.1 kB
assets/useShareDialog-BQjl2TgX.js (removed) 23.7 kB 🟢 -23.7 kB 🟢 -5.77 kB 🟢 -5.12 kB
assets/useShareDialog-C_ui8oB5.js (new) 23.7 kB 🔴 +23.7 kB 🔴 +5.77 kB 🔴 +5.11 kB
assets/ComfyHubPublishDialog-Ddcgb_jA.js (new) 1.32 kB 🔴 +1.32 kB 🔴 +612 B 🔴 +542 B
assets/ComfyHubPublishDialog-Dmxnkb04.js (removed) 1.32 kB 🟢 -1.32 kB 🟢 -614 B 🟢 -553 B
assets/useSubscriptionDialog-BfOWSuhk.js (removed) 1.14 kB 🟢 -1.14 kB 🟢 -545 B 🟢 -481 B
assets/useSubscriptionDialog-CwU1-Tht.js (new) 1.14 kB 🔴 +1.14 kB 🔴 +544 B 🔴 +477 B

Status: 4 added / 4 removed

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

Reusable component library chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/ComfyQueueButton-DuVO-aSG.js (removed) 13.5 kB 🟢 -13.5 kB 🟢 -3.79 kB 🟢 -3.38 kB
assets/ComfyQueueButton-jmqEMVT-.js (new) 13.5 kB 🔴 +13.5 kB 🔴 +3.79 kB 🔴 +3.38 kB
assets/useTerminalTabs-dthLTGt4.js (removed) 11 kB 🟢 -11 kB 🟢 -3.71 kB 🟢 -3.27 kB
assets/useTerminalTabs-ghlDmMZ3.js (new) 11 kB 🔴 +11 kB 🔴 +3.72 kB 🔴 +3.27 kB
assets/SubscribeButton-D6Indg9C.js (new) 2.42 kB 🔴 +2.42 kB 🔴 +1.05 kB 🔴 +925 B
assets/SubscribeButton-D8ZejaBz.js (removed) 2.42 kB 🟢 -2.42 kB 🟢 -1.05 kB 🟢 -944 B
assets/cloudFeedbackTopbarButton-Bt38Cq0x.js (removed) 1.8 kB 🟢 -1.8 kB 🟢 -932 B 🟢 -815 B
assets/cloudFeedbackTopbarButton-DfazeDAg.js (new) 1.8 kB 🔴 +1.8 kB 🔴 +931 B 🔴 +828 B
assets/ComfyQueueButton-Ca_ak5ac.js (new) 1.24 kB 🔴 +1.24 kB 🔴 +580 B 🔴 +516 B
assets/ComfyQueueButton-DfYWagcO.js (removed) 1.24 kB 🟢 -1.24 kB 🟢 -582 B 🟢 -514 B

Status: 5 added / 5 removed / 8 unchanged

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

Stores, services, APIs, and repositories

File Before After Δ Raw Δ Gzip Δ Brotli
assets/dialogService-BvzqYs6I.js (new) 1.97 MB 🔴 +1.97 MB 🔴 +454 kB 🔴 +344 kB
assets/dialogService-YAXNIMf5.js (removed) 1.97 MB 🟢 -1.97 MB 🟢 -454 kB 🟢 -344 kB
assets/api-B4cY_Wpf.js (removed) 894 kB 🟢 -894 kB 🟢 -214 kB 🟢 -168 kB
assets/api-D6PnRFY5.js (new) 894 kB 🔴 +894 kB 🔴 +214 kB 🔴 +168 kB
assets/load3dService-CCFPCgrB.js (removed) 114 kB 🟢 -114 kB 🟢 -24.8 kB 🟢 -21.1 kB
assets/load3dService-CVJXvkCM.js (new) 114 kB 🔴 +114 kB 🔴 +24.8 kB 🔴 +21.1 kB
assets/workflowShareService-CXg38fgN.js (removed) 16.6 kB 🟢 -16.6 kB 🟢 -4.89 kB 🟢 -4.33 kB
assets/workflowShareService-D1V6uMxC.js (new) 16.6 kB 🔴 +16.6 kB 🔴 +4.89 kB 🔴 +4.34 kB
assets/keybindingService-BswlMIiB.js (removed) 13.8 kB 🟢 -13.8 kB 🟢 -3.67 kB 🟢 -3.22 kB
assets/keybindingService-CE4VOlYM.js (new) 13.8 kB 🔴 +13.8 kB 🔴 +3.67 kB 🔴 +3.22 kB
assets/releaseStore-CIeDh4Y0.js (removed) 8.12 kB 🟢 -8.12 kB 🟢 -2.28 kB 🟢 -2 kB
assets/releaseStore-DP8b-WhO.js (new) 8.12 kB 🔴 +8.12 kB 🔴 +2.28 kB 🔴 +2 kB
assets/userStore-BGoE1GM1.js (new) 2.24 kB 🔴 +2.24 kB 🔴 +872 B 🔴 +763 B
assets/userStore-Dv_qhObX.js (removed) 2.24 kB 🟢 -2.24 kB 🟢 -871 B 🟢 -761 B
assets/audioService-D1HX2HfM.js (new) 1.8 kB 🔴 +1.8 kB 🔴 +879 B 🔴 +768 B
assets/audioService-K1z-w1S8.js (removed) 1.8 kB 🟢 -1.8 kB 🟢 -877 B 🟢 -767 B
assets/releaseStore-CFelETdc.js (new) 1.16 kB 🔴 +1.16 kB 🔴 +547 B 🔴 +487 B
assets/releaseStore-DJqVpHOk.js (removed) 1.16 kB 🟢 -1.16 kB 🟢 -549 B 🟢 -489 B
assets/workflowDraftStore-B_V-6Wd_.js (removed) 1.14 kB 🟢 -1.14 kB 🟢 -543 B 🟢 -483 B
assets/workflowDraftStore-DK9G3oTG.js (new) 1.14 kB 🔴 +1.14 kB 🔴 +543 B 🔴 +478 B
assets/dialogService-3IiXDZMm.js (removed) 1.13 kB 🟢 -1.13 kB 🟢 -536 B 🟢 -479 B
assets/dialogService-BUlmHgtN.js (new) 1.13 kB 🔴 +1.13 kB 🔴 +536 B 🔴 +475 B
assets/settingStore-DD58_1bT.js (new) 1.12 kB 🔴 +1.12 kB 🔴 +538 B 🔴 +474 B
assets/settingStore-GHG9TG2_.js (removed) 1.12 kB 🟢 -1.12 kB 🟢 -539 B 🟢 -480 B
assets/assetsStore-BvuH0Jrl.js (removed) 1.12 kB 🟢 -1.12 kB 🟢 -539 B 🟢 -481 B
assets/assetsStore-CconOc9v.js (new) 1.12 kB 🔴 +1.12 kB 🔴 +538 B 🔴 +477 B

Status: 13 added / 13 removed / 4 unchanged

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

Helpers, composables, and utility bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/useConflictDetection-Bw56NPjJ.js (new) 233 kB 🔴 +233 kB 🔴 +51.8 kB 🔴 +42.2 kB
assets/useConflictDetection-CbGodjtD.js (removed) 233 kB 🟢 -233 kB 🟢 -51.8 kB 🟢 -42.2 kB
assets/useLoad3d-B-1W0--3.js (removed) 21.6 kB 🟢 -21.6 kB 🟢 -4.92 kB 🟢 -4.36 kB
assets/useLoad3d-COWC-35C.js (new) 21.6 kB 🔴 +21.6 kB 🔴 +4.92 kB 🔴 +4.37 kB
assets/useLoad3dViewer-CuCDZ6as.js (new) 20.3 kB 🔴 +20.3 kB 🔴 +4.75 kB 🔴 +4.14 kB
assets/useLoad3dViewer-DNcgDmM8.js (removed) 20.3 kB 🟢 -20.3 kB 🟢 -4.75 kB 🟢 -4.14 kB
assets/useFeatureFlags-C0T0eb5I.js (new) 5.95 kB 🔴 +5.95 kB 🔴 +1.79 kB 🔴 +1.52 kB
assets/useFeatureFlags-CeAFieEo.js (removed) 5.95 kB 🟢 -5.95 kB 🟢 -1.79 kB 🟢 -1.52 kB
assets/useCopyToClipboard-B-LZMYhB.js (removed) 5.29 kB 🟢 -5.29 kB 🟢 -1.86 kB 🟢 -1.57 kB
assets/useCopyToClipboard-C-BLJY5E.js (new) 5.29 kB 🔴 +5.29 kB 🔴 +1.86 kB 🔴 +1.57 kB
assets/useWorkspaceUI-Dm6QpSDJ.js (removed) 3.34 kB 🟢 -3.34 kB 🟢 -983 B 🟢 -810 B
assets/useWorkspaceUI-rlrsqpV1.js (new) 3.34 kB 🔴 +3.34 kB 🔴 +981 B 🔴 +807 B
assets/subscriptionCheckoutUtil-BmPe7G0c.js (removed) 3.31 kB 🟢 -3.31 kB 🟢 -1.36 kB 🟢 -1.18 kB
assets/subscriptionCheckoutUtil-HAHajhtE.js (new) 3.31 kB 🔴 +3.31 kB 🔴 +1.36 kB 🔴 +1.18 kB
assets/assetPreviewUtil-BYzLqVUM.js (new) 2.27 kB 🔴 +2.27 kB 🔴 +958 B 🔴 +841 B
assets/assetPreviewUtil-qmwk8MtP.js (removed) 2.27 kB 🟢 -2.27 kB 🟢 -959 B 🟢 -838 B
assets/useUpstreamValue-BIfQh37p.js (new) 2.08 kB 🔴 +2.08 kB 🔴 +806 B 🔴 +716 B
assets/useUpstreamValue-C4lOT0Ua.js (removed) 2.08 kB 🟢 -2.08 kB 🟢 -806 B 🟢 -715 B
assets/useLoad3d-BaNyTyQd.js (new) 1.3 kB 🔴 +1.3 kB 🔴 +604 B 🔴 +566 B
assets/useLoad3d-BDKAcD68.js (removed) 1.3 kB 🟢 -1.3 kB 🟢 -605 B 🟢 -560 B
assets/useLoad3dViewer-BCm76IS9.js (new) 1.24 kB 🔴 +1.24 kB 🔴 +572 B 🔴 +513 B
assets/useLoad3dViewer-bqjm3mBd.js (removed) 1.24 kB 🟢 -1.24 kB 🟢 -572 B 🟢 -517 B
assets/useCurrentUser-BjTk59uE.js (removed) 1.12 kB 🟢 -1.12 kB 🟢 -540 B 🟢 -480 B
assets/useCurrentUser-nwPC_Y-o.js (new) 1.12 kB 🔴 +1.12 kB 🔴 +539 B 🔴 +476 B
assets/useWorkspaceSwitch-D5hXPMNO.js (removed) 747 B 🟢 -747 B 🟢 -384 B 🟢 -331 B
assets/useWorkspaceSwitch-DVyoWrq0.js (new) 747 B 🔴 +747 B 🔴 +381 B 🔴 +330 B

Status: 13 added / 13 removed / 18 unchanged

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

External libraries and shared vendor chunks

Status: 16 unchanged

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

Bundles that do not match a named category

File Before After Δ Raw Δ Gzip Δ Brotli
assets/core-C3sRkPw4.js (removed) 76.6 kB 🟢 -76.6 kB 🟢 -19.8 kB 🟢 -16.9 kB
assets/core-DuNlOyYH.js (new) 76.6 kB 🔴 +76.6 kB 🔴 +19.8 kB 🔴 +16.9 kB
assets/groupNode-Bn2L5c_z.js (removed) 74.9 kB 🟢 -74.9 kB 🟢 -18.7 kB 🟢 -16.5 kB
assets/groupNode-xijkQPGN.js (new) 74.9 kB 🔴 +74.9 kB 🔴 +18.7 kB 🔴 +16.5 kB
assets/WidgetSelect-BjtqqSWH.js (new) 67.2 kB 🔴 +67.2 kB 🔴 +14.6 kB 🔴 +12.7 kB
assets/WidgetSelect-DNBGkCXt.js (removed) 67.2 kB 🟢 -67.2 kB 🟢 -14.6 kB 🟢 -12.7 kB
assets/SubscriptionRequiredDialogContentWorkspace-Dd4bAQ1a.js (new) 49.3 kB 🔴 +49.3 kB 🔴 +9.42 kB 🔴 +8.04 kB
assets/SubscriptionRequiredDialogContentWorkspace-DgscUCLg.js (removed) 49.3 kB 🟢 -49.3 kB 🟢 -9.41 kB 🟢 -8.05 kB
assets/Load3DControls-C3UiuJfp.js (removed) 45.9 kB 🟢 -45.9 kB 🟢 -7.47 kB 🟢 -6.53 kB
assets/Load3DControls-CBLIuLLi.js (new) 45.9 kB 🔴 +45.9 kB 🔴 +7.47 kB 🔴 +6.53 kB
assets/WidgetPainter-BEFtES_9.js (removed) 34 kB 🟢 -34 kB 🟢 -8.29 kB 🟢 -7.32 kB
assets/WidgetPainter-DKJitKNx.js (new) 34 kB 🔴 +34 kB 🔴 +8.29 kB 🔴 +7.32 kB
assets/WorkspacePanelContent-BwbZ1odI.js (new) 32.8 kB 🔴 +32.8 kB 🔴 +7 kB 🔴 +6.17 kB
assets/WorkspacePanelContent-Ce9WeIPv.js (removed) 32.8 kB 🟢 -32.8 kB 🟢 -6.99 kB 🟢 -6.17 kB
assets/Load3dViewerContent-CO1IzktF.js (removed) 28.3 kB 🟢 -28.3 kB 🟢 -5.9 kB 🟢 -5.13 kB
assets/Load3dViewerContent-YIxTA-Ut.js (new) 28.3 kB 🔴 +28.3 kB 🔴 +5.9 kB 🔴 +5.12 kB
assets/SubscriptionRequiredDialogContent-DvRIJ18D.js (removed) 27.5 kB 🟢 -27.5 kB 🟢 -6.96 kB 🟢 -6.15 kB
assets/SubscriptionRequiredDialogContent-LYeVZHZq.js (new) 27.5 kB 🔴 +27.5 kB 🔴 +6.97 kB 🔴 +6.15 kB
assets/WidgetImageCrop-Bm3Ixm9_.js (new) 24.3 kB 🔴 +24.3 kB 🔴 +6.19 kB 🔴 +5.43 kB
assets/WidgetImageCrop-htvVFSKp.js (removed) 24.3 kB 🟢 -24.3 kB 🟢 -6.19 kB 🟢 -5.44 kB
assets/SubscriptionPanelContentWorkspace-BTnL1zlb.js (new) 22.2 kB 🔴 +22.2 kB 🔴 +5.18 kB 🔴 +4.56 kB
assets/SubscriptionPanelContentWorkspace-DrSPy6Qw.js (removed) 22.2 kB 🟢 -22.2 kB 🟢 -5.18 kB 🟢 -4.56 kB
assets/SignInContent-C8nkwLBw.js (new) 20.6 kB 🔴 +20.6 kB 🔴 +5.36 kB 🔴 +4.69 kB
assets/SignInContent-kP8T7cZG.js (removed) 20.6 kB 🟢 -20.6 kB 🟢 -5.36 kB 🟢 -4.68 kB
assets/CurrentUserPopoverWorkspace-CTUyoYJf.js (new) 20.5 kB 🔴 +20.5 kB 🔴 +4.9 kB 🔴 +4.39 kB
assets/CurrentUserPopoverWorkspace-DxOkNiGN.js (removed) 20.5 kB 🟢 -20.5 kB 🟢 -4.89 kB 🟢 -4.38 kB
assets/WidgetInputNumber-DJXAPQ-u.js (new) 19.1 kB 🔴 +19.1 kB 🔴 +4.84 kB 🔴 +4.29 kB
assets/WidgetInputNumber-yXgTv1vh.js (removed) 19.1 kB 🟢 -19.1 kB 🟢 -4.84 kB 🟢 -4.29 kB
assets/Load3D-CedYyH0E.js (new) 18.2 kB 🔴 +18.2 kB 🔴 +4.34 kB 🔴 +3.8 kB
assets/Load3D-DajmC6Mk.js (removed) 18.2 kB 🟢 -18.2 kB 🟢 -4.34 kB 🟢 -3.79 kB
assets/WidgetRecordAudio-C35Qv05S.js (removed) 17.4 kB 🟢 -17.4 kB 🟢 -5 kB 🟢 -4.47 kB
assets/WidgetRecordAudio-CXyluDuZ.js (new) 17.4 kB 🔴 +17.4 kB 🔴 +5 kB 🔴 +4.47 kB
assets/WidgetRange-CnehSPgv.js (new) 17 kB 🔴 +17 kB 🔴 +4.59 kB 🔴 +4.12 kB
assets/WidgetRange-DJMSH9h7.js (removed) 17 kB 🟢 -17 kB 🟢 -4.6 kB 🟢 -4.11 kB
assets/load3d-BRBmST4z.js (removed) 15.7 kB 🟢 -15.7 kB 🟢 -4.57 kB 🟢 -3.95 kB
assets/load3d-D8E9odYQ.js (new) 15.7 kB 🔴 +15.7 kB 🔴 +4.56 kB 🔴 +3.95 kB
assets/WaveAudioPlayer-3JigF5GA.js (removed) 13.4 kB 🟢 -13.4 kB 🟢 -3.69 kB 🟢 -3.23 kB
assets/WaveAudioPlayer-WbBuMKyJ.js (new) 13.4 kB 🔴 +13.4 kB 🔴 +3.69 kB 🔴 +3.23 kB
assets/WidgetCurve-8axeaMWU.js (removed) 12.1 kB 🟢 -12.1 kB 🟢 -3.91 kB 🟢 -3.54 kB
assets/WidgetCurve-DYrj_qqw.js (new) 12.1 kB 🔴 +12.1 kB 🔴 +3.91 kB 🔴 +3.55 kB
assets/TeamWorkspacesDialogContent-B_P-RPY6.js (removed) 11.3 kB 🟢 -11.3 kB 🟢 -3.41 kB 🟢 -3.03 kB
assets/TeamWorkspacesDialogContent-DUymJgQM.js (new) 11.3 kB 🔴 +11.3 kB 🔴 +3.41 kB 🔴 +3.04 kB
assets/nodeTemplates-DIawviZb.js (new) 9.81 kB 🔴 +9.81 kB 🔴 +3.47 kB 🔴 +3.06 kB
assets/nodeTemplates-gPFw1dJJ.js (removed) 9.81 kB 🟢 -9.81 kB 🟢 -3.47 kB 🟢 -3.06 kB
assets/NightlySurveyController-bmkHpwes.js (removed) 8.99 kB 🟢 -8.99 kB 🟢 -3.15 kB 🟢 -2.78 kB
assets/NightlySurveyController-DKqtFrQ4.js (new) 8.99 kB 🔴 +8.99 kB 🔴 +3.15 kB 🔴 +2.79 kB
assets/InviteMemberDialogContent-BHOi2nQc.js (removed) 7.91 kB 🟢 -7.91 kB 🟢 -2.51 kB 🟢 -2.21 kB
assets/InviteMemberDialogContent-CAPiVjrS.js (new) 7.91 kB 🔴 +7.91 kB 🔴 +2.51 kB 🔴 +2.21 kB
assets/Load3DConfiguration-Ctl5ar3V.js (new) 7.82 kB 🔴 +7.82 kB 🔴 +2.31 kB 🔴 +2.02 kB
assets/Load3DConfiguration-VWMcVZ6L.js (removed) 7.82 kB 🟢 -7.82 kB 🟢 -2.31 kB 🟢 -2.02 kB
assets/onboardingCloudRoutes-1ZyWUocc.js (removed) 6.58 kB 🟢 -6.58 kB 🟢 -2.06 kB 🟢 -1.77 kB
assets/onboardingCloudRoutes-CD5wm16V.js (new) 6.58 kB 🔴 +6.58 kB 🔴 +2.05 kB 🔴 +1.76 kB
assets/CreateWorkspaceDialogContent-9HxnJd0y.js (removed) 6.12 kB 🟢 -6.12 kB 🟢 -2.22 kB 🟢 -1.94 kB
assets/CreateWorkspaceDialogContent-CfZ8CXgq.js (new) 6.12 kB 🔴 +6.12 kB 🔴 +2.23 kB 🔴 +1.94 kB
assets/WidgetWithControl-BQqlWgSo.js (removed) 6.02 kB 🟢 -6.02 kB 🟢 -2.4 kB 🟢 -2.15 kB
assets/WidgetWithControl-DN830HUK.js (new) 6.02 kB 🔴 +6.02 kB 🔴 +2.4 kB 🔴 +2.15 kB
assets/FreeTierDialogContent-3qnksub5.js (removed) 5.99 kB 🟢 -5.99 kB 🟢 -2.12 kB 🟢 -1.88 kB
assets/FreeTierDialogContent-D2Qll-Ki.js (new) 5.99 kB 🔴 +5.99 kB 🔴 +2.12 kB 🔴 +1.87 kB
assets/EditWorkspaceDialogContent-Cu_Gs3fB.js (new) 5.92 kB 🔴 +5.92 kB 🔴 +2.19 kB 🔴 +1.94 kB
assets/EditWorkspaceDialogContent-DGcCv8o-.js (removed) 5.92 kB 🟢 -5.92 kB 🟢 -2.19 kB 🟢 -1.92 kB
assets/WidgetTextarea-CY-MEfbi.js (removed) 5.73 kB 🟢 -5.73 kB 🟢 -2.26 kB 🟢 -2.01 kB
assets/WidgetTextarea-CYyM577t.js (new) 5.73 kB 🔴 +5.73 kB 🔴 +2.26 kB 🔴 +2 kB
assets/Preview3d-C8uHtP6D.js (new) 5.56 kB 🔴 +5.56 kB 🔴 +1.88 kB 🔴 +1.65 kB
assets/Preview3d-D-5jd9LC.js (removed) 5.56 kB 🟢 -5.56 kB 🟢 -1.88 kB 🟢 -1.64 kB
assets/ValueControlPopover-DirHx0x3.js (new) 5.5 kB 🔴 +5.5 kB 🔴 +2 kB 🔴 +1.79 kB
assets/ValueControlPopover-DyqP7c0d.js (removed) 5.5 kB 🟢 -5.5 kB 🟢 -2 kB 🟢 -1.78 kB
assets/CancelSubscriptionDialogContent-3ihHJLy4.js (new) 5.46 kB 🔴 +5.46 kB 🔴 +2.05 kB 🔴 +1.79 kB
assets/CancelSubscriptionDialogContent-D06oT3A-.js (removed) 5.46 kB 🟢 -5.46 kB 🟢 -2.05 kB 🟢 -1.79 kB
assets/DeleteWorkspaceDialogContent-DK3ySkCo.js (new) 4.82 kB 🔴 +4.82 kB 🔴 +1.86 kB 🔴 +1.62 kB
assets/DeleteWorkspaceDialogContent-FjSBY-ow.js (removed) 4.82 kB 🟢 -4.82 kB 🟢 -1.86 kB 🟢 -1.62 kB
assets/LeaveWorkspaceDialogContent-DiWSMRO6.js (new) 4.65 kB 🔴 +4.65 kB 🔴 +1.81 kB 🔴 +1.58 kB
assets/LeaveWorkspaceDialogContent-EmltBCKW.js (removed) 4.65 kB 🟢 -4.65 kB 🟢 -1.81 kB 🟢 -1.57 kB
assets/RemoveMemberDialogContent-ofmW0JEq.js (new) 4.63 kB 🔴 +4.63 kB 🔴 +1.76 kB 🔴 +1.54 kB
assets/RemoveMemberDialogContent-ZH4GI0M0.js (removed) 4.63 kB 🟢 -4.63 kB 🟢 -1.76 kB 🟢 -1.54 kB
assets/RevokeInviteDialogContent-BnBu5m9n.js (removed) 4.54 kB 🟢 -4.54 kB 🟢 -1.77 kB 🟢 -1.55 kB
assets/RevokeInviteDialogContent-UtkhPs6Q.js (new) 4.54 kB 🔴 +4.54 kB 🔴 +1.77 kB 🔴 +1.56 kB
assets/tierBenefits-azCrF5Ln.js (removed) 4.45 kB 🟢 -4.45 kB 🟢 -1.58 kB 🟢 -1.36 kB
assets/tierBenefits-BQx5BS1z.js (new) 4.45 kB 🔴 +4.45 kB 🔴 +1.57 kB 🔴 +1.36 kB
assets/InviteMemberUpsellDialogContent-BnSZcphs.js (new) 4.44 kB 🔴 +4.44 kB 🔴 +1.64 kB 🔴 +1.44 kB
assets/InviteMemberUpsellDialogContent-CeRtn_pF.js (removed) 4.44 kB 🟢 -4.44 kB 🟢 -1.64 kB 🟢 -1.43 kB
assets/cloudSessionCookie-B0FFfEQn.js (new) 4.28 kB 🔴 +4.28 kB 🔴 +1.56 kB 🔴 +1.35 kB
assets/cloudSessionCookie-R_VPgJxs.js (removed) 4.28 kB 🟢 -4.28 kB 🟢 -1.56 kB 🟢 -1.36 kB
assets/Media3DTop-BEz0r4wb.js (removed) 4.01 kB 🟢 -4.01 kB 🟢 -1.7 kB 🟢 -1.5 kB
assets/Media3DTop-DRptiYLv.js (new) 4.01 kB 🔴 +4.01 kB 🔴 +1.7 kB 🔴 +1.49 kB
assets/saveMesh-Kamkialb.js (new) 3.98 kB 🔴 +3.98 kB 🔴 +1.73 kB 🔴 +1.54 kB
assets/saveMesh-qxxzRpep.js (removed) 3.98 kB 🟢 -3.98 kB 🟢 -1.73 kB 🟢 -1.53 kB
assets/GlobalToast-DS_3n_jv.js (removed) 3.05 kB 🟢 -3.05 kB 🟢 -1.26 kB 🟢 -1.07 kB
assets/GlobalToast-OGJd4EdC.js (new) 3.05 kB 🔴 +3.05 kB 🔴 +1.26 kB 🔴 +1.07 kB
assets/CloudRunButtonWrapper-BZeKZ8Yi.js (new) 2.2 kB 🔴 +2.2 kB 🔴 +1 kB 🔴 +893 B
assets/CloudRunButtonWrapper-Cg6JpGJq.js (removed) 2.2 kB 🟢 -2.2 kB 🟢 -1 kB 🟢 -894 B
assets/SubscribeToRun-Bu-mh-5F.js (new) 2.13 kB 🔴 +2.13 kB 🔴 +981 B 🔴 +880 B
assets/SubscribeToRun-HC8TyBpD.js (removed) 2.13 kB 🟢 -2.13 kB 🟢 -984 B 🟢 -879 B
assets/MediaAudioTop-4PuQOF22.js (new) 2.08 kB 🔴 +2.08 kB 🔴 +1.01 kB 🔴 +862 B
assets/MediaAudioTop-Bqquwn8S.js (removed) 2.08 kB 🟢 -2.08 kB 🟢 -1.01 kB 🟢 -860 B
assets/cloudBadges-ByRWm5Vh.js (new) 1.93 kB 🔴 +1.93 kB 🔴 +965 B 🔴 +831 B
assets/cloudBadges-D5O-Ym4_.js (removed) 1.93 kB 🟢 -1.93 kB 🟢 -962 B 🟢 -831 B
assets/cloudSubscription-BLdUY-Ik.js (new) 1.85 kB 🔴 +1.85 kB 🔴 +886 B 🔴 +783 B
assets/cloudSubscription-uYSiAQt7.js (removed) 1.85 kB 🟢 -1.85 kB 🟢 -885 B 🟢 -767 B
assets/graphHasMissingNodes-BF6tp2oo.js (removed) 1.84 kB 🟢 -1.84 kB 🟢 -860 B 🟢 -775 B
assets/graphHasMissingNodes-BU68xUuN.js (new) 1.84 kB 🔴 +1.84 kB 🔴 +862 B 🔴 +755 B
assets/Load3D-D-IOoe7E.js (removed) 1.55 kB 🟢 -1.55 kB 🟢 -695 B 🟢 -619 B
assets/Load3D-DMS9ZwiO.js (new) 1.55 kB 🔴 +1.55 kB 🔴 +692 B 🔴 +617 B
assets/previousFullPath-DNCAqgOR.js (removed) 1.53 kB 🟢 -1.53 kB 🟢 -693 B 🟢 -618 B
assets/previousFullPath-DnMrVoFF.js (new) 1.53 kB 🔴 +1.53 kB 🔴 +692 B 🔴 +594 B
assets/nightlyBadges-CWTQVVH7.js (new) 1.46 kB 🔴 +1.46 kB 🔴 +731 B 🔴 +666 B
assets/nightlyBadges-DCC7KqWK.js (removed) 1.46 kB 🟢 -1.46 kB 🟢 -733 B 🟢 -668 B
assets/Load3dViewerContent-BzQXbDRM.js (removed) 1.43 kB 🟢 -1.43 kB 🟢 -649 B 🟢 -593 B
assets/Load3dViewerContent-CUVSXJkU.js (new) 1.43 kB 🔴 +1.43 kB 🔴 +649 B 🔴 +579 B
assets/SubscriptionPanelContentWorkspace-ctKnjlkN.js (new) 1.32 kB 🔴 +1.32 kB 🔴 +602 B 🔴 +525 B
assets/SubscriptionPanelContentWorkspace-DvKuzcQG.js (removed) 1.32 kB 🟢 -1.32 kB 🟢 -604 B 🟢 -526 B
assets/WidgetLegacy-CK8B2kvj.js (removed) 1.15 kB 🟢 -1.15 kB 🟢 -550 B 🟢 -487 B
assets/WidgetLegacy-DqX5lo9p.js (new) 1.15 kB 🔴 +1.15 kB 🔴 +550 B 🔴 +485 B
assets/changeTracker-Bwv3Wr2S.js (removed) 1.12 kB 🟢 -1.12 kB 🟢 -539 B 🟢 -472 B
assets/changeTracker-C5aOUWSj.js (new) 1.12 kB 🔴 +1.12 kB 🔴 +539 B 🔴 +469 B

Status: 57 added / 57 removed / 78 unchanged

⚡ Performance Report

canvas-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 61.8 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 57.6 MB heap
canvas-zoom-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 65.6 MB heap
dom-widget-clipping: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 48.4 MB heap
large-graph-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 54.8 MB heap
large-graph-pan: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 68.3 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 61.5 MB heap
minimap-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 56.3 MB heap
subgraph-dom-widget-clipping: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 48.9 MB heap
subgraph-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 60.8 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 53.3 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 49.5 MB heap
vue-large-graph-idle: · 58.1 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 161.4 MB heap
vue-large-graph-pan: · 56.3 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 164.0 MB heap
workflow-execution: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 53.8 MB heap

⚠️ 3 regressions detected

Metric Baseline PR (median) Δ Sig
canvas-mouse-sweep: task duration 982ms 1022ms +4% ⚠️ z=2.7
subgraph-mouse-sweep: task duration 915ms 932ms +2% ⚠️ z=2.4
workflow-execution: event listeners 37 71 +92% ⚠️ z=4.4
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 9ms +14% z=-2.0
canvas-idle: layout count 0 0 +0%
canvas-idle: style recalc count 9 10 +11% z=-2.1
canvas-idle: task duration 351ms 390ms +11% z=-0.1
canvas-idle: script duration 18ms 22ms +24% z=-1.5
canvas-idle: TBT 0ms 0ms +0%
canvas-idle: heap used 61.2 MB 61.8 MB +1%
canvas-idle: DOM nodes 18 19 +6% z=-2.8
canvas-idle: event listeners 6 6 +0% z=-1.2
canvas-mouse-sweep: avg frame time 17ms 17ms +0% z=-0.4
canvas-mouse-sweep: p95 frame time 17ms 17ms +0%
canvas-mouse-sweep: layout duration 4ms 4ms -3% z=-0.2
canvas-mouse-sweep: style recalc duration 44ms 45ms +3% z=0.8
canvas-mouse-sweep: layout count 12 12 +0%
canvas-mouse-sweep: style recalc count 82 82 +0% z=1.4
canvas-mouse-sweep: task duration 982ms 1022ms +4% ⚠️ z=2.7
canvas-mouse-sweep: script duration 121ms 138ms +14% z=0.4
canvas-mouse-sweep: TBT 0ms 0ms +0%
canvas-mouse-sweep: heap used 64.8 MB 57.6 MB -11%
canvas-mouse-sweep: DOM nodes 64 65 +2% z=1.0
canvas-mouse-sweep: event listeners 6 6 +0% z=-0.6
canvas-zoom-sweep: avg frame time 17ms 17ms +0% z=0.5
canvas-zoom-sweep: p95 frame time 17ms 17ms -1%
canvas-zoom-sweep: layout duration 1ms 1ms +4% z=0.5
canvas-zoom-sweep: style recalc duration 17ms 16ms -4% z=-1.8
canvas-zoom-sweep: layout count 6 6 +0%
canvas-zoom-sweep: style recalc count 31 32 +3% z=1.6
canvas-zoom-sweep: task duration 294ms 307ms +5% z=-0.9
canvas-zoom-sweep: script duration 20ms 22ms +13% z=-1.7
canvas-zoom-sweep: TBT 0ms 0ms +0%
canvas-zoom-sweep: heap used 65.6 MB 65.6 MB -0%
canvas-zoom-sweep: DOM nodes 76 77 +1% z=-2.8
canvas-zoom-sweep: event listeners 19 19 +0% 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 8ms +4% z=-1.9
dom-widget-clipping: layout count 0 0 +0%
dom-widget-clipping: style recalc count 11 12 +9% z=-2.2
dom-widget-clipping: task duration 336ms 340ms +1% z=-1.5
dom-widget-clipping: script duration 59ms 64ms +8% z=-1.2
dom-widget-clipping: TBT 0ms 0ms +0%
dom-widget-clipping: heap used 48.4 MB 48.4 MB -0%
dom-widget-clipping: DOM nodes 18 20 +11% z=-1.5
dom-widget-clipping: event listeners 2 2 +0% variance too high
large-graph-idle: avg frame time 17ms 17ms +0% z=-1.0
large-graph-idle: p95 frame time 17ms 17ms +0%
large-graph-idle: layout duration 0ms 0ms +0%
large-graph-idle: style recalc duration 8ms 10ms +30% z=-2.2
large-graph-idle: layout count 0 0 +0%
large-graph-idle: style recalc count 9 10 +11% z=-5.1
large-graph-idle: task duration 525ms 593ms +13% z=0.9
large-graph-idle: script duration 87ms 107ms +23% z=0.4
large-graph-idle: TBT 0ms 0ms +0%
large-graph-idle: heap used 54.2 MB 54.8 MB +1%
large-graph-idle: DOM nodes -260 -258 -1% z=-312.8
large-graph-idle: event listeners -125 -127 +2% z=-25.1
large-graph-pan: avg frame time 17ms 17ms +0% z=0.3
large-graph-pan: p95 frame time 17ms 17ms +1%
large-graph-pan: layout duration 0ms 0ms +0%
large-graph-pan: style recalc duration 18ms 17ms -6% z=-0.2
large-graph-pan: layout count 0 0 +0%
large-graph-pan: style recalc count 69 68 -1% z=-2.4
large-graph-pan: task duration 1053ms 1122ms +7% z=0.9
large-graph-pan: script duration 371ms 406ms +9% z=-0.1
large-graph-pan: TBT 0ms 0ms +0%
large-graph-pan: heap used 70.3 MB 68.3 MB -3%
large-graph-pan: DOM nodes -262 -263 +0% z=-170.9
large-graph-pan: event listeners -125 -127 +2% z=-159.3
large-graph-zoom: avg frame time 17ms 17ms +0%
large-graph-zoom: p95 frame time 17ms 17ms +1%
large-graph-zoom: layout duration 8ms 7ms -4%
large-graph-zoom: style recalc duration 18ms 18ms +3%
large-graph-zoom: layout count 60 60 +0%
large-graph-zoom: style recalc count 66 65 -2%
large-graph-zoom: task duration 1300ms 1351ms +4%
large-graph-zoom: script duration 458ms 491ms +7%
large-graph-zoom: TBT 0ms 0ms +0%
large-graph-zoom: heap used 53.4 MB 61.5 MB +15%
large-graph-zoom: DOM nodes -265 -267 +1%
large-graph-zoom: event listeners -123 -123 +0%
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 +3% z=-2.1
minimap-idle: layout count 0 0 +0%
minimap-idle: style recalc count 9 8 -11% z=-2.3
minimap-idle: task duration 543ms 543ms -0% z=0.3
minimap-idle: script duration 87ms 95ms +9% z=-0.3
minimap-idle: TBT 0ms 0ms +0%
minimap-idle: heap used 56.6 MB 56.3 MB -1%
minimap-idle: DOM nodes -264 -264 +0% z=-207.0
minimap-idle: event listeners -125 -127 +2% z=-199.3
subgraph-dom-widget-clipping: avg frame time 17ms 17ms +0% z=0.1
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 +4% z=-0.7
subgraph-dom-widget-clipping: layout count 0 0 +0%
subgraph-dom-widget-clipping: style recalc count 47 48 +2% z=0.1
subgraph-dom-widget-clipping: task duration 357ms 356ms -0% z=-1.2
subgraph-dom-widget-clipping: script duration 121ms 123ms +1% z=-0.9
subgraph-dom-widget-clipping: TBT 0ms 0ms +0%
subgraph-dom-widget-clipping: heap used 48.7 MB 48.9 MB +0%
subgraph-dom-widget-clipping: DOM nodes 20 22 +10% z=-0.2
subgraph-dom-widget-clipping: event listeners 8 8 +0% z=-1.4
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 +16% z=-0.3
subgraph-idle: layout count 0 0 +0%
subgraph-idle: style recalc count 10 10 +0% z=-1.4
subgraph-idle: task duration 358ms 363ms +2% z=-0.2
subgraph-idle: script duration 19ms 17ms -12% z=-1.3
subgraph-idle: TBT 0ms 0ms +0%
subgraph-idle: heap used 61.9 MB 60.8 MB -2%
subgraph-idle: DOM nodes 20 20 +0% z=-1.2
subgraph-idle: event listeners 6 6 +0% variance too high
subgraph-mouse-sweep: avg frame time 17ms 17ms +0% z=0.4
subgraph-mouse-sweep: p95 frame time 17ms 17ms +0%
subgraph-mouse-sweep: layout duration 5ms 4ms -3% z=-0.7
subgraph-mouse-sweep: style recalc duration 46ms 47ms +2% z=1.3
subgraph-mouse-sweep: layout count 16 16 +0%
subgraph-mouse-sweep: style recalc count 83 84 +1% z=2.0
subgraph-mouse-sweep: task duration 915ms 932ms +2% ⚠️ z=2.4
subgraph-mouse-sweep: script duration 91ms 104ms +15% z=0.5
subgraph-mouse-sweep: TBT 0ms 0ms +0%
subgraph-mouse-sweep: heap used 53.7 MB 53.3 MB -1%
subgraph-mouse-sweep: DOM nodes 71 72 +1% z=2.3
subgraph-mouse-sweep: event listeners 6 6 +0% variance too high
viewport-pan-sweep: avg frame time 17ms 17ms +0%
viewport-pan-sweep: p95 frame time 17ms 17ms -1%
viewport-pan-sweep: layout duration 0ms 0ms +0%
viewport-pan-sweep: style recalc duration 53ms 51ms -3%
viewport-pan-sweep: layout count 0 0 +0%
viewport-pan-sweep: style recalc count 251 250 -0%
viewport-pan-sweep: task duration 3857ms 3811ms -1%
viewport-pan-sweep: script duration 1288ms 1261ms -2%
viewport-pan-sweep: TBT 0ms 0ms +0%
viewport-pan-sweep: heap used 75.8 MB 49.5 MB -35%
viewport-pan-sweep: DOM nodes -258 -260 +1%
viewport-pan-sweep: event listeners -111 -111 +0%
vue-large-graph-idle: avg frame time 17ms 17ms +3%
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 10649ms 12041ms +13%
vue-large-graph-idle: script duration 569ms 595ms +4%
vue-large-graph-idle: TBT 0ms 0ms +0%
vue-large-graph-idle: heap used 154.3 MB 161.4 MB +5%
vue-large-graph-idle: DOM nodes -9850 -9850 +0%
vue-large-graph-idle: event listeners -23965 -23959 -0%
vue-large-graph-pan: avg frame time 17ms 18ms +3%
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 17ms 17ms -3%
vue-large-graph-pan: layout count 0 0 +0%
vue-large-graph-pan: style recalc count 65 66 +2%
vue-large-graph-pan: task duration 12839ms 14299ms +11%
vue-large-graph-pan: script duration 848ms 891ms +5%
vue-large-graph-pan: TBT 0ms 0ms +0%
vue-large-graph-pan: heap used 166.5 MB 164.0 MB -1%
vue-large-graph-pan: DOM nodes -9852 -9848 -0%
vue-large-graph-pan: event listeners -23961 -23955 -0%
workflow-execution: avg frame time 17ms 17ms +0% z=0.6
workflow-execution: p95 frame time 17ms 17ms -0%
workflow-execution: layout duration 1ms 2ms +21% z=1.1
workflow-execution: style recalc duration 20ms 21ms +4% z=-1.4
workflow-execution: layout count 5 5 +0% z=0.1
workflow-execution: style recalc count 13 16 +23% z=-0.9
workflow-execution: task duration 103ms 110ms +6% z=-1.2
workflow-execution: script duration 26ms 22ms -16% z=-2.4
workflow-execution: TBT 0ms 0ms +0%
workflow-execution: heap used 46.8 MB 53.8 MB +15%
workflow-execution: DOM nodes 154 154 +0% z=-1.0
workflow-execution: event listeners 37 71 +92% ⚠️ z=4.4
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-04-28T02:49:45.241Z",
  "gitSha": "bc1f773f47da8517d5ab1ff8e105bd6cd07b15cc",
  "branch": "glary/implement-sales-form-submission",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2005.0170000000094,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 7.142000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 341.41900000000004,
      "heapDeltaBytes": 20966628,
      "heapUsedBytes": 64700524,
      "domNodes": 14,
      "jsHeapTotalBytes": 22806528,
      "scriptDurationMs": 16.91399999999999,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-idle",
      "durationMs": 2026.1429999999905,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 9.905000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 436.809,
      "heapDeltaBytes": 20770356,
      "heapUsedBytes": 64753328,
      "domNodes": 19,
      "jsHeapTotalBytes": 23330816,
      "scriptDurationMs": 24.427000000000003,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-idle",
      "durationMs": 2012.540999999942,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 9.046,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 389.97499999999997,
      "heapDeltaBytes": 21133588,
      "heapUsedBytes": 66321764,
      "domNodes": 20,
      "jsHeapTotalBytes": 22806528,
      "scriptDurationMs": 21.926999999999996,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2090.822000000003,
      "styleRecalcs": 82,
      "styleRecalcDurationMs": 49.317,
      "layouts": 12,
      "layoutDurationMs": 4.023,
      "taskDurationMs": 1107.086,
      "heapDeltaBytes": 16499360,
      "heapUsedBytes": 60448224,
      "domNodes": 63,
      "jsHeapTotalBytes": 23592960,
      "scriptDurationMs": 142.443,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2066.11300000003,
      "styleRecalcs": 81,
      "styleRecalcDurationMs": 44.16100000000001,
      "layouts": 12,
      "layoutDurationMs": 3.5620000000000003,
      "taskDurationMs": 1021.55,
      "heapDeltaBytes": 15812544,
      "heapUsedBytes": 59687128,
      "domNodes": 65,
      "jsHeapTotalBytes": 24117248,
      "scriptDurationMs": 138.139,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2023.0639999999767,
      "styleRecalcs": 84,
      "styleRecalcDurationMs": 45.028,
      "layouts": 12,
      "layoutDurationMs": 3.5329999999999995,
      "taskDurationMs": 962.3739999999999,
      "heapDeltaBytes": 16493484,
      "heapUsedBytes": 60542220,
      "domNodes": 66,
      "jsHeapTotalBytes": 23068672,
      "scriptDurationMs": 128.502,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1748.5699999999724,
      "styleRecalcs": 31,
      "styleRecalcDurationMs": 16.351,
      "layouts": 6,
      "layoutDurationMs": 0.68,
      "taskDurationMs": 315.15000000000003,
      "heapDeltaBytes": 25045680,
      "heapUsedBytes": 68802980,
      "domNodes": 77,
      "jsHeapTotalBytes": 21495808,
      "scriptDurationMs": 27.959000000000003,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1732.904000000019,
      "styleRecalcs": 32,
      "styleRecalcDurationMs": 15.479,
      "layouts": 6,
      "layoutDurationMs": 0.565,
      "taskDurationMs": 279.40999999999997,
      "heapDeltaBytes": 24743228,
      "heapUsedBytes": 68780336,
      "domNodes": 77,
      "jsHeapTotalBytes": 21233664,
      "scriptDurationMs": 17.776999999999997,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1708.3089999999856,
      "styleRecalcs": 32,
      "styleRecalcDurationMs": 17.615999999999996,
      "layouts": 6,
      "layoutDurationMs": 0.667,
      "taskDurationMs": 307.32899999999995,
      "heapDeltaBytes": 24443004,
      "heapUsedBytes": 67872432,
      "domNodes": 77,
      "jsHeapTotalBytes": 20709376,
      "scriptDurationMs": 22.14,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 551.1359999999854,
      "styleRecalcs": 12,
      "styleRecalcDurationMs": 7.8069999999999995,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 339.94599999999997,
      "heapDeltaBytes": 7059572,
      "heapUsedBytes": 50825028,
      "domNodes": 20,
      "jsHeapTotalBytes": 12320768,
      "scriptDurationMs": 63.809000000000005,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 529.5559999999568,
      "styleRecalcs": 13,
      "styleRecalcDurationMs": 9.046000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 340.73199999999997,
      "heapDeltaBytes": 7035068,
      "heapUsedBytes": 50762364,
      "domNodes": 21,
      "jsHeapTotalBytes": 12582912,
      "scriptDurationMs": 65.072,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 583.8109999999688,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 8.333999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 332.19200000000006,
      "heapDeltaBytes": 6721740,
      "heapUsedBytes": 50574940,
      "domNodes": 18,
      "jsHeapTotalBytes": 12058624,
      "scriptDurationMs": 56.565000000000005,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.663333333333338,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2075.8770000000195,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 9.934999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 587.2310000000001,
      "heapDeltaBytes": 4843644,
      "heapUsedBytes": 57428476,
      "domNodes": -259,
      "jsHeapTotalBytes": 16158720,
      "scriptDurationMs": 107.19700000000002,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2010.6979999999908,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 9.677999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 593.163,
      "heapDeltaBytes": 4455820,
      "heapUsedBytes": 57107628,
      "domNodes": -257,
      "jsHeapTotalBytes": 15896576,
      "scriptDurationMs": 105.77999999999999,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2024.4089999999915,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 10.834000000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 601.992,
      "heapDeltaBytes": 5185352,
      "heapUsedBytes": 57524692,
      "domNodes": -258,
      "jsHeapTotalBytes": 16158720,
      "scriptDurationMs": 109.75100000000002,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2128.8979999999924,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 18.017,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1130.708,
      "heapDeltaBytes": 18969716,
      "heapUsedBytes": 73944760,
      "domNodes": -262,
      "jsHeapTotalBytes": 17674240,
      "scriptDurationMs": 406.286,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2115.844999999979,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 16.337,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1064.776,
      "heapDeltaBytes": -16649808,
      "heapUsedBytes": 48095460,
      "domNodes": -263,
      "jsHeapTotalBytes": 16011264,
      "scriptDurationMs": 383.773,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2103.1939999999167,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 17.169999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1122.326,
      "heapDeltaBytes": 16383304,
      "heapUsedBytes": 71660164,
      "domNodes": -264,
      "jsHeapTotalBytes": 18722816,
      "scriptDurationMs": 407.48400000000004,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3162.8729999999905,
      "styleRecalcs": 65,
      "styleRecalcDurationMs": 18.269000000000002,
      "layouts": 60,
      "layoutDurationMs": 7.263,
      "taskDurationMs": 1351.189,
      "heapDeltaBytes": 6061332,
      "heapUsedBytes": 62503420,
      "domNodes": -269,
      "jsHeapTotalBytes": 17469440,
      "scriptDurationMs": 490.763,
      "eventListeners": -123,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3114.435999999955,
      "styleRecalcs": 64,
      "styleRecalcDurationMs": 17.094999999999995,
      "layouts": 60,
      "layoutDurationMs": 7.196000000000001,
      "taskDurationMs": 1329.9509999999998,
      "heapDeltaBytes": 7687248,
      "heapUsedBytes": 64533524,
      "domNodes": -267,
      "jsHeapTotalBytes": 16158720,
      "scriptDurationMs": 490.36199999999997,
      "eventListeners": -123,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3179.8949999999877,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 18.429000000000006,
      "layouts": 60,
      "layoutDurationMs": 7.429,
      "taskDurationMs": 1387.1789999999999,
      "heapDeltaBytes": 9172504,
      "heapUsedBytes": 65747912,
      "domNodes": -264,
      "jsHeapTotalBytes": 16945152,
      "scriptDurationMs": 531.555,
      "eventListeners": -123,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "minimap-idle",
      "durationMs": 2050.0129999999785,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 7.626000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 542.793,
      "heapDeltaBytes": 3069492,
      "heapUsedBytes": 59030788,
      "domNodes": -264,
      "jsHeapTotalBytes": 16158720,
      "scriptDurationMs": 95.498,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "minimap-idle",
      "durationMs": 2050.3759999999716,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 8.167999999999997,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 518.852,
      "heapDeltaBytes": 2708720,
      "heapUsedBytes": 58692688,
      "domNodes": -264,
      "jsHeapTotalBytes": 16420864,
      "scriptDurationMs": 89.029,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "minimap-idle",
      "durationMs": 2069.19999999991,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.873999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 581.395,
      "heapDeltaBytes": 3189852,
      "heapUsedBytes": 59005860,
      "domNodes": -262,
      "jsHeapTotalBytes": 15634432,
      "scriptDurationMs": 106.74899999999998,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 575.7020000000352,
      "styleRecalcs": 46,
      "styleRecalcDurationMs": 11.332,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 375.1960000000001,
      "heapDeltaBytes": 6926820,
      "heapUsedBytes": 51268788,
      "domNodes": 18,
      "jsHeapTotalBytes": 13631488,
      "scriptDurationMs": 128.65099999999998,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.663333333333338,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 564.8780000000215,
      "styleRecalcs": 48,
      "styleRecalcDurationMs": 12.029,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 356.4510000000001,
      "heapDeltaBytes": 7031868,
      "heapUsedBytes": 51350440,
      "domNodes": 22,
      "jsHeapTotalBytes": 13369344,
      "scriptDurationMs": 122.64,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 550.5010000000539,
      "styleRecalcs": 48,
      "styleRecalcDurationMs": 12.200999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 352.31300000000005,
      "heapDeltaBytes": 6871688,
      "heapUsedBytes": 51139964,
      "domNodes": 22,
      "jsHeapTotalBytes": 12320768,
      "scriptDurationMs": 119.75999999999999,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2013.625999999988,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 10.261999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 363.125,
      "heapDeltaBytes": 19923684,
      "heapUsedBytes": 63776756,
      "domNodes": 20,
      "jsHeapTotalBytes": 23068672,
      "scriptDurationMs": 16.188999999999997,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2014.3080000000282,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 10.203,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 356.321,
      "heapDeltaBytes": 20061236,
      "heapUsedBytes": 64294008,
      "domNodes": 21,
      "jsHeapTotalBytes": 22806528,
      "scriptDurationMs": 17.349000000000004,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-idle",
      "durationMs": 1997.7039999999988,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 8.778,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 371.86499999999995,
      "heapDeltaBytes": -4816100,
      "heapUsedBytes": 45719152,
      "domNodes": 18,
      "jsHeapTotalBytes": 24903680,
      "scriptDurationMs": 16.767999999999997,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1997.5049999999896,
      "styleRecalcs": 87,
      "styleRecalcDurationMs": 46.759,
      "layouts": 16,
      "layoutDurationMs": 4.243,
      "taskDurationMs": 939.91,
      "heapDeltaBytes": 11828900,
      "heapUsedBytes": 55908072,
      "domNodes": 71,
      "jsHeapTotalBytes": 23330816,
      "scriptDurationMs": 103.58800000000001,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1990.30300000004,
      "styleRecalcs": 84,
      "styleRecalcDurationMs": 48.05200000000001,
      "layouts": 16,
      "layoutDurationMs": 4.648000000000001,
      "taskDurationMs": 931.7169999999999,
      "heapDeltaBytes": 11907516,
      "heapUsedBytes": 55886224,
      "domNodes": 72,
      "jsHeapTotalBytes": 23330816,
      "scriptDurationMs": 105.21699999999998,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1974.0070000000287,
      "styleRecalcs": 84,
      "styleRecalcDurationMs": 46.726,
      "layouts": 16,
      "layoutDurationMs": 4.485,
      "taskDurationMs": 904.9209999999999,
      "heapDeltaBytes": 11917432,
      "heapUsedBytes": 56061724,
      "domNodes": 72,
      "jsHeapTotalBytes": 22806528,
      "scriptDurationMs": 103.90599999999999,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8184.618,
      "styleRecalcs": 250,
      "styleRecalcDurationMs": 52.687,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 3811.3230000000003,
      "heapDeltaBytes": 24260408,
      "heapUsedBytes": 76686256,
      "domNodes": -260,
      "jsHeapTotalBytes": 19771392,
      "scriptDurationMs": 1255.209,
      "eventListeners": -111,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8184.405000000026,
      "styleRecalcs": 250,
      "styleRecalcDurationMs": 51.19799999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 3851.95,
      "heapDeltaBytes": -595988,
      "heapUsedBytes": 51877140,
      "domNodes": -260,
      "jsHeapTotalBytes": 19828736,
      "scriptDurationMs": 1288.889,
      "eventListeners": -111,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8163.714000000027,
      "styleRecalcs": 250,
      "styleRecalcDurationMs": 51.084,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 3714.3360000000002,
      "heapDeltaBytes": -514928,
      "heapUsedBytes": 51955688,
      "domNodes": -261,
      "jsHeapTotalBytes": 19304448,
      "scriptDurationMs": 1261.444,
      "eventListeners": -111,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 12400.514000000043,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 12390.255,
      "heapDeltaBytes": -29177812,
      "heapUsedBytes": 187340420,
      "domNodes": -9850,
      "jsHeapTotalBytes": 21557248,
      "scriptDurationMs": 591.4229999999999,
      "eventListeners": -23959,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 18.333333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 11984.163000000024,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 11974.139000000001,
      "heapDeltaBytes": -28516436,
      "heapUsedBytes": 169281136,
      "domNodes": -9848,
      "jsHeapTotalBytes": 24178688,
      "scriptDurationMs": 594.5930000000001,
      "eventListeners": -23963,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.223333333333358,
      "p95FrameDurationMs": 16.80000000000291
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 12051.299000000086,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 12040.896999999999,
      "heapDeltaBytes": -46361344,
      "heapUsedBytes": 168927816,
      "domNodes": -9850,
      "jsHeapTotalBytes": 25489408,
      "scriptDurationMs": 598.722,
      "eventListeners": -23959,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.219999999999953,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 14601.693000000012,
      "styleRecalcs": 67,
      "styleRecalcDurationMs": 17.596,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 14567.047,
      "heapDeltaBytes": -50297036,
      "heapUsedBytes": 162553416,
      "domNodes": -9848,
      "jsHeapTotalBytes": -13918208,
      "scriptDurationMs": 926.4080000000001,
      "eventListeners": -23955,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 33.29999999999927
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 14112.535999999978,
      "styleRecalcs": 65,
      "styleRecalcDurationMs": 15.887999999999957,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 14091.271,
      "heapDeltaBytes": -39194688,
      "heapUsedBytes": 174104556,
      "domNodes": -9852,
      "jsHeapTotalBytes": -25190400,
      "scriptDurationMs": 840.327,
      "eventListeners": -23953,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 14321.086000000036,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 16.735,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 14299.152000000002,
      "heapDeltaBytes": -41238060,
      "heapUsedBytes": 171938180,
      "domNodes": -9848,
      "jsHeapTotalBytes": -10248192,
      "scriptDurationMs": 891.2789999999999,
      "eventListeners": -23983,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.220000000000073,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "workflow-execution",
      "durationMs": 467.1369999999797,
      "styleRecalcs": 19,
      "styleRecalcDurationMs": 29.034000000000006,
      "layouts": 6,
      "layoutDurationMs": 1.825,
      "taskDurationMs": 153.279,
      "heapDeltaBytes": 5394616,
      "heapUsedBytes": 56742736,
      "domNodes": 167,
      "jsHeapTotalBytes": 524288,
      "scriptDurationMs": 33.437999999999995,
      "eventListeners": 71,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999727
    },
    {
      "name": "workflow-execution",
      "durationMs": 123.81199999992987,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 20.866,
      "layouts": 5,
      "layoutDurationMs": 1.712,
      "taskDurationMs": 91.054,
      "heapDeltaBytes": 3334068,
      "heapUsedBytes": 48987688,
      "domNodes": 143,
      "jsHeapTotalBytes": 262144,
      "scriptDurationMs": 19.717000000000002,
      "eventListeners": 37,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999727
    },
    {
      "name": "workflow-execution",
      "durationMs": 457.1819999999889,
      "styleRecalcs": 16,
      "styleRecalcDurationMs": 21.124000000000002,
      "layouts": 5,
      "layoutDurationMs": 1.268,
      "taskDurationMs": 110.038,
      "heapDeltaBytes": 4968796,
      "heapUsedBytes": 56440064,
      "domNodes": 154,
      "jsHeapTotalBytes": 524288,
      "scriptDurationMs": 22.034,
      "eventListeners": 71,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999727
    }
  ]
}

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: 2

🧹 Nitpick comments (1)
apps/website/src/components/contact/FormSection.test.ts (1)

33-50: Prefer label/role queries in the form helper.

This helper is currently coupled to placeholder and copy text, so minor copy changes will break several tests at once. The form already exposes labels and control roles, so getByLabelText / getByRole would keep the assertions anchored to accessible behavior instead of marketing copy.

Based on learnings: "In test files, prefer selecting or asserting on accessible properties (text content, aria-label, role, accessible name) over data-testid attributes."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/website/src/components/contact/FormSection.test.ts` around lines 33 -
50, The helper fillRequiredFields is using brittle selectors like
screen.getByPlaceholderText and screen.getByText; switch these to
accessibility-first queries: replace screen.getByPlaceholderText calls with
screen.getByLabelText using the form field labels (and keep user.type), and
replace screen.getByText button/radio clicks with screen.getByRole('button' or
'radio', { name: /accessible label/i }) so the test targets
labels/roles/accessible names rather than marketing copy; update the user.click
and user.type calls accordingly and keep the function signature and return value
(fillRequiredFields returns user) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/website/src/components/contact/FormSection.vue`:
- Around line 135-159: handleSubmit currently doesn't validate the checkbox
group and will submit when buildsWorkflows is empty; add a client-side check in
the handleSubmit function to ensure buildsWorkflows.value has at least one
selection, and if empty set status.value = 'error', populate errorDetail.value
with a user-facing message and return early (so the
HUBSPOT_FIELD_NAMES.buildsWorkflows field isn't posted empty). Apply the same
validation to the other identical submit handler referenced (the duplicate
handleSubmit block around the other occurrence) so both places enforce the
required buildsWorkflows checkbox group before calling submitHubspotForm.

In `@apps/website/src/utils/submitHubspotForm.ts`:
- Around line 139-142: The cookie parsing currently splits on '; ' which fails
when cookies have no space or include extra spaces; in the submitHubspotForm.ts
logic that computes match from cookieString, change the approach to split on ';'
(just semicolon), trim each resulting entry, find the one that startsWith
'hubspotutk=', and then return only the value after the '=' (trimmed) rather
than the whole entry; update the code paths that reference the match variable so
they extract and use the hubspotutk value correctly.

---

Nitpick comments:
In `@apps/website/src/components/contact/FormSection.test.ts`:
- Around line 33-50: The helper fillRequiredFields is using brittle selectors
like screen.getByPlaceholderText and screen.getByText; switch these to
accessibility-first queries: replace screen.getByPlaceholderText calls with
screen.getByLabelText using the form field labels (and keep user.type), and
replace screen.getByText button/radio clicks with screen.getByRole('button' or
'radio', { name: /accessible label/i }) so the test targets
labels/roles/accessible names rather than marketing copy; update the user.click
and user.type calls accordingly and keep the function signature and return value
(fillRequiredFields returns user) intact.
🪄 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: 88cf3333-e471-49c4-99a8-87ca567cc5f0

📥 Commits

Reviewing files that changed from the base of the PR and between f404887 and bd555a8.

📒 Files selected for processing (7)
  • .env_example
  • apps/website/src/components/contact/FormSection.test.ts
  • apps/website/src/components/contact/FormSection.vue
  • apps/website/src/i18n/translations.ts
  • apps/website/src/utils/submitHubspotForm.test.ts
  • apps/website/src/utils/submitHubspotForm.ts
  • apps/website/vitest.config.ts

Comment thread apps/website/src/components/contact/FormSection.vue
Comment thread apps/website/src/utils/submitHubspotForm.ts
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

@@             Coverage Diff             @@
##             main   #11710       +/-   ##
===========================================
- Coverage   69.01%   51.47%   -17.54%     
===========================================
  Files        1481     1374      -107     
  Lines       83864    70339    -13525     
  Branches    23173    18654     -4519     
===========================================
- Hits        57877    36209    -21668     
- Misses      25048    33531     +8483     
+ Partials      939      599      -340     
Flag Coverage Δ
e2e ?
unit 51.47% <ø> (+0.43%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.
see 994 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.

- Validate the workflow-builder checkbox group client-side: handleSubmit
  now blocks empty submissions with a localized error and never posts an
  empty value to HubSpot's required field.
- Mark the workflow-builder section as required in the UI (asterisk).
- Fix readHubspotTrackingCookie to handle cookie strings without spaces
  after semicolons (split on ';' + trim).
- Wrap form labels around their inputs so the form is reachable via
  getByLabelText / accessible-name queries; switch component-test
  helpers from placeholder/text queries to label/role queries; add
  coverage for the new empty-builds validation and the no-space cookie
  case.
@glary-bot
Copy link
Copy Markdown

glary-bot Bot commented Apr 28, 2026

All three pieces of feedback addressed in f00d8f1cc:

Finding Status
Major: buildsWorkflows not validated client-side Added JS guard in handleSubmit + localized error key + * on the label. (See inline reply for why I kept the JS guard instead of HTML5 :required on sr-only checkboxes.)
Minor: cookie parser fails when cookies lack spaces after ; Split on ';', trim each entry. Added two regression tests for no-space and mixed-spacing inputs.
Nitpick: test helpers were coupled to placeholder/marketing copy Wrapped form labels around inputs so they're reachable via implicit label association, then switched the test helper to getByLabelText / getByRole. Tests now anchor to accessible names.

Quality gates: 41 tests pass (was 38; +1 component test for empty-builds, +2 cookie cases), pnpm typecheck clean, pnpm knip clean, pnpm lint 0 errors, pnpm build succeeds (39 pages).

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: 2

🧹 Nitpick comments (1)
apps/website/src/components/contact/FormSection.test.ts (1)

56-98: Add one multi-select serialization case for buildsWorkflows.

This only covers the single-checkbox path right now, so a regression in the ';' join logic would still keep the suite green. One test with two selections would lock down the HubSpot-specific wire format this PR is adding.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/website/src/components/contact/FormSection.test.ts` around lines 56 -
98, Update the test in FormSection.test.ts (the "submits the HubSpot payload..."
spec that uses render(FormSection), fillRequiredFields, and submitMock) to cover
multi-select serialization for the buildsWorkflows field: after calling
fillRequiredFields, simulate selecting two options for the
who_primarily_builds_workflows input (so the form's multi-select contains two
choices), submit, then assert that the mapped field in args.fields for name
'who_primarily_builds_workflows' has value equal to the two choices joined with
';' (verifying the HubSpot ';' join logic). Keep using submitMock and the
existing args/fieldByName mapping to locate and assert the value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/website/src/components/contact/FormSection.vue`:
- Around line 233-267: Trim the free-text v-models before running validation or
sending to HubSpot: in the FormSection.vue submit handler (the method that reads
v-models firstName, lastName, email, phone, and the message/textarea model) call
.trim() on firstName, lastName, email and the message field (or use trimmed
local copies) and use those trimmed values for validation and submission so
whitespace-only input is rejected by the required checks; update the bound
variables or ensure the validation uses the trimmed values.
- Around line 26-32: The hubspotConfig currently casts
import.meta.env.PUBLIC_HUBSPOT_REGION to HubspotRegion without validating the
runtime string; change the hubspotConfig creation (the hubspotConfig constant
and its region field) to perform a runtime check against the allowed
HubspotRegion values (e.g., a small set/array of valid region strings) and only
use the env value if it matches exactly, otherwise fall back to 'na1' (and
optionally emit a console.warn or logger message); remove the raw type assertion
and ensure the runtime-validated value is assigned to region so typos like "eu"
or "EU1" don't silently produce the wrong host.

---

Nitpick comments:
In `@apps/website/src/components/contact/FormSection.test.ts`:
- Around line 56-98: Update the test in FormSection.test.ts (the "submits the
HubSpot payload..." spec that uses render(FormSection), fillRequiredFields, and
submitMock) to cover multi-select serialization for the buildsWorkflows field:
after calling fillRequiredFields, simulate selecting two options for the
who_primarily_builds_workflows input (so the form's multi-select contains two
choices), submit, then assert that the mapped field in args.fields for name
'who_primarily_builds_workflows' has value equal to the two choices joined with
';' (verifying the HubSpot ';' join logic). Keep using submitMock and the
existing args/fieldByName mapping to locate and assert the value.
🪄 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: f27aaf5d-850b-4ec2-84d6-3ec7c9ac8c0c

📥 Commits

Reviewing files that changed from the base of the PR and between bd555a8 and f00d8f1.

📒 Files selected for processing (5)
  • apps/website/src/components/contact/FormSection.test.ts
  • apps/website/src/components/contact/FormSection.vue
  • apps/website/src/i18n/translations.ts
  • apps/website/src/utils/submitHubspotForm.test.ts
  • apps/website/src/utils/submitHubspotForm.ts

Comment thread apps/website/src/components/contact/FormSection.vue
Comment thread apps/website/src/components/contact/FormSection.vue Outdated
- Trim free-text v-models with the .trim modifier and reject
  whitespace-only required fields explicitly in handleSubmit so
  whitespace-only submissions cannot pass validation.
- Validate PUBLIC_HUBSPOT_REGION at runtime via resolveHubspotRegion:
  unknown values now log a console.warn and fall back to 'na1' instead
  of silently passing through a typo to the wrong host.
- Add tests: resolveHubspotRegion (3 cases), multi-select join for
  who_primarily_builds_workflows, and whitespace-only rejection of
  required fields.
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: 3

🧹 Nitpick comments (1)
apps/website/src/components/contact/FormSection.test.ts (1)

65-69: Use the real submitHubspotForm input type here.

This inline payload shape will drift if the utility contract changes. Prefer Parameters<typeof SubmitModule.submitHubspotForm>[0] (or an exported request type) so the test stays locked to the production signature.

Based on learnings, “In TypeScript test files (e.g., any test under src), avoid duplicating interface/type definitions. Import real type definitions from the component modules under test and reference them directly, so there is a single source of truth and to prevent type drift.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/website/src/components/contact/FormSection.test.ts` around lines 65 -
69, Replace the inline payload shape with the real input type for the HubSpot
submit function so the test stays in sync with production: import the module
that exports submitHubspotForm (e.g., SubmitModule or the function export) and
type the captured arg as Parameters<typeof SubmitModule.submitHubspotForm>[0]
(or use the exported request type if available) instead of the hardcoded object
literal; update the declaration that currently asserts
submitMock.mock.calls[0][0] to use that imported type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/website/src/components/contact/FormSection.vue`:
- Around line 165-185: This change sends PII (names, email, phone, free-text
notes, page metadata and hutk via readHubspotTrackingCookie) to HubSpot from
submitHubspotForm; update the form UX and documentation to obtain and record
user consent before sending: add a mandatory consent checkbox (tied to the form
state used by FormSection.vue and validated before calling submitHubspotForm),
ensure the consent value and a timestamp are included in the submission payload
(alongside fields/context), and update the public privacy policy and any in-form
disclosure text to mention HubSpot as a processor and what data (hutk, pageUri,
pageName, contact fields) is shared.
- Around line 26-35: The hubspotConfig currently falls back to production
defaults which causes non-prod runs to submit to the live form; update the logic
in hubspotConfig and isFormConfigured so it does NOT default to the production
portalId/formGuid. Specifically, remove or replace the hard-coded production
fallbacks in the hubspotConfig object (the portalId and formGuid values) and
make isFormConfigured require explicit environment values (e.g., consider
treating empty/undefined or a special placeholder as not configured) so that
local/preview builds will fail-safe unless a non-prod sandbox or explicit config
is provided; adjust uses of hubspotConfig and isFormConfigured accordingly to
prevent accidental submissions to the live sales form.

In `@apps/website/src/utils/submitHubspotForm.ts`:
- Line 140: The current guard uses isHubspotSuccessBody(parsed) which treats any
non-null object (including arrays) as valid; update isHubspotSuccessBody to
ensure parsed is a plain object and contains the expected
HubspotSubmissionResult fields (e.g., check typeof parsed === "object" &&
!Array.isArray(parsed) and validate required keys like "inlineMessage" or
"portalId" as used elsewhere), then return parsed only if that stricter check
passes; also apply the same stricter validation to the other return site that
uses isHubspotSuccessBody in this module so malformed payloads (like []) are
rejected and an empty object is returned instead.

---

Nitpick comments:
In `@apps/website/src/components/contact/FormSection.test.ts`:
- Around line 65-69: Replace the inline payload shape with the real input type
for the HubSpot submit function so the test stays in sync with production:
import the module that exports submitHubspotForm (e.g., SubmitModule or the
function export) and type the captured arg as Parameters<typeof
SubmitModule.submitHubspotForm>[0] (or use the exported request type if
available) instead of the hardcoded object literal; update the declaration that
currently asserts submitMock.mock.calls[0][0] to use that imported type.
🪄 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: 0e586f4e-0359-412c-9bd4-0b6d3dc962aa

📥 Commits

Reviewing files that changed from the base of the PR and between f00d8f1 and 968d997.

📒 Files selected for processing (5)
  • apps/website/src/components/contact/FormSection.test.ts
  • apps/website/src/components/contact/FormSection.vue
  • apps/website/src/i18n/translations.ts
  • apps/website/src/utils/submitHubspotForm.test.ts
  • apps/website/src/utils/submitHubspotForm.ts

Comment thread apps/website/src/components/contact/FormSection.vue
Comment on lines +165 to +185
await submitHubspotForm({
config: hubspotConfig,
fields: [
field(HUBSPOT_FIELD_NAMES.firstName, trimmedFirstName),
field(HUBSPOT_FIELD_NAMES.lastName, trimmedLastName),
field(HUBSPOT_FIELD_NAMES.email, trimmedEmail),
field(HUBSPOT_FIELD_NAMES.phone, trimmedPhone),
field(HUBSPOT_FIELD_NAMES.package, selectedPackage.value),
field(HUBSPOT_FIELD_NAMES.comfyUsage, comfyUsage.value),
field(
HUBSPOT_FIELD_NAMES.buildsWorkflows,
buildsWorkflows.value.join(';')
),
field(HUBSPOT_FIELD_NAMES.lookingFor, trimmedLookingFor)
],
context: {
hutk: readHubspotTrackingCookie(),
pageUri:
typeof window === 'undefined' ? undefined : window.location.href,
pageName: typeof document === 'undefined' ? undefined : document.title
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Ship the HubSpot disclosure/policy update with this PII handoff.

This submission now sends name, work email, phone, free-text notes, page metadata, and hutk to HubSpot. I don’t see a matching disclosure/consent update in the form UX or an accompanying privacy-policy update in the changed surface, so the site would start sharing contact-form PII with a new processor without telling users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/website/src/components/contact/FormSection.vue` around lines 165 - 185,
This change sends PII (names, email, phone, free-text notes, page metadata and
hutk via readHubspotTrackingCookie) to HubSpot from submitHubspotForm; update
the form UX and documentation to obtain and record user consent before sending:
add a mandatory consent checkbox (tied to the form state used by FormSection.vue
and validated before calling submitHubspotForm), ensure the consent value and a
timestamp are included in the submission payload (alongside fields/context), and
update the public privacy policy and any in-form disclosure text to mention
HubSpot as a processor and what data (hutk, pageUri, pageName, contact fields)
is shared.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Partially addressed in 56e4e79ac, partially pushing back.

What I changed: added a privacy disclosure paragraph immediately under the submit button, in en + zh-CN, that explicitly names HubSpot as the CRM/processor and links to /privacy-policy. So the data sharing is now disclosed at point of collection rather than only in the policy footer.

What I'm pushing back on:

  1. Mandatory consent checkbox tied to the form. The HubSpot form definition (managed in the HubSpot UI, not in this repo) is the source of truth for required fields. I confirmed the live form's required-field list earlier in this PR; consent isn't one of them. Adding a consent: true field to the submission payload would either be silently ignored by HubSpot or fail with FIELD_NOT_IN_FORM_DEFINITION. The right place to add a GDPR/CCPA consent capture is on the HubSpot form itself (HubSpot has a built-in "Legitimate Interest" / "Process Data" / "Communications" consent module), and once it's added there the field internal name comes back through the same render-definition endpoint and we can wire it up. That's a separate change owned by sales/marketing ops.

  2. Privacy-policy text update. The /privacy-policy page is data-driven from i18n/translations.ts (the privacy.* keys). The existing intro already covers third-party processors generically (block 2: "In the event our site contains links to third-party sites and services, please be aware that those sites and services have their own privacy policies"). Whether HubSpot needs to be named explicitly there — and how — is a legal/ops call, not a frontend code change. Happy to add a sub-task or open a separate PR if the team wants the disclosure expanded there too, but I don't want to author that text unilaterally on this PR.

  3. Consent timestamp in the payload. Same reason as (1): the HubSpot form schema doesn't have a slot for it, and HubSpot already records the submission timestamp server-side via submittedAt.

So this PR closes the in-form disclosure gap; the HubSpot-side consent module + privacy-policy text expansion belong in a follow-up that loops in legal/ops.

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.

Skipped: comment is from another GitHub bot.

)
}

return isHubspotSuccessBody(parsed) ? parsed : {}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tighten the 2xx response guard.

isHubspotSuccessBody() currently accepts any non-null object, so a malformed successful payload like [] can be returned from Line 140 as a HubspotSubmissionResult. Restrict this to plain objects, or validate the fields you actually consume.

♻️ Proposed fix
 function isHubspotSuccessBody(
   value: unknown
 ): value is HubspotSubmissionResult {
-  return typeof value === 'object' && value !== null
+  return (
+    typeof value === 'object' &&
+    value !== null &&
+    !Array.isArray(value)
+  )
 }

Also applies to: 179-183

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/website/src/utils/submitHubspotForm.ts` at line 140, The current guard
uses isHubspotSuccessBody(parsed) which treats any non-null object (including
arrays) as valid; update isHubspotSuccessBody to ensure parsed is a plain object
and contains the expected HubspotSubmissionResult fields (e.g., check typeof
parsed === "object" && !Array.isArray(parsed) and validate required keys like
"inlineMessage" or "portalId" as used elsewhere), then return parsed only if
that stricter check passes; also apply the same stricter validation to the other
return site that uses isHubspotSuccessBody in this module so malformed payloads
(like []) are rejected and an empty object is returned instead.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed in 56e4e79ac. Applied your suggested diff verbatim — isHubspotSuccessBody now also checks !Array.isArray(value) so a malformed [] from the success branch is rejected and we fall through to the empty-object return on line 140.

Added a regression test that fulfills the mock with [1, 2, 3] as the JSON body and asserts submitHubspotForm resolves with {} instead of leaking the array.

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.

Skipped: comment is from another GitHub bot.

…esponse parser

- Production HubSpot Portal ID and Form GUID are now used only when
  import.meta.env.PROD is true. Local dev and Vercel preview deploys
  fail-safe with the submit button disabled unless the env vars are
  explicitly set, so non-prod runs cannot pollute the live CRM.
- Add a privacy disclosure under the submit button (en + zh-CN) noting
  that submissions are sent to HubSpot, with a link to /privacy-policy.
- Tighten isHubspotSuccessBody to reject arrays so a malformed 200
  response cannot be returned as a HubspotSubmissionResult.
- Replace inline payload type in the component test with the real
  Parameters<typeof submitHubspotForm>[0] so the test stays in sync
  with the production signature.
- Add tests for the prod-gating fail-safe and the array-response guard.
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: 2

🧹 Nitpick comments (2)
apps/website/src/utils/submitHubspotForm.test.ts (2)

278-282: Restore console.warn after the suite to avoid global mock leakage.

Line 278 installs a spy, but Line 281 only clears call history. Add restore teardown so the global console.warn implementation is always put back.

♻️ Suggested fix
-import { afterEach, describe, expect, it, vi } from 'vitest'
+import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'
...
 describe('resolveHubspotRegion', () => {
   const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})

   afterEach(() => {
     warnSpy.mockClear()
   })
+
+  afterAll(() => {
+    warnSpy.mockRestore()
+  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/website/src/utils/submitHubspotForm.test.ts` around lines 278 - 282, The
test installs a spy via warnSpy = vi.spyOn(console, 'warn') but only calls
warnSpy.mockClear() in afterEach, which leaves the global console.warn mocked;
update the teardown to restore the original implementation by calling
warnSpy.mockRestore() (or warnSpy.mockReset() followed by mockRestore) — either
in afterEach alongside mockClear or in afterAll — so that console.warn is
returned to its original implementation after the suite.

245-261: Use a table-driven test for cookie separator variants.

These three cases assert the same behavior with different inputs; collapsing them into it.each keeps intent and reduces repetition.

♻️ Suggested refactor
-  it('reads the hubspotutk cookie value with spaces after semicolons', () => {
-    expect(
-      readHubspotTrackingCookie('foo=bar; hubspotutk=abc123; baz=qux')
-    ).toBe('abc123')
-  })
-
-  it('reads the hubspotutk cookie value when separators have no spaces', () => {
-    expect(readHubspotTrackingCookie('foo=bar;hubspotutk=abc123;baz=qux')).toBe(
-      'abc123'
-    )
-  })
-
-  it('reads the hubspotutk cookie value when separators are mixed', () => {
-    expect(
-      readHubspotTrackingCookie('foo=bar; hubspotutk=abc123;baz=qux')
-    ).toBe('abc123')
-  })
+  it.each([
+    'foo=bar; hubspotutk=abc123; baz=qux',
+    'foo=bar;hubspotutk=abc123;baz=qux',
+    'foo=bar; hubspotutk=abc123;baz=qux'
+  ])('reads the hubspotutk cookie value from "%s"', (cookie) => {
+    expect(readHubspotTrackingCookie(cookie)).toBe('abc123')
+  })
As per coding guidelines: "Do not write redundant tests; follow composable testing patterns".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/website/src/utils/submitHubspotForm.test.ts` around lines 245 - 261,
Replace the three redundant tests that call readHubspotTrackingCookie with
different cookie-separator strings by converting them into a single table-driven
test using it.each; create an array of input strings like 'foo=bar;
hubspotutk=abc123; baz=qux', 'foo=bar;hubspotutk=abc123;baz=qux', and 'foo=bar;
hubspotutk=abc123;baz=qux' and assert each yields 'abc123' in one it.each block
that references readHubspotTrackingCookie to remove repetition and follow
composable testing patterns.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/website/src/components/contact/FormSection.test.ts`:
- Around line 10-16: The tests import FormSection at module load so
hubspotConfig and isFormConfigured are evaluated with the wrong env; to fix,
call vi.resetModules() in a beforeEach, stub import.meta.env (use vi.stubEnv)
for each scenario before importing, then dynamically import FormSection (e.g.,
await import('.../FormSection.vue')) after stubbing so the module evaluates with
the desired environment; update both the "configured" and "unconfigured" test
setups that reference FormSection, hubspotConfig, and isFormConfigured to follow
this pattern.

In `@apps/website/src/i18n/translations.ts`:
- Around line 3406-3410: The contact.form.privacyDisclosure string in
translations.ts currently hardcodes the anchor href (/privacy-policy); remove
the hardcoded link markup and replace it with a placeholder (e.g., a token or
just plain text surrounding the "Privacy Policy" text) so the localized copy
contains only text and a link placeholder, then in FormSection.vue render the
anchor around that placeholder using the locale-aware path returned by
localePath('privacy-policy') (or localePath('/privacy-policy') consistent with
the app) so the link respects locale routing while keeping the translated text
in translations.ts.

---

Nitpick comments:
In `@apps/website/src/utils/submitHubspotForm.test.ts`:
- Around line 278-282: The test installs a spy via warnSpy = vi.spyOn(console,
'warn') but only calls warnSpy.mockClear() in afterEach, which leaves the global
console.warn mocked; update the teardown to restore the original implementation
by calling warnSpy.mockRestore() (or warnSpy.mockReset() followed by
mockRestore) — either in afterEach alongside mockClear or in afterAll — so that
console.warn is returned to its original implementation after the suite.
- Around line 245-261: Replace the three redundant tests that call
readHubspotTrackingCookie with different cookie-separator strings by converting
them into a single table-driven test using it.each; create an array of input
strings like 'foo=bar; hubspotutk=abc123; baz=qux',
'foo=bar;hubspotutk=abc123;baz=qux', and 'foo=bar; hubspotutk=abc123;baz=qux'
and assert each yields 'abc123' in one it.each block that references
readHubspotTrackingCookie to remove repetition and follow composable testing
patterns.
🪄 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: 96e844cb-a798-4684-90e8-0262f3ff674e

📥 Commits

Reviewing files that changed from the base of the PR and between 968d997 and 56e4e79.

📒 Files selected for processing (5)
  • apps/website/src/components/contact/FormSection.test.ts
  • apps/website/src/components/contact/FormSection.vue
  • apps/website/src/i18n/translations.ts
  • apps/website/src/utils/submitHubspotForm.test.ts
  • apps/website/src/utils/submitHubspotForm.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/website/src/utils/submitHubspotForm.ts

Comment on lines +10 to +16
beforeAll(() => {
vi.stubEnv('PUBLIC_HUBSPOT_PORTAL_ID', '244637579')
vi.stubEnv(
'PUBLIC_HUBSPOT_FORM_ID_CONTACT_SALES',
'94e05eab-1373-47f7-ab5e-d84f9e6aa262'
)
})
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/website/src/components/contact/FormSection.test.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 9410


🏁 Script executed:

cat -n apps/website/src/components/contact/FormSection.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 18421


🏁 Script executed:

# Check if import.meta.env values are accessed at module scope in FormSection.vue
rg -A 5 "const hubspotConfig|const isFormConfigured" apps/website/src/components/contact/FormSection.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 431


🏁 Script executed:

# Verify the import order and hook timing in the test file
head -50 apps/website/src/components/contact/FormSection.test.ts | rg -n "beforeAll|import FormSection|vi.stubEnv"

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 286


🏁 Script executed:

# Check the component rendering to confirm isFormConfigured is used to control button state
rg -B 2 -A 2 ":disabled=.*isFormConfigured" apps/website/src/components/contact/FormSection.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 230


Stub import.meta.env before loading FormSection.

FormSection.vue builds hubspotConfig and isFormConfigured at module evaluation time. This file statically imports the component on line 41, which executes during test file initialization before beforeAll hooks run. As a result, the component captures whatever env state existed when the test file loaded, not the state set up by subsequent env stubs. Both test scenarios (configured and unconfigured) exercise the same component snapshot, making the assertions unreliable. Use vi.resetModules() in beforeEach, convert the import to dynamic, and re-import after each env stub/unstub so each test case actually evaluates the component with its intended environment.

💡 Minimal pattern to isolate the env-dependent cases
-import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
...
-beforeAll(() => {
+beforeEach(() => {
+  vi.resetModules()
   vi.stubEnv('PUBLIC_HUBSPOT_PORTAL_ID', '244637579')
   vi.stubEnv(
     'PUBLIC_HUBSPOT_FORM_ID_CONTACT_SALES',
     '94e05eab-1373-47f7-ab5e-d84f9e6aa262'
   )
 })
...
-import FormSection from './FormSection.vue'
+async function renderFormSection() {
+  const { default: FormSection } = await import('./FormSection.vue')
+  return render(FormSection)
+}
...
-    render(FormSection)
+    await renderFormSection()
...
-  beforeAll(() => {
-    vi.unstubAllEnvs()
-  })
-
-  it('disables the submit button when neither env nor production defaults provide IDs', () => {
-    render(FormSection)
+  it('disables the submit button when neither env nor production defaults provide IDs', async () => {
+    vi.unstubAllEnvs()
+    vi.resetModules()
+    await renderFormSection()

Also applies to: 41-41, 216-219

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/website/src/components/contact/FormSection.test.ts` around lines 10 -
16, The tests import FormSection at module load so hubspotConfig and
isFormConfigured are evaluated with the wrong env; to fix, call
vi.resetModules() in a beforeEach, stub import.meta.env (use vi.stubEnv) for
each scenario before importing, then dynamically import FormSection (e.g., await
import('.../FormSection.vue')) after stubbing so the module evaluates with the
desired environment; update both the "configured" and "unconfigured" test setups
that reference FormSection, hubspotConfig, and isFormConfigured to follow this
pattern.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pushing back on this one — the analysis is wrong about Vue 3 SFC evaluation timing.

Vue 3 <script setup> compiles the body into a setup(__props) function on the component definition. import.meta.env.PUBLIC_HUBSPOT_PORTAL_ID is read inside that function, which runs once per render, not at import time. So the static import FormSection from './FormSection.vue' only registers the component definition; nothing about hubspotConfig is captured until render(FormSection) is called inside each test.

I verified this empirically with a throwaway probe test before deciding:

// import FormSection statically at the top
import FormSection from './FormSection.vue'

it('A: with no stubs, button disabled', () => {
  vi.unstubAllEnvs()
  render(FormSection)
  expect(submitButton().disabled).toBe(true)  // ✓
})

it('B: with stubs (applied AFTER static import), button enabled', () => {
  vi.stubEnv('PUBLIC_HUBSPOT_PORTAL_ID', '999')
  vi.stubEnv('PUBLIC_HUBSPOT_FORM_ID_CONTACT_SALES', 'guid')
  render(FormSection)
  expect(submitButton().disabled).toBe(false)  // ✓
})

Both pass. If hubspotConfig were captured at import time, B would also see the empty values and the button would still be disabled. The existing tests already rely on this — the configured tests assert args.config.portalId === '244637579' (the stubbed value), and the unconfigured test sees disabled === true. Both states are observable, which proves the env reads happen at render time.

So vi.stubEnv in beforeAll + vi.unstubAllEnvs() in the second describe's beforeAll is the simpler, faster pattern here. Switching to vi.resetModules() + dynamic await import('./FormSection.vue') per test would re-compile the SFC each time (much slower) without changing observable behavior.

Happy to revisit if I'm missing something — e.g., if the Vue compiler is configured to hoist import.meta.env reads to module scope in this project's setup, the analysis would change. From the compiled output I see in the test runner, that's not happening here.

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.

Skipped: comment is from another GitHub bot.

Comment thread apps/website/src/i18n/translations.ts Outdated
…kie tests

- Render the privacy-policy link in the disclosure with the locale-aware
  path from getRoutes(locale).privacyPolicy instead of a hardcoded
  /privacy-policy. The i18n string is split into a before/link-label/
  after triple so zh-CN visitors land on /zh-CN/privacy-policy.
- Restore the global console.warn implementation in afterAll for the
  resolveHubspotRegion describe block so the warn spy doesn't leak
  across files.
- Collapse the three cookie-separator-variant tests into a single
  it.each block per the project's "no redundant tests" guideline.
- Add component tests covering both the en and zh-CN privacy-policy
  link hrefs to lock the locale routing in place.
@glary-bot
Copy link
Copy Markdown

glary-bot Bot commented Apr 28, 2026

Latest review's 2 nitpicks also addressed in 5563f0b09:

  • console.warn spy restoration: added afterAll(() => warnSpy.mockRestore()) to the resolveHubspotRegion describe block so the spy doesn't leak across test files.
  • Redundant cookie tests: collapsed the three separator-variant tests into a single it.each per the project's "no redundant tests" guideline.

The Major (env-stub timing) finding I'm pushing back on with empirical evidence — see the inline reply on that thread. tl;dr: Vue 3 <script setup> reads import.meta.env per render inside setup(__props), not at import time, so the existing static-import + vi.stubEnv pattern works correctly. I verified with a throwaway probe before deciding.

Quality gates: 50 tests pass (was 48; +2 for the new locale-aware link assertions), typecheck/knip/lint clean, build succeeds.

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.

♻️ Duplicate comments (1)
apps/website/src/components/contact/FormSection.vue (1)

33-44: ⚠️ Potential issue | 🟠 Major

import.meta.env.PROD still points preview/staging production builds at the live HubSpot form.

PROD here means "built/run in production mode," not "this is the one true production deployment." A preview or staging deployment built in production mode will still pick up these hard-coded IDs when the overrides are missing, so the CRM-pollution risk from the earlier review is still present. Please gate the fallback on an explicit deployment-environment signal, or require explicit IDs outside the live site. The current fail-safe test only covers test mode, so it won’t catch this path. Based on learnings: 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.

In Vite, what do `import.meta.env.PROD` and `import.meta.env.DEV` mean, and is `PROD` true for preview or staging deployments that are built in production mode?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/website/src/components/contact/FormSection.vue` around lines 33 - 44,
The current fallback uses import.meta.env.PROD which is true for any build with
production mode and can cause preview/staging to use live HubSpot IDs; update
hubspotConfig and isFormConfigured so the hard-coded PROD_HUBSPOT_* IDs are only
used when an explicit deployment marker indicates real production (e.g., a new
env var like import.meta.env.PUBLIC_DEPLOYMENT_ENV === 'production' or
PUBLIC_USE_LIVE_HUBSPOT === 'true'), otherwise require explicit
PUBLIC_HUBSPOT_PORTAL_ID and PUBLIC_HUBSPOT_FORM_ID_CONTACT_SALES to be present
and treat missing values as unconfigured; adjust resolveBranch in the fallback
logic around hubspotConfig.portalId, hubspotConfig.formGuid,
import.meta.env.PROD and references to
PROD_HUBSPOT_PORTAL_ID/PROD_HUBSPOT_FORM_ID_CONTACT_SALES to gate on the
explicit deploy var.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/website/src/components/contact/FormSection.vue`:
- Around line 33-44: The current fallback uses import.meta.env.PROD which is
true for any build with production mode and can cause preview/staging to use
live HubSpot IDs; update hubspotConfig and isFormConfigured so the hard-coded
PROD_HUBSPOT_* IDs are only used when an explicit deployment marker indicates
real production (e.g., a new env var like import.meta.env.PUBLIC_DEPLOYMENT_ENV
=== 'production' or PUBLIC_USE_LIVE_HUBSPOT === 'true'), otherwise require
explicit PUBLIC_HUBSPOT_PORTAL_ID and PUBLIC_HUBSPOT_FORM_ID_CONTACT_SALES to be
present and treat missing values as unconfigured; adjust resolveBranch in the
fallback logic around hubspotConfig.portalId, hubspotConfig.formGuid,
import.meta.env.PROD and references to
PROD_HUBSPOT_PORTAL_ID/PROD_HUBSPOT_FORM_ID_CONTACT_SALES to gate on the
explicit deploy var.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a0e96045-62ed-4b28-9b73-07cb151096df

📥 Commits

Reviewing files that changed from the base of the PR and between 56e4e79 and 5563f0b.

📒 Files selected for processing (4)
  • apps/website/src/components/contact/FormSection.test.ts
  • apps/website/src/components/contact/FormSection.vue
  • apps/website/src/i18n/translations.ts
  • apps/website/src/utils/submitHubspotForm.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/website/src/i18n/translations.ts

…back

import.meta.env.PROD is true for any production-mode build, including
Vercel preview deploys. The previous fallback meant preview/staging
deploys could submit test entries to the live HubSpot CRM whenever the
env vars weren't set. Drop the hardcoded fallback entirely so every
environment (production included) must set PUBLIC_HUBSPOT_PORTAL_ID and
PUBLIC_HUBSPOT_FORM_ID_CONTACT_SALES explicitly. When unset the submit
button is disabled and a console.warn flags the misconfiguration.

.env_example documents the production values in a comment for ops to
copy into Vercel project settings.

Renamed the fail-safe describe block to drop the now-misleading
"preview/dev" qualifier, since the fail-safe applies in every
environment.
@glary-bot
Copy link
Copy Markdown

glary-bot Bot commented Apr 28, 2026

CodeRabbit's re-flag was correct — import.meta.env.PROD is true for any production-mode build, including Vercel preview deploys, so my earlier PROD-gate didn't actually protect preview/staging from sending submissions to the live HubSpot CRM. Fixed in 294d90f36.

What changed

  • Dropped the hardcoded production Portal ID + Form GUID fallbacks entirely. The component now reads PUBLIC_HUBSPOT_PORTAL_ID and PUBLIC_HUBSPOT_FORM_ID_CONTACT_SALES from env directly, defaults to '', and the existing isFormConfigured check disables the submit button when either is missing. A console.warn fires on first render in any environment where the form is unconfigured, matching the pattern this repo already uses for the Ashby integration.
  • .env_example now documents the production values in a comment for ops to copy into Vercel project settings, with the actual env var lines left empty so the fail-safe applies by default.

⚠️ Action item for ops before merging

This PR will ship the contact form in a disabled state on production unless someone sets these in Vercel → Project Settings → Environment Variables (Production scope only):

PUBLIC_HUBSPOT_PORTAL_ID=244637579
PUBLIC_HUBSPOT_FORM_ID_CONTACT_SALES=94e05eab-1373-47f7-ab5e-d84f9e6aa262

Leave them unset for Preview and Development scopes so those deploys keep failing safe. <@U09UNN70STE> can you grab those before this lands?

Quality gates: 50 tests pass, typecheck/knip/lint clean, build succeeds with 39 pages.

@benceruleanlu
Copy link
Copy Markdown
Member

A lower diff PR in the meantime is #11723, but I have an open mind, we could also take this' implementation, just unsure why HubSpot has this as a legacy v3 API.

@glary-bot
Copy link
Copy Markdown

glary-bot Bot commented Apr 28, 2026

Replying to <@benceruleanlu>'s comment:

Thanks for flagging — totally happy to defer to whichever path the team wants. These two PRs are exactly the two non-deprecated paths to HubSpot that the original Slack thread enumerated:

#11723 (HubSpot embed) This PR (Forms API v3)
Diff size small larger
Form rendering HubSpot's hosted iframe + their styling Our own Vue UI, brand styles, BrandButton, etc.
Field structure whatever HubSpot configures, no app code change locked to the HubSpot form definition I pulled (firstname/lastname/email/phone + the three custom radio/checkbox/textarea)
i18n HubSpot's locale handling (limited) Full en + zh-CN via translations.ts, including the new privacy disclosure
Validation HubSpot's Both client-side (HTML5 required, .trim, JS guards for the multi-checkbox) and HubSpot-side, with HubSpot's per-field error messages surfaced in our UI
Tracking HubSpot script loads, sets/reads hubspotutk itself, plus pulls in their full tracking pixel We just read the hubspotutk cookie if their tracking script is loaded by something else (e.g. GTM). No third-party script dependency added by this PR.
CSP needs script-src https://js-na2.hsforms.net needs connect-src https://api.hsforms.com
Bloomberg-style "form is broken" repros Browser blocks the third-party script → silent failure Submit button disabled → fail-safe

"unsure why HubSpot has this as a legacy v3 API"

Fair concern — I read it the same way at first. After looking at the official OpenAPI spec, the answer is: it's only categorized as "legacy" in HubSpot's docs nav, not actually deprecated. There's no deprecated: true on the endpoint, no announced sunset, and no v4 successor — submissions/v3/integration/submit/{portalId}/{formGuid} is still the documented path for "send form data from a non-HubSpot frontend". HubSpot reorganized their docs in 2024 and put the older HubSpot CMS-paired Forms APIs under the "legacy" tree, but this unauthenticated submissions endpoint is the same surface the embed script (#11723's path) calls under the hood. There IS a single deprecated parameter (skipValidation) inside the schema, but I'm not using it.

"we could also take this' implementation"

If the design lean is "keep our own brand-styled form, don't iframe HubSpot's UI on the contact page", this PR is ready (modulo the env-var rollout I flagged in the previous comment). If the lean is "smaller diff, less code to maintain, accept HubSpot's iframe styling", #11723 is the cleaner pick — and the Bloomberg lead-loss issue gets fixed either way.

I'd let you and <@U09UNN70STE>'s equivalent here make the call — both paths land submissions in the same HubSpot form, so the CRM side is identical. One thing worth noting: a few of the items I caught while writing this PR (HubSpot's form requires fields the website form doesn't surface, the multi-select join semantics, the hutk propagation, etc.) won't be issues in #11723 because HubSpot's iframe renders directly from their form definition — so that path is also lower risk.

Happy to close this in favor of #11723 if that's the call. Or if you'd prefer to take this implementation, the open ops action is the env-var rollout I flagged just above.

@benceruleanlu benceruleanlu removed their assignment Apr 30, 2026
@DrJKL DrJKL closed this Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants