From e89a0f96cd97f901b02785a8d7029f6ab312ea52 Mon Sep 17 00:00:00 2001 From: Robin Huang Date: Tue, 10 Mar 2026 15:05:19 -0700 Subject: [PATCH] feat: track app mode entry and shared workflow loading (#9720) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- .env_example | 3 +++ .../builder/useAppSetDefaultView.ts | 2 ++ src/platform/telemetry/TelemetryRegistry.ts | 5 ++++ .../cloud/MixpanelTelemetryProvider.ts | 5 ++++ .../cloud/PostHogTelemetryProvider.test.ts | 3 ++- .../cloud/PostHogTelemetryProvider.ts | 8 ++++++- src/platform/telemetry/types.ts | 23 +++++++++++++++++-- .../sharing/components/ShareUrlCopyField.vue | 7 ++++++ .../components/ShareWorkflowDialogContent.vue | 15 ++++++++++++ .../sharing/composables/useShareDialog.ts | 11 +++++++++ .../useSharedWorkflowUrlLoader.test.ts | 6 +++-- .../composables/useSharedWorkflowUrlLoader.ts | 4 +++- .../composables/useTemplateUrlLoader.ts | 2 ++ 13 files changed, 87 insertions(+), 7 deletions(-) diff --git a/.env_example b/.env_example index c58a864e2a..a2a9666615 100644 --- a/.env_example +++ b/.env_example @@ -38,6 +38,9 @@ TEST_COMFYUI_DIR=/home/ComfyUI ALGOLIA_APP_ID=4E0RO38HS8 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_AUTH_TOKEN=private-token # get from sentry # SENTRY_ORG=comfy-org diff --git a/src/components/builder/useAppSetDefaultView.ts b/src/components/builder/useAppSetDefaultView.ts index df2635cbbc..ccee38cdaf 100644 --- a/src/components/builder/useAppSetDefaultView.ts +++ b/src/components/builder/useAppSetDefaultView.ts @@ -1,6 +1,7 @@ import { computed } from 'vue' import { useAppMode } from '@/composables/useAppMode' +import { useTelemetry } from '@/platform/telemetry' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { app } from '@/scripts/app' import { useDialogService } from '@/services/dialogService' @@ -54,6 +55,7 @@ export function useAppSetDefaultView() { appliedAsApp, onViewApp: () => { closeAppliedDialog() + useTelemetry()?.trackEnterLinear({ source: 'app_builder' }) setMode('app') }, onExitToWorkflow: () => { diff --git a/src/platform/telemetry/TelemetryRegistry.ts b/src/platform/telemetry/TelemetryRegistry.ts index 786f061a71..a299f563a2 100644 --- a/src/platform/telemetry/TelemetryRegistry.ts +++ b/src/platform/telemetry/TelemetryRegistry.ts @@ -4,6 +4,7 @@ import type { AuthMetadata, BeginCheckoutMetadata, EnterLinearMetadata, + ShareFlowMetadata, ExecutionErrorMetadata, ExecutionSuccessMetadata, ExecutionTriggerSource, @@ -160,6 +161,10 @@ export class TelemetryRegistry implements TelemetryDispatcher { this.dispatch((provider) => provider.trackEnterLinear?.(metadata)) } + trackShareFlow(metadata: ShareFlowMetadata): void { + this.dispatch((provider) => provider.trackShareFlow?.(metadata)) + } + trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void { this.dispatch((provider) => provider.trackPageVisibilityChanged?.(metadata)) } diff --git a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts index 30043522cd..6894a7e62c 100644 --- a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts +++ b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts @@ -15,6 +15,7 @@ import type { AuthMetadata, CreditTopupMetadata, EnterLinearMetadata, + ShareFlowMetadata, ExecutionContext, ExecutionTriggerSource, ExecutionErrorMetadata, @@ -362,6 +363,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { this.trackEvent(TelemetryEvents.ENTER_LINEAR_MODE, metadata) } + trackShareFlow(metadata: ShareFlowMetadata): void { + this.trackEvent(TelemetryEvents.SHARE_FLOW, metadata) + } + trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void { this.trackEvent(TelemetryEvents.PAGE_VISIBILITY_CHANGED, metadata) } diff --git a/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts b/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts index f36b7f5ba6..b18dcfb7ec 100644 --- a/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts +++ b/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts @@ -85,7 +85,8 @@ describe('PostHogTelemetryProvider', () => { autocapture: false, capture_pageview: false, capture_pageleave: false, - persistence: 'localStorage+cookie' + persistence: 'localStorage+cookie', + debug: false }) }) diff --git a/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts b/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts index aba47173e8..95ec9582a6 100644 --- a/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts +++ b/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts @@ -8,6 +8,7 @@ import type { RemoteConfig } from '@/platform/remoteConfig/types' import type { AuthMetadata, EnterLinearMetadata, + ShareFlowMetadata, ExecutionContext, ExecutionErrorMetadata, ExecutionSuccessMetadata, @@ -104,7 +105,8 @@ export class PostHogTelemetryProvider implements TelemetryProvider { autocapture: false, capture_pageview: false, capture_pageleave: false, - persistence: 'localStorage+cookie' + persistence: 'localStorage+cookie', + debug: import.meta.env.VITE_POSTHOG_DEBUG === 'true' }) this.isInitialized = true this.flushEventQueue() @@ -346,6 +348,10 @@ export class PostHogTelemetryProvider implements TelemetryProvider { this.trackEvent(TelemetryEvents.ENTER_LINEAR_MODE, metadata) } + trackShareFlow(metadata: ShareFlowMetadata): void { + this.trackEvent(TelemetryEvents.SHARE_FLOW, metadata) + } + trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void { this.trackEvent(TelemetryEvents.PAGE_VISIBILITY_CHANGED, metadata) } diff --git a/src/platform/telemetry/types.ts b/src/platform/telemetry/types.ts index 4e97a115c0..347d71414d 100644 --- a/src/platform/telemetry/types.ts +++ b/src/platform/telemetry/types.ts @@ -137,13 +137,29 @@ export interface WorkflowImportMetadata { /** * 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 { 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 */ @@ -362,6 +378,7 @@ export interface TelemetryProvider { trackWorkflowImported?(metadata: WorkflowImportMetadata): void trackWorkflowOpened?(metadata: WorkflowImportMetadata): void trackEnterLinear?(metadata: EnterLinearMetadata): void + trackShareFlow?(metadata: ShareFlowMetadata): void // Page visibility events trackPageVisibilityChanged?(metadata: PageVisibilityMetadata): void @@ -447,7 +464,8 @@ export const TelemetryEvents = { // Workflow Management WORKFLOW_IMPORTED: 'app:workflow_imported', 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_CHANGED: 'app:page_visibility_changed', @@ -521,4 +539,5 @@ export type TelemetryEventProperties = | HelpCenterClosedMetadata | WorkflowCreatedMetadata | EnterLinearMetadata + | ShareFlowMetadata | SubscriptionMetadata diff --git a/src/platform/workflow/sharing/components/ShareUrlCopyField.vue b/src/platform/workflow/sharing/components/ShareUrlCopyField.vue index 87b4d9a48f..c61ecb3698 100644 --- a/src/platform/workflow/sharing/components/ShareUrlCopyField.vue +++ b/src/platform/workflow/sharing/components/ShareUrlCopyField.vue @@ -26,17 +26,24 @@ import { refAutoReset } from '@vueuse/core' import Button from '@/components/ui/button/Button.vue' import Input from '@/components/ui/input/Input.vue' +import { useAppMode } from '@/composables/useAppMode' import { useCopyToClipboard } from '@/composables/useCopyToClipboard' +import { useTelemetry } from '@/platform/telemetry' const { url } = defineProps<{ url: string }>() const { copyToClipboard } = useCopyToClipboard() +const { isAppMode } = useAppMode() const copied = refAutoReset(false, 2000) async function handleCopy() { await copyToClipboard(url) copied.value = true + useTelemetry()?.trackShareFlow({ + step: 'link_copied', + source: isAppMode.value ? 'app_mode' : 'graph_mode' + }) } diff --git a/src/platform/workflow/sharing/components/ShareWorkflowDialogContent.vue b/src/platform/workflow/sharing/components/ShareWorkflowDialogContent.vue index e1454a58f6..95061f5285 100644 --- a/src/platform/workflow/sharing/components/ShareWorkflowDialogContent.vue +++ b/src/platform/workflow/sharing/components/ShareWorkflowDialogContent.vue @@ -167,7 +167,9 @@ import type { import { useWorkflowShareService } from '@/platform/workflow/sharing/services/workflowShareService' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { useWorkflowService } from '@/platform/workflow/core/services/workflowService' +import { useAppMode } from '@/composables/useAppMode' import { useFeatureFlags } from '@/composables/useFeatureFlags' +import { useTelemetry } from '@/platform/telemetry' import { appendJsonExt } from '@/utils/formatUtil' import { cn } from '@/utils/tailwindUtil' @@ -182,6 +184,11 @@ const publishDialog = useComfyHubPublishDialog() const shareService = useWorkflowShareService() const workflowStore = useWorkflowStore() 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 DialogMode = 'shareLink' | 'publishToHub' @@ -298,6 +305,10 @@ async function refreshDialogState() { if (!workflow || workflow.isTemporary || workflow.isModified) { dialogState.value = 'unsaved' + useTelemetry()?.trackShareFlow({ + step: 'save_prompted', + source: getShareSource() + }) if (workflow) { workflowName.value = stripJsonExtension(workflow.filename) } @@ -379,6 +390,10 @@ const { ) dialogState.value = 'shared' acknowledged.value = false + useTelemetry()?.trackShareFlow({ + step: 'link_created', + source: getShareSource() + }) return result }, diff --git a/src/platform/workflow/sharing/composables/useShareDialog.ts b/src/platform/workflow/sharing/composables/useShareDialog.ts index 7699b93fa9..358a33d014 100644 --- a/src/platform/workflow/sharing/composables/useShareDialog.ts +++ b/src/platform/workflow/sharing/composables/useShareDialog.ts @@ -1,4 +1,6 @@ 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 { useDialogStore } from '@/stores/dialogStore' import { useWorkflowStore } from '../../management/stores/workflowStore' @@ -13,6 +15,7 @@ export function useShareDialog() { const dialogStore = useDialogStore() const { pruneLinearData } = useAppModeStore() const workflowStore = useWorkflowStore() + const { isAppMode } = useAppMode() function hide() { dialogStore.closeDialog({ key: DIALOG_KEY }) @@ -51,7 +54,15 @@ export function useShareDialog() { share() } + function getShareSource() { + return isAppMode.value ? 'app_mode' : ('graph_mode' as const) + } + function showShareDialog() { + useTelemetry()?.trackShareFlow({ + step: 'dialog_opened', + source: getShareSource() + }) dialogService.showLayoutDialog({ key: DIALOG_KEY, component: ShareWorkflowDialogContent, diff --git a/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.test.ts b/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.test.ts index fffddce32c..fd42ae7a59 100644 --- a/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.test.ts +++ b/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.test.ts @@ -164,7 +164,8 @@ describe('useSharedWorkflowUrlLoader', () => { { nodes: [] }, true, true, - 'Test Workflow' + 'Test Workflow', + { openSource: 'shared_url' } ) expect(mockRouterReplace).toHaveBeenCalledWith({ query: {} }) expect(preservedQueryMocks.clearPreservedQuery).toHaveBeenCalledWith( @@ -360,7 +361,8 @@ describe('useSharedWorkflowUrlLoader', () => { expect.anything(), true, true, - 'Open shared workflow' + 'Open shared workflow', + { openSource: 'shared_url' } ) }) }) diff --git a/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.ts b/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.ts index 0abca9ad05..b75601f8ff 100644 --- a/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.ts +++ b/src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.ts @@ -138,7 +138,9 @@ export function useSharedWorkflowUrlLoader() { const nonOwnedAssets = payload.assets.filter((a) => !a.in_library) try { - await app.loadGraphData(payload.workflowJson, true, true, workflowName) + await app.loadGraphData(payload.workflowJson, true, true, workflowName, { + openSource: 'shared_url' + }) } catch (error) { console.error( '[useSharedWorkflowUrlLoader] Failed to load workflow graph:', diff --git a/src/platform/workflow/templates/composables/useTemplateUrlLoader.ts b/src/platform/workflow/templates/composables/useTemplateUrlLoader.ts index 05a5331620..34b1a5a8ec 100644 --- a/src/platform/workflow/templates/composables/useTemplateUrlLoader.ts +++ b/src/platform/workflow/templates/composables/useTemplateUrlLoader.ts @@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router' import { clearPreservedQuery } from '@/platform/navigation/preservedQueryManager' import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces' +import { useTelemetry } from '@/platform/telemetry' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useTemplateWorkflows } from './useTemplateWorkflows' @@ -121,6 +122,7 @@ export function useTemplateUrlLoader() { }) } else if (modeParam === 'linear') { // Set linear mode after successful template load + useTelemetry()?.trackEnterLinear({ source: 'template_url' }) canvasStore.linearMode = true } } catch (error) {