mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 18:22:40 +00:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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: () => {
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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' }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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:',
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user