feat: track app mode entry and shared workflow loading (#9720)

## Summary

- Track entering app mode from template URL (`source: template_url`) and
default view dialog (`source: default_view_dialog`)
- Tag shared workflow loads with `openSource: 'shared'` instead of
defaulting to `'unknown'`
- Rename telemetry event from `app:toggle_linear_mode` to
`app:app_mode_opened`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9720-feat-track-app-mode-entry-and-shared-workflow-loading-31f6d73d365081af8c6ae3247a50cf3f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Robin Huang
2026-03-10 15:05:19 -07:00
committed by GitHub
parent 12989e8b63
commit e89a0f96cd
13 changed files with 87 additions and 7 deletions

View File

@@ -38,6 +38,9 @@ TEST_COMFYUI_DIR=/home/ComfyUI
ALGOLIA_APP_ID=4E0RO38HS8 ALGOLIA_APP_ID=4E0RO38HS8
ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579 ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579
# Enable PostHog debug logging in the browser console.
# VITE_POSTHOG_DEBUG=true
# Sentry ENV vars replace with real ones for debugging # Sentry ENV vars replace with real ones for debugging
# SENTRY_AUTH_TOKEN=private-token # get from sentry # SENTRY_AUTH_TOKEN=private-token # get from sentry
# SENTRY_ORG=comfy-org # SENTRY_ORG=comfy-org

View File

@@ -1,6 +1,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useAppMode } from '@/composables/useAppMode' import { useAppMode } from '@/composables/useAppMode'
import { useTelemetry } from '@/platform/telemetry'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app' import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService' import { useDialogService } from '@/services/dialogService'
@@ -54,6 +55,7 @@ export function useAppSetDefaultView() {
appliedAsApp, appliedAsApp,
onViewApp: () => { onViewApp: () => {
closeAppliedDialog() closeAppliedDialog()
useTelemetry()?.trackEnterLinear({ source: 'app_builder' })
setMode('app') setMode('app')
}, },
onExitToWorkflow: () => { onExitToWorkflow: () => {

View File

@@ -4,6 +4,7 @@ import type {
AuthMetadata, AuthMetadata,
BeginCheckoutMetadata, BeginCheckoutMetadata,
EnterLinearMetadata, EnterLinearMetadata,
ShareFlowMetadata,
ExecutionErrorMetadata, ExecutionErrorMetadata,
ExecutionSuccessMetadata, ExecutionSuccessMetadata,
ExecutionTriggerSource, ExecutionTriggerSource,
@@ -160,6 +161,10 @@ export class TelemetryRegistry implements TelemetryDispatcher {
this.dispatch((provider) => provider.trackEnterLinear?.(metadata)) this.dispatch((provider) => provider.trackEnterLinear?.(metadata))
} }
trackShareFlow(metadata: ShareFlowMetadata): void {
this.dispatch((provider) => provider.trackShareFlow?.(metadata))
}
trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void { trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void {
this.dispatch((provider) => provider.trackPageVisibilityChanged?.(metadata)) this.dispatch((provider) => provider.trackPageVisibilityChanged?.(metadata))
} }

View File

@@ -15,6 +15,7 @@ import type {
AuthMetadata, AuthMetadata,
CreditTopupMetadata, CreditTopupMetadata,
EnterLinearMetadata, EnterLinearMetadata,
ShareFlowMetadata,
ExecutionContext, ExecutionContext,
ExecutionTriggerSource, ExecutionTriggerSource,
ExecutionErrorMetadata, ExecutionErrorMetadata,
@@ -362,6 +363,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
this.trackEvent(TelemetryEvents.ENTER_LINEAR_MODE, metadata) this.trackEvent(TelemetryEvents.ENTER_LINEAR_MODE, metadata)
} }
trackShareFlow(metadata: ShareFlowMetadata): void {
this.trackEvent(TelemetryEvents.SHARE_FLOW, metadata)
}
trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void { trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void {
this.trackEvent(TelemetryEvents.PAGE_VISIBILITY_CHANGED, metadata) this.trackEvent(TelemetryEvents.PAGE_VISIBILITY_CHANGED, metadata)
} }

View File

@@ -85,7 +85,8 @@ describe('PostHogTelemetryProvider', () => {
autocapture: false, autocapture: false,
capture_pageview: false, capture_pageview: false,
capture_pageleave: false, capture_pageleave: false,
persistence: 'localStorage+cookie' persistence: 'localStorage+cookie',
debug: false
}) })
}) })

View File

@@ -8,6 +8,7 @@ import type { RemoteConfig } from '@/platform/remoteConfig/types'
import type { import type {
AuthMetadata, AuthMetadata,
EnterLinearMetadata, EnterLinearMetadata,
ShareFlowMetadata,
ExecutionContext, ExecutionContext,
ExecutionErrorMetadata, ExecutionErrorMetadata,
ExecutionSuccessMetadata, ExecutionSuccessMetadata,
@@ -104,7 +105,8 @@ export class PostHogTelemetryProvider implements TelemetryProvider {
autocapture: false, autocapture: false,
capture_pageview: false, capture_pageview: false,
capture_pageleave: false, capture_pageleave: false,
persistence: 'localStorage+cookie' persistence: 'localStorage+cookie',
debug: import.meta.env.VITE_POSTHOG_DEBUG === 'true'
}) })
this.isInitialized = true this.isInitialized = true
this.flushEventQueue() this.flushEventQueue()
@@ -346,6 +348,10 @@ export class PostHogTelemetryProvider implements TelemetryProvider {
this.trackEvent(TelemetryEvents.ENTER_LINEAR_MODE, metadata) this.trackEvent(TelemetryEvents.ENTER_LINEAR_MODE, metadata)
} }
trackShareFlow(metadata: ShareFlowMetadata): void {
this.trackEvent(TelemetryEvents.SHARE_FLOW, metadata)
}
trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void { trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void {
this.trackEvent(TelemetryEvents.PAGE_VISIBILITY_CHANGED, metadata) this.trackEvent(TelemetryEvents.PAGE_VISIBILITY_CHANGED, metadata)
} }

View File

@@ -137,13 +137,29 @@ export interface WorkflowImportMetadata {
/** /**
* The source of the workflow open/import action * The source of the workflow open/import action
*/ */
open_source?: 'file_button' | 'file_drop' | 'template' | 'unknown' open_source?:
| 'file_button'
| 'file_drop'
| 'template'
| 'shared_url'
| 'unknown'
} }
export interface EnterLinearMetadata { export interface EnterLinearMetadata {
source?: string source?: string
} }
type ShareFlowStep =
| 'dialog_opened'
| 'save_prompted'
| 'link_created'
| 'link_copied'
export interface ShareFlowMetadata {
step: ShareFlowStep
source?: 'app_mode' | 'graph_mode'
}
/** /**
* Workflow open metadata * Workflow open metadata
*/ */
@@ -362,6 +378,7 @@ export interface TelemetryProvider {
trackWorkflowImported?(metadata: WorkflowImportMetadata): void trackWorkflowImported?(metadata: WorkflowImportMetadata): void
trackWorkflowOpened?(metadata: WorkflowImportMetadata): void trackWorkflowOpened?(metadata: WorkflowImportMetadata): void
trackEnterLinear?(metadata: EnterLinearMetadata): void trackEnterLinear?(metadata: EnterLinearMetadata): void
trackShareFlow?(metadata: ShareFlowMetadata): void
// Page visibility events // Page visibility events
trackPageVisibilityChanged?(metadata: PageVisibilityMetadata): void trackPageVisibilityChanged?(metadata: PageVisibilityMetadata): void
@@ -447,7 +464,8 @@ export const TelemetryEvents = {
// Workflow Management // Workflow Management
WORKFLOW_IMPORTED: 'app:workflow_imported', WORKFLOW_IMPORTED: 'app:workflow_imported',
WORKFLOW_OPENED: 'app:workflow_opened', WORKFLOW_OPENED: 'app:workflow_opened',
ENTER_LINEAR_MODE: 'app:toggle_linear_mode', ENTER_LINEAR_MODE: 'app:app_mode_opened',
SHARE_FLOW: 'app:share_flow',
// Page Visibility // Page Visibility
PAGE_VISIBILITY_CHANGED: 'app:page_visibility_changed', PAGE_VISIBILITY_CHANGED: 'app:page_visibility_changed',
@@ -521,4 +539,5 @@ export type TelemetryEventProperties =
| HelpCenterClosedMetadata | HelpCenterClosedMetadata
| WorkflowCreatedMetadata | WorkflowCreatedMetadata
| EnterLinearMetadata | EnterLinearMetadata
| ShareFlowMetadata
| SubscriptionMetadata | SubscriptionMetadata

View File

@@ -26,17 +26,24 @@ import { refAutoReset } from '@vueuse/core'
import Button from '@/components/ui/button/Button.vue' import Button from '@/components/ui/button/Button.vue'
import Input from '@/components/ui/input/Input.vue' import Input from '@/components/ui/input/Input.vue'
import { useAppMode } from '@/composables/useAppMode'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard' import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useTelemetry } from '@/platform/telemetry'
const { url } = defineProps<{ const { url } = defineProps<{
url: string url: string
}>() }>()
const { copyToClipboard } = useCopyToClipboard() const { copyToClipboard } = useCopyToClipboard()
const { isAppMode } = useAppMode()
const copied = refAutoReset(false, 2000) const copied = refAutoReset(false, 2000)
async function handleCopy() { async function handleCopy() {
await copyToClipboard(url) await copyToClipboard(url)
copied.value = true copied.value = true
useTelemetry()?.trackShareFlow({
step: 'link_copied',
source: isAppMode.value ? 'app_mode' : 'graph_mode'
})
} }
</script> </script>

View File

@@ -167,7 +167,9 @@ import type {
import { useWorkflowShareService } from '@/platform/workflow/sharing/services/workflowShareService' import { useWorkflowShareService } from '@/platform/workflow/sharing/services/workflowShareService'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService' import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { useAppMode } from '@/composables/useAppMode'
import { useFeatureFlags } from '@/composables/useFeatureFlags' import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useTelemetry } from '@/platform/telemetry'
import { appendJsonExt } from '@/utils/formatUtil' import { appendJsonExt } from '@/utils/formatUtil'
import { cn } from '@/utils/tailwindUtil' import { cn } from '@/utils/tailwindUtil'
@@ -182,6 +184,11 @@ const publishDialog = useComfyHubPublishDialog()
const shareService = useWorkflowShareService() const shareService = useWorkflowShareService()
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const workflowService = useWorkflowService() const workflowService = useWorkflowService()
const { isAppMode } = useAppMode()
function getShareSource() {
return isAppMode.value ? 'app_mode' : ('graph_mode' as const)
}
type DialogState = 'loading' | 'unsaved' | 'ready' | 'shared' | 'stale' type DialogState = 'loading' | 'unsaved' | 'ready' | 'shared' | 'stale'
type DialogMode = 'shareLink' | 'publishToHub' type DialogMode = 'shareLink' | 'publishToHub'
@@ -298,6 +305,10 @@ async function refreshDialogState() {
if (!workflow || workflow.isTemporary || workflow.isModified) { if (!workflow || workflow.isTemporary || workflow.isModified) {
dialogState.value = 'unsaved' dialogState.value = 'unsaved'
useTelemetry()?.trackShareFlow({
step: 'save_prompted',
source: getShareSource()
})
if (workflow) { if (workflow) {
workflowName.value = stripJsonExtension(workflow.filename) workflowName.value = stripJsonExtension(workflow.filename)
} }
@@ -379,6 +390,10 @@ const {
) )
dialogState.value = 'shared' dialogState.value = 'shared'
acknowledged.value = false acknowledged.value = false
useTelemetry()?.trackShareFlow({
step: 'link_created',
source: getShareSource()
})
return result return result
}, },

View File

@@ -1,4 +1,6 @@
import ShareWorkflowDialogContent from '@/platform/workflow/sharing/components/ShareWorkflowDialogContent.vue' import ShareWorkflowDialogContent from '@/platform/workflow/sharing/components/ShareWorkflowDialogContent.vue'
import { useAppMode } from '@/composables/useAppMode'
import { useTelemetry } from '@/platform/telemetry'
import { useDialogService } from '@/services/dialogService' import { useDialogService } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore' import { useDialogStore } from '@/stores/dialogStore'
import { useWorkflowStore } from '../../management/stores/workflowStore' import { useWorkflowStore } from '../../management/stores/workflowStore'
@@ -13,6 +15,7 @@ export function useShareDialog() {
const dialogStore = useDialogStore() const dialogStore = useDialogStore()
const { pruneLinearData } = useAppModeStore() const { pruneLinearData } = useAppModeStore()
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const { isAppMode } = useAppMode()
function hide() { function hide() {
dialogStore.closeDialog({ key: DIALOG_KEY }) dialogStore.closeDialog({ key: DIALOG_KEY })
@@ -51,7 +54,15 @@ export function useShareDialog() {
share() share()
} }
function getShareSource() {
return isAppMode.value ? 'app_mode' : ('graph_mode' as const)
}
function showShareDialog() { function showShareDialog() {
useTelemetry()?.trackShareFlow({
step: 'dialog_opened',
source: getShareSource()
})
dialogService.showLayoutDialog({ dialogService.showLayoutDialog({
key: DIALOG_KEY, key: DIALOG_KEY,
component: ShareWorkflowDialogContent, component: ShareWorkflowDialogContent,

View File

@@ -164,7 +164,8 @@ describe('useSharedWorkflowUrlLoader', () => {
{ nodes: [] }, { nodes: [] },
true, true,
true, true,
'Test Workflow' 'Test Workflow',
{ openSource: 'shared_url' }
) )
expect(mockRouterReplace).toHaveBeenCalledWith({ query: {} }) expect(mockRouterReplace).toHaveBeenCalledWith({ query: {} })
expect(preservedQueryMocks.clearPreservedQuery).toHaveBeenCalledWith( expect(preservedQueryMocks.clearPreservedQuery).toHaveBeenCalledWith(
@@ -360,7 +361,8 @@ describe('useSharedWorkflowUrlLoader', () => {
expect.anything(), expect.anything(),
true, true,
true, true,
'Open shared workflow' 'Open shared workflow',
{ openSource: 'shared_url' }
) )
}) })
}) })

View File

@@ -138,7 +138,9 @@ export function useSharedWorkflowUrlLoader() {
const nonOwnedAssets = payload.assets.filter((a) => !a.in_library) const nonOwnedAssets = payload.assets.filter((a) => !a.in_library)
try { try {
await app.loadGraphData(payload.workflowJson, true, true, workflowName) await app.loadGraphData(payload.workflowJson, true, true, workflowName, {
openSource: 'shared_url'
})
} catch (error) { } catch (error) {
console.error( console.error(
'[useSharedWorkflowUrlLoader] Failed to load workflow graph:', '[useSharedWorkflowUrlLoader] Failed to load workflow graph:',

View File

@@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
import { clearPreservedQuery } from '@/platform/navigation/preservedQueryManager' import { clearPreservedQuery } from '@/platform/navigation/preservedQueryManager'
import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces' import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'
import { useTelemetry } from '@/platform/telemetry'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useTemplateWorkflows } from './useTemplateWorkflows' import { useTemplateWorkflows } from './useTemplateWorkflows'
@@ -121,6 +122,7 @@ export function useTemplateUrlLoader() {
}) })
} else if (modeParam === 'linear') { } else if (modeParam === 'linear') {
// Set linear mode after successful template load // Set linear mode after successful template load
useTelemetry()?.trackEnterLinear({ source: 'template_url' })
canvasStore.linearMode = true canvasStore.linearMode = true
} }
} catch (error) { } catch (error) {