Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,13 @@ const allTemplates = computed(() => {
// Navigation
const selectedNavItem = ref<string | null>(initialCategory)

// 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 })
})
Comment on lines +549 to +552

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Category-selection telemetry fires on internal sort-driven nav resets.

Line 549 watches all selectedNavItem changes, but Line 622 also sets selectedNavItem programmatically (popularall) when sort changes. That means trackTemplateCategorySelected is emitted for non-user category selections.

Proposed fix
 const selectedNavItem = ref<string | null>(initialCategory)
+const navChangeSource = ref<'user' | 'sort_sync'>('user')

 watch(selectedNavItem, (to, from) => {
+  if (navChangeSource.value === 'sort_sync') {
+    navChangeSource.value = 'user'
+    return
+  }
   if (!to || to === from) return
   useTelemetry()?.trackTemplateCategorySelected({ category_id: to })
 })
@@
   } else if (source === 'sort') {
@@
     if (isPopularNav && !isPopularSort) {
+      navChangeSource.value = 'sort_sync'
       selectedNavItem.value = 'all'
     }
   }
 }
🤖 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
549 - 552, The watcher on selectedNavItem in WorkflowTemplateSelectorDialog.vue
is firing telemetry for all changes to selectedNavItem, including programmatic
changes that occur when the sort order changes (line 622 resets selectedNavItem
from popular to all). Move the trackTemplateCategorySelected telemetry call from
the selectedNavItem watcher to the actual user interaction handler (the
click/select event that triggers the category selection) instead, so telemetry
only fires for intentional user selections and not for programmatic resets
triggered by sort changes.


// Filter templates based on selected navigation item
const navigationFilteredTemplates = computed(() => {
if (!selectedNavItem.value) {
Expand Down
99 changes: 98 additions & 1 deletion src/platform/cloud/subscription/components/PricingTable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const mockIsYearlySubscription = ref(false)
const mockAccessBillingPortal = vi.fn()
const mockReportError = vi.fn()
const mockTrackBeginCheckout = vi.fn()
const mockTrackSubscription = vi.fn()
const mockTrackBillingCycleToggled = vi.fn()
const mockUserId = ref<string | undefined>('user-123')
const mockGetAuthHeader = vi.fn(() =>
Promise.resolve({ Authorization: 'Bearer test-token' })
Expand Down Expand Up @@ -111,7 +113,9 @@ vi.mock('@/stores/authStore', () => ({

vi.mock('@/platform/telemetry', () => ({
useTelemetry: () => ({
trackBeginCheckout: mockTrackBeginCheckout
trackBeginCheckout: mockTrackBeginCheckout,
trackSubscription: mockTrackSubscription,
trackBillingCycleToggled: mockTrackBillingCycleToggled
})
}))

Expand Down Expand Up @@ -222,6 +226,8 @@ describe('PricingTable', () => {
mockAccessBillingPortal.mockReset()
mockAccessBillingPortal.mockResolvedValue(true)
mockTrackBeginCheckout.mockReset()
mockTrackSubscription.mockReset()
mockTrackBillingCycleToggled.mockReset()
mockLocalStorage.__reset()
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
Expand Down Expand Up @@ -439,4 +445,95 @@ describe('PricingTable', () => {
expect(onChooseTeamWorkspace).toHaveBeenCalledOnce()
})
})

describe('subscribe-now click telemetry', () => {
it('fires subscribe_clicked with tier/cycle/source on the new-subscriber path', async () => {
mockIsActiveSubscription.value = false

const windowOpenSpy = vi
.spyOn(window, 'open')
.mockImplementation(() => null)

renderComponent()
await flushPromises()

const creatorButton = screen
.getAllByRole('button')
.find((b) => b.textContent?.includes('Creator'))

await userEvent.click(creatorButton!)
await flushPromises()

expect(mockTrackSubscription).toHaveBeenCalledWith('subscribe_clicked', {
current_tier: undefined,
tier: 'creator',
cycle: 'yearly',
source: 'pricing_table'
})

windowOpenSpy.mockRestore()
})

it('fires subscribe_clicked on the change path for an existing subscriber', async () => {
mockIsActiveSubscription.value = true
mockSubscriptionTier.value = 'STANDARD'

renderComponent()
await flushPromises()

const proButton = screen
.getAllByRole('button')
.find((b) => b.textContent?.includes('Pro'))

await userEvent.click(proButton!)
await flushPromises()

expect(mockTrackSubscription).toHaveBeenCalledWith('subscribe_clicked', {
current_tier: 'standard',
tier: 'pro',
cycle: 'yearly',
source: 'pricing_table'
})
})

it('does not fire subscribe_clicked when clicking the current plan', async () => {
mockIsActiveSubscription.value = true
mockSubscriptionTier.value = 'CREATOR'

renderComponent()
await flushPromises()

const currentPlanButton = screen
.getAllByRole('button')
.find((b) => b.textContent?.includes('Current Plan'))

await userEvent.click(currentPlanButton!)
await flushPromises()

expect(mockTrackSubscription).not.toHaveBeenCalled()
})
})

describe('billing cycle toggle telemetry', () => {
it('fires billing_cycle_toggled with from/to when switching to monthly', async () => {
renderComponent()
await flushPromises()

const monthlyToggle = screen.getByRole('button', { name: 'Monthly' })
await userEvent.click(monthlyToggle)
await flushPromises()

expect(mockTrackBillingCycleToggled).toHaveBeenCalledWith({
from: 'yearly',
to: 'monthly'
})
})

it('does not fire on initial render when the cycle has not changed', async () => {
renderComponent()
await flushPromises()

expect(mockTrackBillingCycleToggled).not.toHaveBeenCalled()
})
})
})
19 changes: 18 additions & 1 deletion src/platform/cloud/subscription/components/PricingTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ import { storeToRefs } from 'pinia'
import Popover from 'primevue/popover'
import SelectButton from 'primevue/selectbutton'
import type { ToggleButtonPassThroughMethodOptions } from 'primevue/togglebutton'
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'

import Button from '@/components/ui/button/Button.vue'
Expand Down Expand Up @@ -374,6 +374,13 @@ const loadingTier = ref<CheckoutTierKey | null>(null)
const popover = ref()
const currentBillingCycle = ref<BillingCycle>('yearly')

// Track monthly/yearly toggles so we can see whether the annual-discount
// nudge actually moves the cycle selection before checkout.
watch(currentBillingCycle, (to, from) => {
if (!isCloud || to === from) return
telemetry?.trackBillingCycleToggled({ from, to })
})

const hasPaidSubscription = computed(
() => isActiveSubscription.value && !isFreeTier.value
)
Expand Down Expand Up @@ -448,6 +455,16 @@ const handleSubscribe = wrapWithErrorHandlingAsync(
isLoading.value = true
loadingTier.value = tierKey

// Fire the subscribe-now click before opening checkout so the funnel
// 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(),
tier: tierKey,
cycle: currentBillingCycle.value,
source: 'pricing_table'
})

try {
if (hasPaidSubscription.value) {
const targetPlan = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ watch(
const handleSubscribe = () => {
if (isCloud) {
useTelemetry()?.trackSubscription('subscribe_clicked', {
current_tier: subscriptionTier.value?.toLowerCase()
current_tier: subscriptionTier.value?.toLowerCase(),
source: 'subscribe_button'
})
}
isAwaitingStripeSubscription.value = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import SubscribeToRun from './SubscribeToRun.vue'
const mockShowSubscriptionDialog = vi.fn()
const mockCanManageSubscription = ref(true)
const mockIsMdOrLarger = ref(true)
const mockTrackRunButton = vi.fn()
const mockTrackSubscription = vi.fn()

vi.mock('@/composables/billing/useBillingContext', () => ({
useBillingContext: () => ({
Expand All @@ -30,7 +32,10 @@ vi.mock('@/platform/distribution/types', () => ({
}))

vi.mock('@/platform/telemetry', () => ({
useTelemetry: () => null
useTelemetry: () => ({
trackRunButton: mockTrackRunButton,
trackSubscription: mockTrackSubscription
})
}))

vi.mock('@vueuse/core', async (importOriginal) => {
Expand Down Expand Up @@ -111,4 +116,15 @@ describe('SubscribeToRun', () => {

expect(mockShowSubscriptionDialog).toHaveBeenCalledOnce()
})

it('tracks both the run button and a subscribe-now click on click', async () => {
const { user } = renderButton()

await user.click(screen.getByTestId('subscribe-to-run-button'))

expect(mockTrackRunButton).toHaveBeenCalledWith({ subscribe_to_run: true })
expect(mockTrackSubscription).toHaveBeenCalledWith('subscribe_clicked', {
source: 'subscribe_to_run'
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ const buttonTooltip = computed(() =>

function handleSubscribeToRun() {
if (isCloud) {
useTelemetry()?.trackRunButton({ subscribe_to_run: true })
const telemetry = useTelemetry()
telemetry?.trackRunButton({ subscribe_to_run: true })
// Also count this as a subscribe-now click so the lock-button CTA shows up
// in the subscribe-click funnel alongside the pricing table and the
// legacy SubscribeButton (previously the only fired surface).
telemetry?.trackSubscription('subscribe_clicked', {
source: 'subscribe_to_run'
})
}

showSubscriptionDialog()
Expand Down
19 changes: 19 additions & 0 deletions src/platform/telemetry/TelemetryRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type { AuditLog } from '@/services/customerEventsService'
import type {
AuthMetadata,
BeginCheckoutMetadata,
BillingCycleToggledMetadata,
AuthErrorMetadata,
TemplateCategorySelectedMetadata,
DefaultViewSetMetadata,
EnterLinearMetadata,
ShareFlowMetadata,
Expand Down Expand Up @@ -86,6 +89,22 @@ export class TelemetryRegistry implements TelemetryDispatcher {
this.dispatch((provider) => provider.trackBeginCheckout?.(metadata))
}

trackBillingCycleToggled(metadata: BillingCycleToggledMetadata): void {
this.dispatch((provider) => provider.trackBillingCycleToggled?.(metadata))
}

trackAuthError(metadata: AuthErrorMetadata): void {
this.dispatch((provider) => provider.trackAuthError?.(metadata))
}

trackTemplateCategorySelected(
metadata: TemplateCategorySelectedMetadata
): void {
this.dispatch((provider) =>
provider.trackTemplateCategorySelected?.(metadata)
)
}

trackMonthlySubscriptionSucceeded(
metadata?: SubscriptionSuccessMetadata
): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,18 @@ describe('PostHogTelemetryProvider', () => {
)
})

it('captures billing cycle toggles with from/to', async () => {
const provider = createProvider()
await vi.dynamicImportSettled()

provider.trackBillingCycleToggled({ from: 'yearly', to: 'monthly' })

expect(hoisted.mockCapture).toHaveBeenCalledWith(
TelemetryEvents.BILLING_CYCLE_TOGGLED,
{ from: 'yearly', to: 'monthly' }
)
})

it('captures search queries with surface, query, length, and result count', async () => {
const provider = createProvider()
await vi.dynamicImportSettled()
Expand Down
28 changes: 25 additions & 3 deletions src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

import type {
AuthMetadata,
BillingCycleToggledMetadata,
AuthErrorMetadata,
TemplateCategorySelectedMetadata,
DefaultViewSetMetadata,
EnterLinearMetadata,
ShareFlowMetadata,
Expand Down Expand Up @@ -126,9 +129,14 @@
api_host:
window.__CONFIG__?.posthog_api_host || 'https://t.comfy.org',
ui_host: 'https://us.posthog.com',
autocapture: false,
capture_pageview: false,
capture_pageleave: false,
// Web analytics enabled so cloud.comfy.org gets heatmaps + $pageview
// (the login/onboarding pages previously had zero coverage). autocapture
// does NOT record input *values* (posthog masks them), and these defaults
// remain overridable per-environment via `serverConfig` (spread below).
autocapture: true,
capture_pageview: true,
capture_pageleave: true,
heatmaps: true,

Check failure on line 139 in src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts

View workflow job for this annotation

GitHub Actions / lint-and-format

Object literal may only specify known properties, and 'heatmaps' does not exist in type 'Partial<PostHogConfig>'.

Check failure on line 139 in src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts

View workflow job for this annotation

GitHub Actions / perf-tests

Object literal may only specify known properties, and 'heatmaps' does not exist in type 'Partial<PostHogConfig>'.

Check failure on line 139 in src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts

View workflow job for this annotation

GitHub Actions / setup

Object literal may only specify known properties, and 'heatmaps' does not exist in type 'Partial<PostHogConfig>'.

Check failure on line 139 in src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts

View workflow job for this annotation

GitHub Actions / scan

Object literal may only specify known properties, and 'heatmaps' does not exist in type 'Partial<PostHogConfig>'.

Check failure on line 139 in src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts

View workflow job for this annotation

GitHub Actions / validate-fonts

Object literal may only specify known properties, and 'heatmaps' does not exist in type 'Partial<PostHogConfig>'.

Check failure on line 139 in src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts

View workflow job for this annotation

GitHub Actions / collect

Object literal may only specify known properties, and 'heatmaps' does not exist in type 'Partial<PostHogConfig>'.
persistence: 'localStorage+cookie',
Comment on lines +132 to 140

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

For the posthog-js version used in this repository, what is the correct posthog.initoption to enable heatmaps? Isheatmapsvalid, or should a different key be used (for exampledisable_heatmaps)?

💡 Result:

To enable heatmaps in posthog-js, the correct configuration option to use in the posthog.init call is enable_heatmaps [1][2]. You should set this option to true within the configuration object: posthog.init('<ph_project_token>', { api_host: 'https://us.i.posthog.com', enable_heatmaps: true }) While some internal configuration references or older/alternative settings may show capture_heatmaps [3], the official and documented key for enabling this feature via initialization is enable_heatmaps [1][2]. Note that if you want to disable features, PostHog typically uses a disable_* prefix (e.g., disable_session_recording), but for enabling heatmaps, the enable_heatmaps key is the standard approach [1][3].

Citations:


🏁 Script executed:

# Check the PostHogTelemetryProvider.ts file around line 139
sed -n '130,145p' src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts

# Also check the PostHog version in package.json
rg '"posthog"' package.json -A 1

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1058


🏁 Script executed:

# Check PostHog version
rg '"posthog"' package.json -B 1 -A 1

# Also verify the TypeScript error by checking tsconfig
fd -n tsconfig

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 301


🏁 Script executed:

# Check PostHog version
rg '"posthog"' package.json -B 1 -A 1

# Find tsconfig files
fd tsconfig

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 502


🏁 Script executed:

# Find package.json files and check PostHog version
find . -name "package.json" -type f | head -5 | xargs grep -l "posthog"

# Check the root package.json for PostHog
cat package.json | grep -A 2 -B 2 "posthog"

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 252


🏁 Script executed:

# Check pnpm workspace catalog or pnpm-lock.yaml for actual PostHog version
rg 'posthog-js' pnpm-lock.yaml -A 3 | head -20

# Also check the catalog in package.json structure
cat package.json | grep -A 50 "catalog"

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 5781


🏁 Script executed:

# Check if there are any TypeScript definitions for PostHog to see what keys are allowed
rg "interface PostHogConfig" --type ts -A 20

# Also search for any existing usage of enable_heatmaps or heatmaps in the codebase
rg "enable_heatmaps|heatmaps" --type ts -B 2 -A 2

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1356


🏁 Script executed:

# Check the node_modules PostHog types to see if heatmaps is a valid config key
rg "heatmaps|enable_heatmaps" node_modules/posthog-js -t ts -A 1 -B 1 2>/dev/null | head -30

# Also check what keys are actually defined in PostHogConfig
rg "PostHogConfig" node_modules/posthog-js --type ts -A 30 2>/dev/null | head -50

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


🏁 Script executed:

# Check if there's a build or tsc output showing the error
rg "TS2353" . --type ts --type json 2>/dev/null | head -10

# Let's verify the exact line and surrounding context
sed -n '128,145p' src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts | cat -n

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1249


Replace heatmaps with enable_heatmaps on line 139

The PostHog SDK (v1.358.1) does not support the heatmaps configuration key. Use enable_heatmaps: true instead to enable heatmaps in the initialization config.

🧰 Tools
🪛 GitHub Actions: CI: Dist Telemetry Scan / 0_scan.txt

[error] 139-139: TypeScript (vue-tsc) error TS2353: Object literal may only specify known properties; 'heatmaps' does not exist in type 'Partial'.

🪛 GitHub Actions: CI: Dist Telemetry Scan / scan

[error] 139-139: TypeScript (vue-tsc) error TS2353: Object literal may only specify known properties, and 'heatmaps' does not exist in type 'Partial'.

🪛 GitHub Actions: CI: OSS Assets Validation / 1_validate-fonts.txt

[error] 139-139: TypeScript (vue-tsc) failed with TS2353: Object literal may only specify known properties; property 'heatmaps' does not exist in type 'Partial'.

🪛 GitHub Actions: CI: OSS Assets Validation / validate-fonts

[error] 139-139: TypeScript (vue-tsc) error TS2353: Object literal may only specify known properties, and 'heatmaps' does not exist in type 'Partial'.

🪛 GitHub Check: scan

[failure] 139-139:
Object literal may only specify known properties, and 'heatmaps' does not exist in type 'Partial'.

🪛 GitHub Check: validate-fonts

[failure] 139-139:
Object literal may only specify known properties, and 'heatmaps' does not exist in type 'Partial'.

🤖 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/telemetry/providers/cloud/PostHogTelemetryProvider.ts` around
lines 132 - 140, The PostHog SDK initialization configuration in
PostHogTelemetryProvider contains an invalid configuration key `heatmaps: true`
which is not supported by PostHog SDK v1.358.1. Replace the `heatmaps` key with
`enable_heatmaps` in the configuration object that includes other properties
like `autocapture`, `capture_pageview`, `capture_pageleave`, and `persistence`
to properly enable heatmaps functionality in PostHog.

Sources: Linters/SAST tools, Pipeline failures

debug: import.meta.env.VITE_POSTHOG_DEBUG === 'true',
...serverConfig,
Expand Down Expand Up @@ -350,6 +358,20 @@
this.trackEvent(eventName, metadata)
}

trackBillingCycleToggled(metadata: BillingCycleToggledMetadata): void {
this.trackEvent(TelemetryEvents.BILLING_CYCLE_TOGGLED, metadata)
}

trackAuthError(metadata: AuthErrorMetadata): void {
this.trackEvent(TelemetryEvents.AUTH_ERROR, metadata)
}

trackTemplateCategorySelected(
metadata: TemplateCategorySelectedMetadata
): void {
this.trackEvent(TelemetryEvents.TEMPLATE_CATEGORY_SELECTED, metadata)
}

trackAddApiCreditButtonClicked(): void {
this.trackEvent(TelemetryEvents.ADD_API_CREDIT_BUTTON_CLICKED)
}
Expand Down
Loading
Loading