feat(telemetry): instrument cloud funnel gaps (auth, onboarding, canvas, output) - #12894
feat(telemetry): instrument cloud funnel gaps (auth, onboarding, canvas, output)#12894deepme987 wants to merge 27 commits into
Conversation
…as, output) Closes the dark sub-steps between landing and first output that the macro funnel hid. New PostHog events (all cloud-only): - app:auth_method_selected / app:oauth_popup_result / app:auth_failed — the OAuth popup void where users abandon before auth completes. - app:canvas_ready — canvas first interactive, with is_new_user and ms_since_auth, anchoring new-user activation (user_logged_in carries neither). Bridged across the onboarding location.href reload via a per-tab sessionStorage marker set at auth completion. - app:onboarding_routed — the UserCheckView post-auth fork (waitlist / survey / onboarded), lighting where users vanish before the canvas. - app:output_viewed — first media output of a run becomes visible (the activation moment); deduped per run, is_first_output flags the session's first. - Checkout-failure + paywall-reason events on the subscription path. Re-enables app:workflow_opened / app:workflow_created (previously in the default-disabled list) and removes the dead email-verification telemetry path. GTM and Mixpanel providers ignore the new methods via optional dispatch; PostHog carries them.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughExtends cloud telemetry across auth, onboarding, checkout, and execution output flows by adding new metadata types, ChangesCloud Telemetry Expansion
Website PostHog CTA Click Tracking
Sequence Diagram(s)sequenceDiagram
rect rgba(70, 130, 180, 0.5)
Note over authStore,sessionStorage: Auth flow
end
participant authStore
participant TelemetryRegistry
participant sessionStorage as authActivationMarker
participant GraphView
authStore->>TelemetryRegistry: trackAuthMethodSelected(method, view)
authStore->>authStore: Firebase auth + createCustomer
authStore->>TelemetryRegistry: trackOAuthPopupResult / trackAuthFailed
authStore->>sessionStorage: markAuthForActivation(isNewUser)
Note over GraphView: later, on canvas ready
GraphView->>sessionStorage: consumeAuthActivation()
sessionStorage-->>GraphView: {at, isNewUser} or null
GraphView->>TelemetryRegistry: trackCanvasReady(is_new_user, ms_since_auth)
sequenceDiagram
rect rgba(100, 160, 100, 0.5)
Note over useSubscription,TelemetryRegistry: Checkout lifecycle
end
participant useSubscription
participant server as initiateSubscriptionCheckout
participant windowOpen as window.open
participant TelemetryRegistry
useSubscription->>server: initiateSubscriptionCheckout()
alt server error
server-->>TelemetryRegistry: trackCheckoutInitiateFailed(stage=server_error)
else no checkout_url
server-->>TelemetryRegistry: trackCheckoutInitiateFailed(stage=no_url)
end
useSubscription->>windowOpen: open(checkout_url)
alt window blocked
windowOpen-->>TelemetryRegistry: trackCheckoutWindowBlocked()
else opened
useSubscription->>TelemetryRegistry: trackCheckoutViewed(attempt_id, tier, cycle)
Note over TelemetryRegistry: on return
useSubscription->>TelemetryRegistry: trackCheckoutReturned(success|unknown|cancelled)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 6 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (6 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/stores/executionStore.ts (2)
98-106: ⚡ Quick winConsider moving session tracking state into the store or document the rationale.
The
outputViewedRunsSet andsessionHasViewedOutputboolean are declared at module level rather than within the Pinia store. While this achieves tab-level persistence across store instances, it has tradeoffs:
- Testing: Cannot easily reset between tests; state persists across test cases
- Observability: Not reactive, invisible to Vue DevTools
- Convention: Deviates from typical Pinia patterns where all state lives inside
defineStoreIf module-level state is intentional for session-scoped tracking (surviving component unmount/remount), consider adding a comment explaining why this state must live outside the store. Otherwise, consider moving it inside the store and resetting it on logout or app reload.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/stores/executionStore.ts` around lines 98 - 106, The module-level variables outputViewedRuns and sessionHasViewedOutput are declared outside the Pinia store, which makes them difficult to test and invisible to Vue DevTools. Either move these variables inside the defineStore function as part of the store's state object to align with Pinia conventions and enable proper test isolation, or if session-level persistence across store instances is intentional, add a clear code comment above these declarations explaining the architectural rationale for keeping them at module level.
341-359: 💤 Low valueOptional: Add comment explaining Set insertion-order guarantee.
The eviction logic on lines 348-349 relies on ES2015+ Sets maintaining insertion order, so
.values().next().valuereturns the oldest entry. While correct, this is a subtle detail that might not be immediately obvious to future maintainers.Consider adding a brief comment:
// Sets maintain insertion order (ES2015+), so first value is oldest const oldest = outputViewedRuns.values().next().value🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/stores/executionStore.ts` around lines 341 - 359, Add a brief comment above the line that retrieves the oldest entry from the outputViewedRuns Set (the line with `const oldest = outputViewedRuns.values().next().value`) to explain that Sets maintain insertion order in ES2015+. This clarifies why accessing the first value via `.values().next().value` reliably returns the oldest/first-inserted entry, which is a subtle implementation detail that future maintainers should understand.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/stores/executionStore.ts`:
- Around line 98-106: The module-level variables outputViewedRuns and
sessionHasViewedOutput are declared outside the Pinia store, which makes them
difficult to test and invisible to Vue DevTools. Either move these variables
inside the defineStore function as part of the store's state object to align
with Pinia conventions and enable proper test isolation, or if session-level
persistence across store instances is intentional, add a clear code comment
above these declarations explaining the architectural rationale for keeping them
at module level.
- Around line 341-359: Add a brief comment above the line that retrieves the
oldest entry from the outputViewedRuns Set (the line with `const oldest =
outputViewedRuns.values().next().value`) to explain that Sets maintain insertion
order in ES2015+. This clarifies why accessing the first value via
`.values().next().value` reliably returns the oldest/first-inserted entry, which
is a subtle implementation detail that future maintainers should understand.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6435d586-5020-4f95-a13e-7132aa9e7195
📒 Files selected for processing (16)
src/platform/cloud/onboarding/UserCheckView.vuesrc/platform/cloud/subscription/composables/useSubscription.tssrc/platform/cloud/subscription/composables/useSubscriptionDialog.tssrc/platform/telemetry/TelemetryRegistry.tssrc/platform/telemetry/authActivationMarker.tssrc/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.tssrc/platform/telemetry/providers/cloud/GtmTelemetryProvider.tssrc/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.test.tssrc/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.tssrc/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.tssrc/platform/telemetry/providers/cloud/PostHogTelemetryProvider.tssrc/platform/telemetry/types.tssrc/stores/authStore.test.tssrc/stores/authStore.tssrc/stores/executionStore.tssrc/views/GraphView.vue
💤 Files with no reviewable changes (4)
- src/platform/telemetry/providers/cloud/GtmTelemetryProvider.ts
- src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.test.ts
- src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts
- src/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.ts
Codecov Report❌ Patch coverage is @@ Coverage Diff @@
## main #12894 +/- ##
==========================================
+ Coverage 76.21% 76.46% +0.25%
==========================================
Files 1574 1574
Lines 102210 88251 -13959
Branches 32027 26674 -5353
==========================================
- Hits 77897 67482 -10415
+ Misses 23489 20118 -3371
+ Partials 824 651 -173
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 288 files with indirect coverage changes 🚀 New features to boost your workflow:
|
…l events - authActivationMarker: mark->consume round-trip, single-consume (clears the key), null on missing / corrupt / wrong-shape, and a swallowed sessionStorage failure. - executionStore output_viewed: first media output of a run emits once, per-run dedup, is_first_output flips after the session's first, media_type mapping (image/video/gifs/audio), text-only skipped, isCloud-gated. - PostHogTelemetryProvider: each new funnel method (canvas_ready, onboarding_routed, output_viewed, auth_method_selected, oauth_popup_result, auth_failed, checkout_initiate_failed, checkout_window_blocked) captures the right event name and forwards its metadata.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/stores/executionStore.test.ts (1)
1424-1589: ⚡ Quick winCover the dedupe eviction contract.
The suite verifies per-run suppression, but not the bounded eviction path that prevents
outputViewedRunsfrom growing indefinitely. Add one case that emits more than 256 unique run IDs, then verifies the oldest ID can emit again withis_first_output: false.As per coding guidelines, “Write tests for all changes, especially bug fixes to catch future regressions.”
🧪 Proposed regression test
it('does not emit for a non-media (text-only) output', async () => { const store = await freshStore() startRun(store, 'run-text') @@ expect(mockTrackOutputViewed).not.toHaveBeenCalled() }) + + it('evicts the oldest run id from the output_viewed dedupe set', async () => { + const maxTrackedOutputRuns = 256 + const store = await freshStore() + + for (let index = 0; index <= maxTrackedOutputRuns; index++) { + const runId = `run-${index}` + startRun(store, runId) + fireExecuted({ node: 'save-1', prompt_id: runId, output: imageOutput }) + } + + expect(mockTrackOutputViewed).toHaveBeenCalledTimes( + maxTrackedOutputRuns + 1 + ) + mockTrackOutputViewed.mockClear() + + startRun(store, 'run-0') + fireExecuted({ node: 'save-1', prompt_id: 'run-0', output: imageOutput }) + + expect(mockTrackOutputViewed).toHaveBeenCalledWith({ + workflow_run_id: 'run-0', + media_type: 'image', + is_first_output: false + }) + }) it('does not emit anything when isCloud is false', async () => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/stores/executionStore.test.ts` around lines 1424 - 1589, Add a new test case within the describe block that verifies the bounded eviction path for the outputViewedRuns deduplication set. Create a test that calls freshStore(), then uses startRun() and fireExecuted() to process more than 256 unique run IDs to fill and exceed the capacity of the outputViewedRuns set, then verify that an old run ID (one that should have been evicted when the set reached capacity) can emit trackOutputViewed again with is_first_output set to false, confirming that the eviction prevents unbounded growth of the dedup set.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/stores/executionStore.test.ts`:
- Around line 1538-1543: The test uses `it.each` which violates the
`vitest/consistent-each-for` linting rule. Import `test` from vitest at the top
of the file and replace the `it.each` call with `test.each` to comply with the
linting requirement for parameterized tests.
---
Nitpick comments:
In `@src/stores/executionStore.test.ts`:
- Around line 1424-1589: Add a new test case within the describe block that
verifies the bounded eviction path for the outputViewedRuns deduplication set.
Create a test that calls freshStore(), then uses startRun() and fireExecuted()
to process more than 256 unique run IDs to fill and exceed the capacity of the
outputViewedRuns set, then verify that an old run ID (one that should have been
evicted when the set reached capacity) can emit trackOutputViewed again with
is_first_output set to false, confirming that the eviction prevents unbounded
growth of the dedup set.
🪄 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: 2e04fbb5-cea4-451e-9d9c-760464737ce3
📒 Files selected for processing (3)
src/platform/telemetry/authActivationMarker.test.tssrc/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.tssrc/stores/executionStore.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts
- output_viewed: classify media_type via the canonical getMediaTypeFromFilename (reuse, not re-invent; now covers 3D), and evict-before-add in the dedup set so it never exceeds its bound. - Move the output_viewed dedup state (run set + first-output flag) onto the store instance instead of module scope, so it resets with the store and tests isolate without resetModules. - canvas_ready: stamp ms_since_auth at canvas-interactive time, before runWhenGlobalIdle defers, so the latency is auth->canvas not auth->idle. - Tests: it.each -> it.for with canonical filename-based rows (+ 3D), drop a redundant typeof assertion, call trackCheckoutWindowBlocked with no args to match the real call site.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/stores/executionStore.test.ts`:
- Around line 1532-1556: The parameterized test cases in the it.for block always
use the images bucket in the fireExecuted call, but the test comment states that
bucket name should be irrelevant for media_type classification. Add at least one
additional test case (for example, filename 'a.glb' with bucket 'model_file'
instead of 'images') to the test data, and modify the fireExecuted call to use a
dynamic bucket property from the test case instead of always hardcoding 'images'
in the output object. This ensures the test validates that media_type
classification works across different output bucket types.
🪄 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: 352be80c-dd60-4cef-86fc-64b4661293ef
📒 Files selected for processing (5)
src/platform/telemetry/authActivationMarker.test.tssrc/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.tssrc/stores/executionStore.test.tssrc/stores/executionStore.tssrc/views/GraphView.vue
💤 Files with no reviewable changes (1)
- src/platform/telemetry/authActivationMarker.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts
- src/stores/executionStore.ts
- src/views/GraphView.vue
| // media_type is classified by filename via getMediaTypeFromFilename, so the | ||
| // output bucket name is irrelevant (gif is an image extension; glb is 3D). | ||
| it.for([ | ||
| { filename: 'a.png', expected: 'image' }, | ||
| { filename: 'a.mp4', expected: 'video' }, | ||
| { filename: 'a.webm', expected: 'video' }, | ||
| { filename: 'a.gif', expected: 'image' }, | ||
| { filename: 'a.mp3', expected: 'audio' }, | ||
| { filename: 'a.glb', expected: '3D' } | ||
| ])( | ||
| 'classifies $filename as media_type $expected', | ||
| async ({ filename, expected }, { expect }) => { | ||
| const store = await freshStore() | ||
| startRun(store, 'run-media') | ||
|
|
||
| fireExecuted({ | ||
| node: 'save-1', | ||
| prompt_id: 'run-media', | ||
| output: { images: [{ filename }] } | ||
| }) | ||
|
|
||
| expect(mockTrackOutputViewed).toHaveBeenCalledTimes(1) | ||
| expect(mockTrackOutputViewed).toHaveBeenCalledWith( | ||
| expect.objectContaining({ media_type: expected }) | ||
| ) |
There was a problem hiding this comment.
Retain one non-images bucket case to validate bucket-agnostic behavior.
Line 1550 always uses output.images, so this no longer verifies the
“bucket name is irrelevant” contract. Add one case (e.g., model_file + .glb)
to keep that behavior protected without expanding test volume much.
Suggested minimal test tweak
- it.for([
- { filename: 'a.png', expected: 'image' },
- { filename: 'a.mp4', expected: 'video' },
- { filename: 'a.webm', expected: 'video' },
- { filename: 'a.gif', expected: 'image' },
- { filename: 'a.mp3', expected: 'audio' },
- { filename: 'a.glb', expected: '3D' }
- ])(
+ it.for([
+ { output: { images: [{ filename: 'a.png' }] }, expected: 'image' },
+ { output: { images: [{ filename: 'a.mp4' }] }, expected: 'video' },
+ { output: { images: [{ filename: 'a.webm' }] }, expected: 'video' },
+ { output: { images: [{ filename: 'a.gif' }] }, expected: 'image' },
+ { output: { images: [{ filename: 'a.mp3' }] }, expected: 'audio' },
+ { output: { model_file: [{ filename: 'a.glb' }] }, expected: '3D' }
+ ])(
'classifies $filename as media_type $expected',
- async ({ filename, expected }, { expect }) => {
+ async ({ output, expected }, { expect }) => {
const store = await freshStore()
startRun(store, 'run-media')
fireExecuted({
node: 'save-1',
prompt_id: 'run-media',
- output: { images: [{ filename }] }
+ output
})As per coding guidelines, “Write tests for all changes, especially bug fixes to catch future regressions,” while staying “parsimonious in testing.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/stores/executionStore.test.ts` around lines 1532 - 1556, The
parameterized test cases in the it.for block always use the images bucket in the
fireExecuted call, but the test comment states that bucket name should be
irrelevant for media_type classification. Add at least one additional test case
(for example, filename 'a.glb' with bucket 'model_file' instead of 'images') to
the test data, and modify the fireExecuted call to use a dynamic bucket property
from the test case instead of always hardcoding 'images' in the output object.
This ensures the test validates that media_type classification works across
different output bucket types.
Source: Coding guidelines
Extends the cloud funnel instrumentation (all cloud-only): - app:paywall_viewed — fired when a subscription/paywall dialog opens, with the reason (run-gate wired now; member_invite / upload_model entry points pending billing-context reason threading) and current_tier. - app:checkout_viewed — Stripe checkout window opened, sharing the pending checkout attempt id with the success event. - app:checkout_returned — user returned from checkout (success / cancelled / unknown), deduped once per attempt. - app:first_execution_completed — once-per-user activation moment, guarded by a durable localStorage key (private-mode safe). - Super-properties on every cloud event: is_app_mode (graph vs app mode, via a watcher) and customer_tier (event-level mirror of subscription_tier), plus first-touch attribution ($set_once initial_utm_* on the person at init). The super-property registrations are wrapped so a failure (e.g. Pinia not ready at init) degrades to "property absent" instead of disabling the provider and skipping identify/logout wiring. media_type classification reuses the canonical getMediaTypeFromFilename.
Add a captureCtaClick helper to the website PostHog wrapper and fire
website:cta_clicked from the real CTA elements (hero, nav buttons, nav
products links, product cards) instead of relying on autocapture or
i18n text matching. Each event carries { button, location }.
app:subscribe_now_button_clicked was only fired by the legacy
SubscribeButton, never by the pricing table tier buttons or the
subscribe-to-run lock button - the surfaces users actually click. Add
the same trackSubscription('subscribe_clicked') capture to:
- PricingTable.handleSubscribe (both the new-subscriber and plan-change
paths), before checkout opens, carrying { tier, cycle }
- SubscribeToRun.handleSubscribeToRun, alongside the existing run-button
event
Each surface now tags a source ('pricing_table' | 'subscribe_to_run' |
'subscribe_button') so the subscribe-click funnel can be attributed by
CTA. Extend SubscriptionMetadata with tier/cycle/source; no new event
name.
…oud web analytics
…oud-funnel-telemetry # Conflicts: # src/platform/cloud/subscription/composables/useSubscriptionDialog.ts
…metry-gaps' into deepme987/frontend/cloud-funnel-telemetry # Conflicts: # src/platform/telemetry/TelemetryRegistry.ts # src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts # src/platform/telemetry/types.ts # src/stores/authStore.ts
…etry' into deepme987/frontend/cloud-funnel-telemetry
- drop client-init heatmaps (RemoteConfig-only in posthog-js 1.358) - un-export internal SubscribeClickSource (knip: unused export) - sync provider/auth tests to the unioned config + trackAuthError mock
🎭 Playwright: ✅ 1668 passed, 0 failed · 5 flaky📊 Browser Reports
🎨 Storybook: ✅ Built — View Storybook📦 Bundle: 7.46 MB gzip 🔴 +2.39 kBDetailsSummary
Category Glance App Entry Points — 46.7 kB (baseline 46.7 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.25 MB (baseline 1.25 MB) • 🔴 +283 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 96 kB (baseline 95.3 kB) • 🔴 +713 BTop-level views, pages, and routed surfaces
Status: 9 added / 9 removed / 3 unchanged Panels & Settings — 525 kB (baseline 525 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 11 added / 11 removed / 15 unchanged User & Accounts — 19.9 kB (baseline 19.9 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 6 added / 6 removed / 3 unchanged Editors & Dialogs — 112 kB (baseline 112 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 4 added / 4 removed / 1 unchanged UI Components — 57.2 kB (baseline 57.2 kB) • 🔴 +39 BReusable component library chunks
Status: 5 added / 5 removed / 8 unchanged Data & Services — 269 kB (baseline 269 kB) • ⚪ 0 BStores, services, APIs, and repositories
Status: 13 added / 13 removed / 3 unchanged Utilities & Hooks — 3.33 MB (baseline 3.33 MB) • 🔴 +6.79 kBHelpers, composables, and utility bundles
Status: 14 added / 14 removed / 16 unchanged Vendor & Third-Party — 15.3 MB (baseline 15.3 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 10.4 MB (baseline 10.4 MB) • 🔴 +2.93 kBBundles that do not match a named category
Status: 62 added / 62 removed / 88 unchanged ⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-06-22T21:22:31.404Z",
"gitSha": "032edad77e5ea21b1507735bb75e53111a77948e",
"branch": "deepme987/frontend/cloud-funnel-telemetry",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2014.285000000001,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.540999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 371.694,
"heapDeltaBytes": -2108812,
"heapUsedBytes": 56436920,
"domNodes": 20,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 19.285,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2017.0679999999948,
"styleRecalcs": 7,
"styleRecalcDurationMs": 7.438999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 415.5799999999999,
"heapDeltaBytes": -2349792,
"heapUsedBytes": 56431936,
"domNodes": 14,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 22.547,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1915.5739999999923,
"styleRecalcs": 77,
"styleRecalcDurationMs": 46.093,
"layouts": 12,
"layoutDurationMs": 3.8190000000000004,
"taskDurationMs": 866.552,
"heapDeltaBytes": -7101568,
"heapUsedBytes": 51474320,
"domNodes": 61,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 127.20100000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1798.5090000000241,
"styleRecalcs": 74,
"styleRecalcDurationMs": 38.003,
"layouts": 12,
"layoutDurationMs": 3.6049999999999995,
"taskDurationMs": 782.147,
"heapDeltaBytes": -7427408,
"heapUsedBytes": 51284000,
"domNodes": 56,
"jsHeapTotalBytes": 25427968,
"scriptDurationMs": 124.489,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1726.902999999993,
"styleRecalcs": 31,
"styleRecalcDurationMs": 18.686,
"layouts": 6,
"layoutDurationMs": 0.6330000000000001,
"taskDurationMs": 334.67999999999995,
"heapDeltaBytes": 1710032,
"heapUsedBytes": 60789268,
"domNodes": 77,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 22.853,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1733.082999999965,
"styleRecalcs": 31,
"styleRecalcDurationMs": 21.240999999999996,
"layouts": 6,
"layoutDurationMs": 0.727,
"taskDurationMs": 378.326,
"heapDeltaBytes": 4313728,
"heapUsedBytes": 60247020,
"domNodes": 79,
"jsHeapTotalBytes": 18612224,
"scriptDurationMs": 29.673,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 552.9200000000287,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.609000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 326.544,
"heapDeltaBytes": 7182204,
"heapUsedBytes": 65950092,
"domNodes": 18,
"jsHeapTotalBytes": 18874368,
"scriptDurationMs": 55.511,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 569.1039999999816,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.741,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 347.04999999999995,
"heapDeltaBytes": 7267240,
"heapUsedBytes": 65993516,
"domNodes": 18,
"jsHeapTotalBytes": 18874368,
"scriptDurationMs": 58.473000000000006,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "large-graph-idle",
"durationMs": 1994.5549999999912,
"styleRecalcs": 11,
"styleRecalcDurationMs": 9.987,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 538.4369999999999,
"heapDeltaBytes": -8076484,
"heapUsedBytes": 62064700,
"domNodes": 22,
"jsHeapTotalBytes": 10891264,
"scriptDurationMs": 104.55199999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2014.2260000000078,
"styleRecalcs": 10,
"styleRecalcDurationMs": 10.103,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 607.9590000000001,
"heapDeltaBytes": 13101816,
"heapUsedBytes": 75339420,
"domNodes": 20,
"jsHeapTotalBytes": 6348800,
"scriptDurationMs": 119.08100000000002,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2140.512000000001,
"styleRecalcs": 69,
"styleRecalcDurationMs": 19.266000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1178.641,
"heapDeltaBytes": 2852444,
"heapUsedBytes": 64813288,
"domNodes": -265,
"jsHeapTotalBytes": 7294976,
"scriptDurationMs": 438.515,
"eventListeners": -130,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2162.351000000001,
"styleRecalcs": 70,
"styleRecalcDurationMs": 19.264,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1134.8690000000001,
"heapDeltaBytes": 11235244,
"heapUsedBytes": 84024384,
"domNodes": 18,
"jsHeapTotalBytes": 10280960,
"scriptDurationMs": 407.03700000000003,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3088.628999999969,
"styleRecalcs": 65,
"styleRecalcDurationMs": 18.438999999999997,
"layouts": 60,
"layoutDurationMs": 7.5729999999999995,
"taskDurationMs": 1292.317,
"heapDeltaBytes": 13471948,
"heapUsedBytes": 68320356,
"domNodes": 12,
"jsHeapTotalBytes": 6815744,
"scriptDurationMs": 479.7009999999999,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3185.3320000000167,
"styleRecalcs": 65,
"styleRecalcDurationMs": 21.188,
"layouts": 60,
"layoutDurationMs": 7.902000000000001,
"taskDurationMs": 1474.2640000000001,
"heapDeltaBytes": 15376120,
"heapUsedBytes": 70505908,
"domNodes": 14,
"jsHeapTotalBytes": 7340032,
"scriptDurationMs": 564.012,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2000.7059999999797,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.033000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 566.1030000000001,
"heapDeltaBytes": -9249484,
"heapUsedBytes": 64576928,
"domNodes": 16,
"jsHeapTotalBytes": 7745536,
"scriptDurationMs": 109.13000000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2004.3729999999869,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.554,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 661.098,
"heapDeltaBytes": -9514828,
"heapUsedBytes": 64072364,
"domNodes": 20,
"jsHeapTotalBytes": 7483392,
"scriptDurationMs": 115.77699999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 579.4859999999744,
"styleRecalcs": 48,
"styleRecalcDurationMs": 11.918000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 373.711,
"heapDeltaBytes": 8063516,
"heapUsedBytes": 66980828,
"domNodes": 22,
"jsHeapTotalBytes": 19136512,
"scriptDurationMs": 122.668,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 593.0359999999837,
"styleRecalcs": 48,
"styleRecalcDurationMs": 12.108999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 407.957,
"heapDeltaBytes": 8062508,
"heapUsedBytes": 66750112,
"domNodes": 22,
"jsHeapTotalBytes": 19136512,
"scriptDurationMs": 131.23100000000002,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-idle",
"durationMs": 2006.8129999999655,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.585,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 377.25399999999996,
"heapDeltaBytes": -2631096,
"heapUsedBytes": 56096328,
"domNodes": 20,
"jsHeapTotalBytes": 25165824,
"scriptDurationMs": 19.560000000000002,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 1997.8060000000255,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.190999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 436.01900000000006,
"heapDeltaBytes": -2281880,
"heapUsedBytes": 56596708,
"domNodes": 18,
"jsHeapTotalBytes": 25952256,
"scriptDurationMs": 21.717,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1683.2009999999968,
"styleRecalcs": 76,
"styleRecalcDurationMs": 38.479,
"layouts": 16,
"layoutDurationMs": 4.849,
"taskDurationMs": 662.841,
"heapDeltaBytes": 14811152,
"heapUsedBytes": 66503628,
"domNodes": 62,
"jsHeapTotalBytes": 15466496,
"scriptDurationMs": 91.684,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1702.891999999963,
"styleRecalcs": 75,
"styleRecalcDurationMs": 35.948,
"layouts": 16,
"layoutDurationMs": 4.081,
"taskDurationMs": 731.4150000000001,
"heapDeltaBytes": -11123860,
"heapUsedBytes": 47682212,
"domNodes": 62,
"jsHeapTotalBytes": 26476544,
"scriptDurationMs": 97.80900000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-transition-enter",
"durationMs": 1298.2589999999732,
"styleRecalcs": 17,
"styleRecalcDurationMs": 27.615000000000002,
"layouts": 4,
"layoutDurationMs": 13.717000000000004,
"taskDurationMs": 817.1189999999999,
"heapDeltaBytes": 4555672,
"heapUsedBytes": 80324160,
"domNodes": 13833,
"jsHeapTotalBytes": 17563648,
"scriptDurationMs": 40.329,
"eventListeners": 2529,
"totalBlockingTimeMs": 158,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "viewport-pan-sweep",
"durationMs": 8201.729,
"styleRecalcs": 251,
"styleRecalcDurationMs": 55.614999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3919.0080000000003,
"heapDeltaBytes": -2484384,
"heapUsedBytes": 67594404,
"domNodes": 20,
"jsHeapTotalBytes": 17096704,
"scriptDurationMs": 1303.055,
"eventListeners": 20,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8306.912999999895,
"styleRecalcs": 252,
"styleRecalcDurationMs": 57.519000000000005,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4328.966,
"heapDeltaBytes": 5764796,
"heapUsedBytes": 76214612,
"domNodes": 22,
"jsHeapTotalBytes": 22601728,
"scriptDurationMs": 1513.6970000000001,
"eventListeners": 22,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 13255.900999999994,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 13222.01,
"heapDeltaBytes": -40491144,
"heapUsedBytes": 172534320,
"domNodes": -3310,
"jsHeapTotalBytes": 25399296,
"scriptDurationMs": 663.805,
"eventListeners": -16473,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-idle",
"durationMs": 12531.799999999976,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12516.478000000001,
"heapDeltaBytes": -52289900,
"heapUsedBytes": 161758276,
"domNodes": -8331,
"jsHeapTotalBytes": 14745600,
"scriptDurationMs": 589.136,
"eventListeners": -16468,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 15032.003000000032,
"styleRecalcs": 71,
"styleRecalcDurationMs": 18.83499999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14994.462,
"heapDeltaBytes": -44626676,
"heapUsedBytes": 165087756,
"domNodes": -3308,
"jsHeapTotalBytes": 17272832,
"scriptDurationMs": 902.8600000000001,
"eventListeners": -16474,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.220000000000073,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 14735.352000000035,
"styleRecalcs": 66,
"styleRecalcDurationMs": 18.04899999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14711.247,
"heapDeltaBytes": -54836660,
"heapUsedBytes": 171047200,
"domNodes": -8365,
"jsHeapTotalBytes": 17891328,
"scriptDurationMs": 884.411,
"eventListeners": -16514,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.220000000000073,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 470.72299999996403,
"styleRecalcs": 18,
"styleRecalcDurationMs": 26.317,
"layouts": 5,
"layoutDurationMs": 1.509,
"taskDurationMs": 132.63199999999998,
"heapDeltaBytes": 5490980,
"heapUsedBytes": 65219384,
"domNodes": 168,
"jsHeapTotalBytes": 3407872,
"scriptDurationMs": 24.870000000000005,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 444.7010000000091,
"styleRecalcs": 16,
"styleRecalcDurationMs": 20.056000000000004,
"layouts": 5,
"layoutDurationMs": 1.5250000000000001,
"taskDurationMs": 111.101,
"heapDeltaBytes": 5108928,
"heapUsedBytes": 65176504,
"domNodes": 155,
"jsHeapTotalBytes": 2883584,
"scriptDurationMs": 17.116000000000003,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
}
]
} |
🌐 Website E2ETip All tests passed.
🔗 Website PreviewWebsite Preview: https://comfy-website-preview-pr-12894.vercel.app This commit: https://website-frontend-3xafz3i0s-comfyui.vercel.app Last updated: 2026-06-22T21:12:30Z for |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Deployment failed with the following error: Learn More: https://vercel.com/uy-tieu-s-projects?upgradeToPro=build-rate-limit |
|
Deployment failed with the following error: Learn More: https://vercel.com/uy-tieu-s-projects?upgradeToPro=build-rate-limit |
Restore trackEmailVerification + USER_EMAIL_VERIFY_* (main's new HostTelemetrySink sink consumes them; keeps PR additive). Gate first_execution_completed behind isCloud after Ben's executionStore refactor.
Address review findings on the cloud funnel: - Login-time subscription enforcement was mislabeled reason 'run_workflow' (it only fires on the isLoggedIn watch); tag 'subscription_required' so the run-gate cohort isn't polluted. - Deliberate OAuth popup cancels no longer emit auth_failed (already captured as oauth_popup_result 'cancelled'); add auth-funnel tests locking in the cancel-vs-failure distinction. - Correct the output_viewed doc (fires on output PRODUCED, not visibility). - Drop stale auth-activation markers (>60s) so an unrelated reload can't report a bogus ms_since_auth. - Replace the duplicated VIEWED_MEDIA_TYPES set with isPreviewableMediaType.
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/components/custom/widget/WorkflowTemplateSelectorDialog.vue (1)
547-552: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winConsider including
category_labelin the telemetry payload.The
TemplateCategorySelectedMetadatainterface supports an optionalcategory_labelfield. Including the human-readable label alongsidecategory_idwould improve analytics readability and reduce the need for post-processing lookups. You can derive the label frompageTitlecomputed or by looking up the selected nav item innavItems.📊 Example: add category_label
watch(selectedNavItem, (to, from) => { if (!to || to === from) return - useTelemetry()?.trackTemplateCategorySelected({ category_id: to }) + const label = pageTitle.value + useTelemetry()?.trackTemplateCategorySelected({ + category_id: to, + category_label: label + }) })Alternatively, look up the label directly from
navItems.valueif you want the raw label instead of the computed title.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/custom/widget/WorkflowTemplateSelectorDialog.vue` around lines 547 - 552, The watch function for selectedNavItem only includes category_id in the trackTemplateCategorySelected telemetry call, but should also include category_label for improved analytics readability. Modify the useTelemetry()?.trackTemplateCategorySelected() call to add a category_label field alongside category_id. You can derive the label from the pageTitle computed property or by looking up the selected nav item in the navItems array to get its human-readable label.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/custom/widget/WorkflowTemplateSelectorDialog.vue`:
- Around line 547-552: The telemetry call within the selectedNavItem watch
handler uses optional chaining but lacks the explicit isCloud guard that is used
consistently elsewhere in the file (such as with the trackTemplateLibraryClosed
call). Wrap the useTelemetry()?.trackTemplateCategorySelected() call inside an
if (isCloud) conditional check to align with the existing cloud gating pattern
used throughout this component.
In `@src/platform/cloud/subscription/composables/useSubscription.ts`:
- Around line 133-139: The `lastCheckoutAttempt` variable in the useSubscription
composable is not being reset after terminal checkout outcomes (success,
failure, or cancellation), which causes stale attempt information to persist and
be incorrectly reused by subsequent status syncs. This results in incorrect
cancelled events being emitted for old attempts. Reset `lastCheckoutAttempt` to
null at all terminal outcome paths where checkout completes or fails, ensuring
the variable does not carry over stale data to later status sync operations.
Review both the initial declaration around line 133-139 and the related code
sections around lines 200-215 to identify all terminal outcome paths where this
reset should be applied.
---
Nitpick comments:
In `@src/components/custom/widget/WorkflowTemplateSelectorDialog.vue`:
- Around line 547-552: The watch function for selectedNavItem only includes
category_id in the trackTemplateCategorySelected telemetry call, but should also
include category_label for improved analytics readability. Modify the
useTelemetry()?.trackTemplateCategorySelected() call to add a category_label
field alongside category_id. You can derive the label from the pageTitle
computed property or by looking up the selected nav item in the navItems array
to get its human-readable label.
🪄 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 Plus
Run ID: 0b00fce5-d9e5-462a-bb50-81d680f178e4
📒 Files selected for processing (27)
apps/website/src/components/common/HeaderMain/HeaderMain.vueapps/website/src/components/common/HeaderMain/HeaderMainDesktop.vueapps/website/src/components/common/HeaderMain/HeaderMainMobile.vueapps/website/src/components/common/HeaderMain/NavColumn.vueapps/website/src/components/common/ProductCard.vueapps/website/src/components/common/ProductCardsSection.vueapps/website/src/components/home/HeroSection.vueapps/website/src/data/mainNavigation.tsapps/website/src/scripts/posthog.test.tsapps/website/src/scripts/posthog.tssrc/components/custom/widget/WorkflowTemplateSelectorDialog.vuesrc/platform/cloud/subscription/components/PricingTable.test.tssrc/platform/cloud/subscription/components/PricingTable.vuesrc/platform/cloud/subscription/components/SubscribeButton.vuesrc/platform/cloud/subscription/components/SubscribeToRun.test.tssrc/platform/cloud/subscription/components/SubscribeToRun.vuesrc/platform/cloud/subscription/composables/useSubscription.test.tssrc/platform/cloud/subscription/composables/useSubscription.tssrc/platform/cloud/subscription/composables/useSubscriptionDialog.test.tssrc/platform/cloud/subscription/composables/useSubscriptionDialog.tssrc/platform/telemetry/TelemetryRegistry.test.tssrc/platform/telemetry/TelemetryRegistry.tssrc/platform/telemetry/authActivationMarker.test.tssrc/platform/telemetry/authActivationMarker.tssrc/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.tssrc/platform/telemetry/providers/cloud/PostHogTelemetryProvider.tssrc/platform/telemetry/types.ts
💤 Files with no reviewable changes (2)
- src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts
- src/platform/telemetry/types.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- src/platform/cloud/subscription/composables/useSubscriptionDialog.test.ts
- src/platform/cloud/subscription/composables/useSubscriptionDialog.ts
- src/platform/telemetry/authActivationMarker.ts
- src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts
- src/platform/telemetry/authActivationMarker.test.ts
| // Track category/tab switches (e.g. "Getting Started" vs "All") so we can see | ||
| // which curated entry points users browse before opening a template. | ||
| watch(selectedNavItem, (to, from) => { | ||
| if (!to || to === from) return | ||
| useTelemetry()?.trackTemplateCategorySelected({ category_id: to }) | ||
| }) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Align cloud gating with the existing pattern in this file.
The existing telemetry call at lines 456-465 wraps useTelemetry()?.trackTemplateLibraryClosed(...) inside an explicit if (isCloud) check, but the new category-selection telemetry uses only optional chaining. For consistency within this file, wrap the telemetry call in an if (isCloud) guard.
♻️ Align cloud gating pattern
watch(selectedNavItem, (to, from) => {
if (!to || to === from) return
- useTelemetry()?.trackTemplateCategorySelected({ category_id: to })
+ if (isCloud) {
+ useTelemetry()?.trackTemplateCategorySelected({ category_id: to })
+ }
})🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/custom/widget/WorkflowTemplateSelectorDialog.vue` around lines
547 - 552, The telemetry call within the selectedNavItem watch handler uses
optional chaining but lacks the explicit isCloud guard that is used consistently
elsewhere in the file (such as with the trackTemplateLibraryClosed call). Wrap
the useTelemetry()?.trackTemplateCategorySelected() call inside an if (isCloud)
conditional check to align with the existing cloud gating pattern used
throughout this component.
| // In-session checkout attempt (attempt_id/tier/cycle) for attributing a non-success | ||
| // checkout_returned; success outcomes source the id from metadata instead (cross-session). | ||
| let lastCheckoutAttempt: { | ||
| attempt_id: string | ||
| tier: string | ||
| cycle: string | ||
| } | null = null |
There was a problem hiding this comment.
Reset lastCheckoutAttempt after terminal outcomes to avoid stale cancellation attribution.
lastCheckoutAttempt is used to emit checkout_returned: cancelled, but it is never cleared in these paths. A stale in-session attempt can be reused by later status synces and emit an incorrect cancelled event for an old attempt.
Suggested fix
if (!metadata) {
if (hasPendingSubscriptionCheckoutAttempt()) {
if (lastCheckoutAttempt) {
reportCheckoutReturned(lastCheckoutAttempt.attempt_id, 'unknown')
}
schedulePendingCheckoutRecovery()
} else {
if (lastCheckoutAttempt) {
reportCheckoutReturned(lastCheckoutAttempt.attempt_id, 'cancelled')
+ lastCheckoutAttempt = null
}
stopPendingCheckoutRecovery()
}
return
}
reportCheckoutReturned(metadata.checkout_attempt_id, 'success')
+ if (lastCheckoutAttempt?.attempt_id === metadata.checkout_attempt_id) {
+ lastCheckoutAttempt = null
+ }Also applies to: 200-215
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/platform/cloud/subscription/composables/useSubscription.ts` around lines
133 - 139, The `lastCheckoutAttempt` variable in the useSubscription composable
is not being reset after terminal checkout outcomes (success, failure, or
cancellation), which causes stale attempt information to persist and be
incorrectly reused by subsequent status syncs. This results in incorrect
cancelled events being emitted for old attempts. Reset `lastCheckoutAttempt` to
null at all terminal outcome paths where checkout completes or fails, ensuring
the variable does not carry over stale data to later status sync operations.
Review both the initial declaration around line 133-139 and the related code
sections around lines 200-215 to identify all terminal outcome paths where this
reset should be applied.
…oud-funnel-telemetry # Conflicts: # src/stores/executionStore.test.ts # src/stores/executionStore.ts
christian-byrne
left a comment
There was a problem hiding this comment.
Reviewed 13/33 pausing briefly
| // Track category/tab switches (e.g. "Getting Started" vs "All") so we can see | ||
| // which curated entry points users browse before opening a template. | ||
| watch(selectedNavItem, (to, from) => { | ||
| if (!to || to === from) return |
There was a problem hiding this comment.
First, to === from is not necessary. See https://github.com/vuejs/core/blob/main/packages/reactivity/src/watch.ts.
I also personally don't think !to is necessary if the lifecycle semantics of the nav item ref don't allow for a falsy value outside of initial mount.
| // Track category/tab switches (e.g. "Getting Started" vs "All") so we can see | ||
| // which curated entry points users browse before opening a template. |
There was a problem hiding this comment.
Can we remove these comments that explain things already apparent from the code itself? Then keep only comments explaining something that's truly not inferrable from the code?
| const onboardingSurveyEnabled = computed( | ||
| () => flags.onboardingSurveyEnabled ?? true | ||
| ) | ||
| const telemetry = isCloud ? useTelemetry() : undefined |
There was a problem hiding this comment.
We should stay consistent with the rest of the codebase and just inline all useTelemetry calls. The performance diff is not detectable and generally we should avoid eager import side effects.
| telemetry?.trackOnboardingRouted({ | ||
| destination: 'waitlist', | ||
| survey_completed: !!surveyStatus, | ||
| has_cloud_status: false | ||
| }) |
There was a problem hiding this comment.
I don't think this is necessary as the waitlist doesn't exist anymore.
| // captures intent even if the checkout window is blocked or abandoned. | ||
| // Covers both the 'change' (existing paid subscriber) and 'new' paths. | ||
| telemetry?.trackSubscription('subscribe_clicked', { | ||
| current_tier: subscriptionTier.value?.toLowerCase(), |
There was a problem hiding this comment.
For these events, are they expected to always be lower case? Does that apply to all? We should build that into the types if it's the case so that no future agent can slip up. See https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html#lowercasestringtype
| // Non-success returns attribute via this; success outcomes use the id from metadata (cross-session). | ||
| let lastCheckoutAttempt: { | ||
| attempt_id: string | ||
| tier: string | ||
| cycle: string | ||
| } | null = null | ||
| // Fire checkout_returned once per attempt despite pageshow/visibilitychange firing repeatedly. | ||
| const reportedReturnedAttemptIds = new Set<string>() | ||
|
|
||
| const reportCheckoutReturned = ( | ||
| checkoutAttemptId: string, | ||
| outcome: 'success' | 'cancelled' | 'unknown' | ||
| ) => { | ||
| if (reportedReturnedAttemptIds.has(checkoutAttemptId)) { | ||
| return | ||
| } | ||
| reportedReturnedAttemptIds.add(checkoutAttemptId) | ||
| telemetry?.trackCheckoutReturned({ | ||
| checkout_attempt_id: checkoutAttemptId, | ||
| outcome | ||
| }) | ||
| } | ||
|
|
There was a problem hiding this comment.
A larger thing: there was always a constraint for FE telem whereby the BE never sent an event indicating the subscription succeeded. To allow us to see "user subscribed" we had to use hacks like recording the state and then once seeing the state change on the user/customer object, we inferred a subscription success. It's really not good and has a lot of failure modes. One of the major benefits of unifying data and doing identity stitching and ETL and everything was that we wouldn't have to do that anymore. That is, we would have the subscribe event owned by the BE and the user behaviors owned by the FE and then we ETL and stitch. This PR is adding more FE code to try to infer subscription success. Thoughts?
| | 'subscription_required' | ||
| | 'out_of_credits' | ||
| | 'top_up_blocked' | ||
| // Non-activation cohort: the activation funnel must be able to exclude these. |
There was a problem hiding this comment.
What does this mean exactly?
| const DEFAULT_DISABLED_EVENTS = [ | ||
| TelemetryEvents.WORKFLOW_OPENED, | ||
| TelemetryEvents.PAGE_VISIBILITY_CHANGED, | ||
| TelemetryEvents.TAB_COUNT_TRACKING, | ||
| TelemetryEvents.NODE_SEARCH, | ||
| TelemetryEvents.NODE_SEARCH_RESULT_SELECTED, | ||
| TelemetryEvents.HELP_CENTER_OPENED, | ||
| TelemetryEvents.HELP_RESOURCE_CLICKED, | ||
| TelemetryEvents.HELP_CENTER_CLOSED, | ||
| TelemetryEvents.WORKFLOW_CREATED | ||
| TelemetryEvents.HELP_CENTER_CLOSED | ||
| ] as const satisfies TelemetryEventName[] |
There was a problem hiding this comment.
The idea was that if we wanted to disable/enable these events, we would do it in the feature flags -- rather than changing the defaults.
| autocapture: false, | ||
| capture_pageview: false, | ||
| capture_pageleave: false, | ||
| autocapture: true, |
There was a problem hiding this comment.
For autocapture, I don't think it aligns with our overall analytics strategy.
Our approach is to maintain a deliberate, manually-defined event taxonomy that stays consistent across codebases and surfaces. Those events are ETL'd into a shared warehouse, and much of our analysis relies on agents reasoning over that data.
Autocapture introduces a large volume of implementation-derived events that exist outside that taxonomy. Unlike product events, these events are often tied to UI details such as DOM structure, element labels, CSS classes, and interaction patterns. As the product evolves, those implementation details can change even when the underlying user behavior has not, creating discontinuities in the data and potentially leading to incorrect conclusions. For example, an apparent drop-off in an autocaptured flow may simply reflect a UI refactor rather than an actual change in user behavior.
It also significantly increases the amount of low-signal data in the warehouse. Given that our downstream analysis relies heavily on agents, maintaining a high signal-to-noise ratio is important. A curated set of stable, intentional product events provides a much stronger foundation for analysis than a mix of curated events and a large volume of automatically generated interaction events. In practice, I would expect autocapture to make it harder for both humans and agents to identify meaningful patterns and draw reliable conclusions.
There was a problem hiding this comment.
I'm aligned to not use autocapture to avoid noisy signal bloat (if we can precisely define the events we want to capture with product team).
My hope is we should be able to be more explicit and use glary / @PostHog code to help us define precise event tracking and avoid noisy event bloat. We want to understand activation and conversion and usage intents that drive retained users and who drive growth. cc @deepme987 @stevenltran @benceruleanlu
Does that make sense?
| private setFirstTouchAttribution(): void { | ||
| if (!this.posthog) return | ||
| const params = new URLSearchParams(window.location.search) | ||
| const firstTouch: Record<string, string> = {} | ||
| const source = params.get('utm_source') | ||
| const medium = params.get('utm_medium') | ||
| const campaign = params.get('utm_campaign') | ||
| if (source) firstTouch.initial_utm_source = source | ||
| if (medium) firstTouch.initial_utm_medium = medium | ||
| if (campaign) firstTouch.initial_utm_campaign = campaign | ||
| if (Object.keys(firstTouch).length === 0) return | ||
| try { | ||
| this.posthog.people.set_once(firstTouch) | ||
| } catch (error) { | ||
| console.error('Failed to set first-touch attribution:', error) | ||
| } | ||
| } |
There was a problem hiding this comment.
I believe this should be done automatically by the library, are you certain it's needed? If the lib is already doing it, we are potentially adding redundant events that may in some cases conflict with the lib.
…tidy funnel call sites - Stop emitting subscription_success from the FE inference in useSubscription; the backend now owns it via billing:subscription_created (Stripe webhook). Keep the checkout_returned funnel event and pending-attempt state-sync. - Inline useTelemetry()?.trackOnboardingRouted in UserCheckView and remove the dead waitlist skeleton branch; relabel the user-not-found destination 'login'. - Simplify the template-category watcher (redundant same-value guard).
|
closing - super-seeded |
Quick Read
Tracks the steps between "user lands on the site" and "user sees their first result" — sign-in, the post-login fork, the canvas loading, the first output — so we can see exactly where people drop off instead of guessing from a coarse 6-step funnel. Cloud-only, additive events; no behavior changes.
Summary
Instruments the dark sub-steps of the cloud user funnel between landing and first output, so we can see where users actually drop instead of inferring it from a coarse 6-step macro funnel.
Changes
app:auth_method_selected,app:oauth_popup_result,app:auth_failed— the OAuth popup gap where users abandon before auth completes.app:canvas_ready— canvas first interactive, carryingis_new_userandms_since_auth(user_logged_incarries neither), to anchor new-user activation on the canvas.app:onboarding_routed— theUserCheckViewpost-auth fork (waitlist/survey/onboarded), where users vanish before the canvas.app:output_viewed— first media output of a run becoming visible (the activation moment); deduped per run,is_first_outputflags the session's first.app:checkout_initiate_failed/app:checkout_window_blockedand a paywallreasonon the subscription path.app:workflow_opened/app:workflow_created(previously in the default-disabled list) and removes the dead email-verification telemetry path.isCloud.Review Focus
canvas_readybridge (authActivationMarker.ts):is_new_user/ms_since_authare set at the four auth-completion sites inauthStoreand consumed once when the graph canvas first becomes interactive. The onboarded path navigates via a fulllocation.hrefreload, so an in-memory store ref would not survive; a per-tabsessionStoragemarker does, and is cleared on tab close.output_viewedfires fromexecutionStore.handleExecutedon the first media-bearing node of a run; the seen-run set is bounded (256) to avoid unbounded growth in long sessions.onboarding_routedmapping: the no-cloud-status fork (cloud-login) maps towaitlistper the documented "not yet provisioned" semantics.Companion PRs
billing:subscription_succeeded/billing:subscription_renewed, sampledqueue_state_changed).ComfyUI-Desktop-2.0-Beta) instrumentation is tracked separately and will cross-link here.