Skip to content
Closed
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
151b667
feat(telemetry): instrument cloud funnel gaps (auth, onboarding, canv…
deepme987 Jun 17, 2026
68cd6c8
[automated] Apply ESLint and Oxfmt fixes
actions-user Jun 17, 2026
938609b
test(telemetry): positive-path + behavioral tests for the cloud funne…
deepme987 Jun 17, 2026
2cda397
refactor(telemetry): address review on the cloud funnel events
deepme987 Jun 17, 2026
7da664a
feat(telemetry): add funnel-completion events + context super-properties
deepme987 Jun 17, 2026
8fe6159
feat(website): add explicit homepage CTA telemetry
deepme987 Jun 18, 2026
ad65486
feat(telemetry): fire subscribe-now click from real tier CTAs
deepme987 Jun 18, 2026
5f789e1
[automated] Apply ESLint and Oxfmt fixes
actions-user Jun 18, 2026
f303872
feat(telemetry): track monthly/yearly billing-cycle toggle on pricing…
deepme987 Jun 18, 2026
ffd37e8
feat(telemetry): add auth_error + template_category events; enable cl…
deepme987 Jun 18, 2026
cefe24a
Merge remote-tracking branch 'origin/main' into deepme987/frontend/cl…
deepme987 Jun 18, 2026
631bab4
Merge remote-tracking branch 'origin/deepme987/feat/cloud-funnel-tele…
deepme987 Jun 18, 2026
2338efc
Merge remote-tracking branch 'origin/deepme987/feat/website-cta-telem…
deepme987 Jun 18, 2026
464f383
fix(telemetry): resolve consolidation follow-ups
deepme987 Jun 18, 2026
b7f0acc
[automated] Apply ESLint and Oxfmt fixes
actions-user Jun 18, 2026
e680a68
docs(telemetry): trim verbose comments to repo norm (1-2 lines)
deepme987 Jun 19, 2026
bb4f31c
Merge origin/main into deepme987/frontend/cloud-funnel-telemetry
deepme987 Jun 19, 2026
ecf30e0
chore: revert incidental Tailwind class reorders in website CTA compo…
deepme987 Jun 19, 2026
08a0136
test(telemetry): cover new registry funnel-event dispatch methods
deepme987 Jun 19, 2026
a2b2352
chore: re-trigger CI
deepme987 Jun 19, 2026
b70b862
Merge origin/main into deepme987/frontend/cloud-funnel-telemetry
deepme987 Jun 19, 2026
b8db435
Merge origin/main into deepme987/frontend/cloud-funnel-telemetry
deepme987 Jun 19, 2026
4b73a70
Merge branch 'main' into deepme987/frontend/cloud-funnel-telemetry
deepme987 Jun 22, 2026
f4b1c19
fix(telemetry): correct auth-funnel + run-gate event accuracy (review)
deepme987 Jun 22, 2026
fe9cb9d
docs(telemetry): cut over-commenting to repo terse norm
deepme987 Jun 22, 2026
2b27182
Merge remote-tracking branch 'origin/main' into deepme987/frontend/cl…
deepme987 Jun 22, 2026
147c4f4
refactor(telemetry): drop FE-inferred subscription-success event and …
deepme987 Jun 23, 2026
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
18 changes: 15 additions & 3 deletions apps/website/src/components/common/HeaderMain/HeaderMain.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import type { Locale } from '../../../i18n/translations.ts'
import { t } from '../../../i18n/translations.ts'
import { externalLinks, getRoutes } from '../../../config/routes.ts'
import type { CtaButton } from '../../../scripts/posthog'
import { captureCtaClick } from '../../../scripts/posthog'
import GitHubStarBadge from '../GitHubStarBadge.vue'
import HeaderMainDesktop from './HeaderMainDesktop.vue'
import HeaderMainMobile from './HeaderMainMobile.vue'
Expand All @@ -13,20 +15,29 @@ const { locale = 'en', githubStars = '' } = defineProps<{
}>()
const routes = getRoutes(locale)

const ctaButtons = [
const ctaButtons: {
prefix: string
core: string
ariaLabel: string
href: string
primary: boolean
ctaButton: CtaButton
}[] = [
{
prefix: t('nav.ctaDesktopPrefix', locale),
core: t('nav.ctaDesktopCore', locale),
ariaLabel: t('nav.downloadLocal', locale),
href: routes.download,
primary: false
primary: false,
ctaButton: 'download_desktop'
},
{
prefix: t('nav.ctaCloudPrefix', locale),
core: t('nav.ctaCloudCore', locale),
ariaLabel: t('nav.launchCloud', locale),
href: externalLinks.cloud,
primary: true
primary: true,
ctaButton: 'launch_cloud'
}
]
</script>
Expand Down Expand Up @@ -74,6 +85,7 @@ const ctaButtons = [
:href="cta.href"
:variant="cta.primary ? 'default' : 'outline'"
:aria-label="cta.ariaLabel"
@click="captureCtaClick(cta.ctaButton, 'nav')"
>
<span
><span class="hidden xl:inline-block">{{ cta.prefix }}&nbsp;</span
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { getMainNavigation } from '../../../data/mainNavigation'
import type { NavItem } from '../../../data/mainNavigation'
import type { Locale } from '../../../i18n/translations'
import { captureCtaClick } from '../../../scripts/posthog'
import NavColumn from './NavColumn.vue'
import NavFeaturedCard from './NavFeaturedCard.vue'

Expand Down Expand Up @@ -66,9 +67,14 @@ function isNavItemActive(navItem: NavItem, path: string): boolean {
:active="isNavItemActive(navItem, currentPath)"
:class="navigationMenuTriggerStyle()"
>
<a :href="navItem.href" class="ppformula-text-center">{{
navItem.label
}}</a>
<a
:href="navItem.href"
class="ppformula-text-center"
@click="
navItem.ctaButton && captureCtaClick(navItem.ctaButton, 'nav')
"
>{{ navItem.label }}</a
>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getRoutes } from '../../../config/routes.ts'
import { lockScroll, unlockScroll } from '../../../composables/scrollLock'
import type { Locale } from '../../../i18n/translations.ts'
import { t } from '../../../i18n/translations.ts'
import { captureCtaClick } from '../../../scripts/posthog'
import NavLinkContent from './NavLinkContent.vue'
import Sheet from '@/components/ui/sheet/Sheet.vue'
import SheetContent from '@/components/ui/sheet/SheetContent.vue'
Expand Down Expand Up @@ -94,7 +95,11 @@ onUnmounted(() => {
variant="navMuted"
:type="item.columns ? 'button' : undefined"
:href="item.columns ? undefined : item.href"
@click="item.columns && (activeSection = item.label)"
@click="
item.columns
? (activeSection = item.label)
: item.ctaButton && captureCtaClick(item.ctaButton, 'nav')
"
>
{{ item.label }}
<template #append>
Expand Down Expand Up @@ -147,6 +152,9 @@ onUnmounted(() => {
as="a"
:target="link.external ? '_blank' : undefined"
:rel="link.external ? 'noopener noreferrer' : undefined"
@click="
link.ctaButton && captureCtaClick(link.ctaButton, 'nav')
"
>
<NavLinkContent :item="link" :locale="locale" />
</Button>
Expand Down
2 changes: 2 additions & 0 deletions apps/website/src/components/common/HeaderMain/NavColumn.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import NavigationMenuLink from '@/components/ui/navigation-menu/NavigationMenuLi
import { isHrefActive } from '../../../composables/useCurrentPath'
import type { NavColumn } from '../../../data/mainNavigation'
import type { Locale } from '../../../i18n/translations'
import { captureCtaClick } from '../../../scripts/posthog'
import NavLinkContent from './NavLinkContent.vue'

defineProps<{ column: NavColumn; locale: Locale; currentPath: string }>()
Expand All @@ -26,6 +27,7 @@ defineProps<{ column: NavColumn; locale: Locale; currentPath: string }>()
:target="item.external ? '_blank' : undefined"
:rel="item.external ? 'noopener noreferrer' : undefined"
class="whitespace-nowrap"
@click="item.ctaButton && captureCtaClick(item.ctaButton, 'nav')"
>
<NavLinkContent :item="item" :locale="locale" />
</a>
Expand Down
13 changes: 11 additions & 2 deletions apps/website/src/components/common/ProductCard.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
<script setup lang="ts">
import { cn } from '@comfyorg/tailwind-utils'

const { title, description, cta, href, bg } = defineProps<{
import type { CtaButton } from '../../scripts/posthog'
import { captureCtaClick } from '../../scripts/posthog'

const { title, description, cta, href, bg, ctaButton } = defineProps<{
title: string
description: string
cta: string
href: string
bg: string
ctaButton?: CtaButton
}>()

function onClick() {
if (ctaButton) captureCtaClick(ctaButton, 'products_section')
}
</script>

<template>
Expand All @@ -19,6 +27,7 @@ const { title, description, cta, href, bg } = defineProps<{
bg
)
"
@click="onClick"
>
<h3 class="text-3xl font-light whitespace-pre-line text-white lg:text-4xl">
{{ title }}
Expand All @@ -29,7 +38,7 @@ const { title, description, cta, href, bg } = defineProps<{
{{ description }}
</p>
<span
class="bg-primary-comfy-yellow text-primary-comfy-ink mt-4 inline-block rounded-xl px-4 py-2 text-xs font-bold tracking-wide"
class="bg-primary-comfy-yellow mt-4 inline-block rounded-xl px-4 py-2 text-xs font-bold tracking-wide text-primary-comfy-ink"
>
{{ cta }}
</span>
Expand Down
11 changes: 10 additions & 1 deletion apps/website/src/components/common/ProductCardsSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@ import { cn } from '@comfyorg/tailwind-utils'

import { getRoutes } from '../../config/routes'
import { t } from '../../i18n/translations'
import type { CtaButton } from '../../scripts/posthog'
import ProductCard from './ProductCard.vue'
import SectionLabel from './SectionLabel.vue'

type Product = 'local' | 'cloud' | 'api' | 'enterprise'

const ctaButtonByProduct: Record<Product, CtaButton> = {
local: 'download_desktop',
cloud: 'comfy_cloud',
api: 'products',
enterprise: 'products'
}

const {
locale = 'en',
excludeProduct,
Expand All @@ -29,7 +37,8 @@ function cardDef(product: Product, href: string, bg: string) {
description: t(`products.${product}.description`, locale),
cta: t(`products.${product}.cta`, locale),
href,
bg
bg,
ctaButton: ctaButtonByProduct[product]
}
}

Expand Down
2 changes: 2 additions & 0 deletions apps/website/src/components/home/HeroSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Locale } from '../../i18n/translations'
import { externalLinks } from '../../config/routes'
import { useHeroLogo } from '../../composables/useHeroLogo'
import { t } from '../../i18n/translations'
import { captureCtaClick } from '../../scripts/posthog'
import BrandButton from '../common/BrandButton.vue'

const { locale = 'en' } = defineProps<{ locale?: Locale }>()
Expand Down Expand Up @@ -47,6 +48,7 @@ const { loaded: logoLoaded } = useHeroLogo(logoContainer)
variant="outline"
size="lg"
class="mt-8 w-full p-4 uppercase lg:w-auto lg:min-w-60"
@click="captureCtaClick('run_first_workflow', 'hero')"
>
{{ t('hero.runFirstWorkflow', locale) }}
</BrandButton>
Expand Down
29 changes: 25 additions & 4 deletions apps/website/src/data/mainNavigation.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { externalLinks, getRoutes } from '../config/routes'
import type { Locale } from '../i18n/translations'
import { t } from '../i18n/translations'
import type { CtaButton } from '../scripts/posthog'

export type NavColumnItem = {
label: string
href: string
badge?: 'new'
external?: boolean
ctaButton?: CtaButton
}

export type NavColumn = {
Expand All @@ -31,8 +33,15 @@ export type NavItem =
columns: NavColumn[]
featured?: NavFeatured
href?: never
ctaButton?: never
}
| {
label: string
href: string
columns?: never
featured?: never
ctaButton?: CtaButton
}
| { label: string; href: string; columns?: never; featured?: never }

export function getMainNavigation(locale: Locale): NavItem[] {
const routes = getRoutes(locale)
Expand All @@ -53,8 +62,16 @@ export function getMainNavigation(locale: Locale): NavItem[] {
{
header: t('nav.products', locale),
items: [
{ label: t('nav.comfyLocal', locale), href: routes.download },
{ label: t('nav.comfyCloud', locale), href: routes.cloud },
{
label: t('nav.comfyLocal', locale),
href: routes.download,
ctaButton: 'comfy_desktop'
},
{
label: t('nav.comfyCloud', locale),
href: routes.cloud,
ctaButton: 'comfy_cloud'
},
{
label: t('nav.comfyApi', locale),
href: routes.api,
Expand Down Expand Up @@ -82,7 +99,11 @@ export function getMainNavigation(locale: Locale): NavItem[] {
}
]
},
{ label: t('nav.pricing', locale), href: routes.cloudPricing },
{
label: t('nav.pricing', locale),
href: routes.cloudPricing,
ctaButton: 'pricing'
},
{
label: t('nav.community', locale),
featured: {
Expand Down
25 changes: 25 additions & 0 deletions apps/website/src/scripts/posthog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,28 @@ describe('captureDownloadClick', () => {
expect(hoisted.mockCapture).not.toHaveBeenCalled()
})
})

describe('captureCtaClick', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.resetModules()
})

it('captures the CTA event with button and location', async () => {
const { initPostHog, captureCtaClick } = await import('./posthog')
initPostHog()
captureCtaClick('launch_cloud', 'nav')

expect(hoisted.mockCapture).toHaveBeenCalledWith('website:cta_clicked', {
button: 'launch_cloud',
location: 'nav'
})
})

it('does not capture before PostHog is initialized', async () => {
const { captureCtaClick } = await import('./posthog')
captureCtaClick('run_first_workflow', 'hero')

expect(hoisted.mockCapture).not.toHaveBeenCalled()
})
})
20 changes: 20 additions & 0 deletions apps/website/src/scripts/posthog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,23 @@ export function captureDownloadClick(platform: Platform) {
console.error('PostHog download click capture failed', error)
}
}

export type CtaButton =
| 'launch_cloud'
| 'download_desktop'
| 'pricing'
| 'run_first_workflow'
| 'products'
| 'comfy_desktop'
| 'comfy_cloud'

export type CtaLocation = 'nav' | 'hero' | 'products_section'

export function captureCtaClick(button: CtaButton, location: CtaLocation) {
if (!initialized) return
try {
posthog.capture('website:cta_clicked', { button, location })
} catch (error) {
console.error('PostHog CTA click capture failed', error)
}
}
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.

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.

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?

watch(selectedNavItem, (to, from) => {
if (!to || to === from) return

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.

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.

useTelemetry()?.trackTemplateCategorySelected({ category_id: to })
})

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.

🛠️ 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.


// Filter templates based on selected navigation item
const navigationFilteredTemplates = computed(() => {
if (!selectedNavItem.value) {
Expand Down
Loading
Loading