diff --git a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts index 12f4eaa39..e0fd27955 100644 --- a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts +++ b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts @@ -244,6 +244,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { this.trackEvent(TelemetryEvents.WORKFLOW_IMPORTED, metadata) } + trackWorkflowOpened(metadata: WorkflowImportMetadata): void { + this.trackEvent(TelemetryEvents.WORKFLOW_OPENED, 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 21e0a83dc..1ede515c2 100644 --- a/src/platform/telemetry/types.ts +++ b/src/platform/telemetry/types.ts @@ -113,8 +113,22 @@ export interface CreditTopupMetadata { export interface WorkflowImportMetadata { missing_node_count: number missing_node_types: string[] + /** + * The source of the workflow open/import action + */ + open_source?: 'file_button' | 'file_drop' | 'template' | 'unknown' } +/** + * Workflow open metadata + */ +/** + * Enumerated sources for workflow open/import actions. + */ +export type WorkflowOpenSource = NonNullable< + WorkflowImportMetadata['open_source'] +> + /** * Template library metadata */ @@ -205,6 +219,7 @@ export interface TelemetryProvider { // Workflow management events trackWorkflowImported(metadata: WorkflowImportMetadata): void + trackWorkflowOpened(metadata: WorkflowImportMetadata): void // Page visibility events trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void @@ -262,6 +277,7 @@ export const TelemetryEvents = { // Workflow Management WORKFLOW_IMPORTED: 'app:workflow_imported', + WORKFLOW_OPENED: 'app:workflow_opened', // Page Visibility PAGE_VISIBILITY_CHANGED: 'app:page_visibility_changed', diff --git a/src/platform/workflow/templates/composables/useTemplateWorkflows.ts b/src/platform/workflow/templates/composables/useTemplateWorkflows.ts index f33582f80..d3bea7fb9 100644 --- a/src/platform/workflow/templates/composables/useTemplateWorkflows.ts +++ b/src/platform/workflow/templates/composables/useTemplateWorkflows.ts @@ -138,7 +138,9 @@ export function useTemplateWorkflows() { } dialogStore.closeDialog() - await app.loadGraphData(json, true, true, workflowName) + await app.loadGraphData(json, true, true, workflowName, { + openSource: 'template' + }) return true } @@ -159,7 +161,9 @@ export function useTemplateWorkflows() { } dialogStore.closeDialog() - await app.loadGraphData(json, true, true, workflowName) + await app.loadGraphData(json, true, true, workflowName, { + openSource: 'template' + }) return true } catch (error) { diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 634c08112..847e0dfab 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -19,6 +19,7 @@ import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' import { isCloud } from '@/platform/distribution/types' import { useSettingStore } from '@/platform/settings/settingStore' import { useTelemetry } from '@/platform/telemetry' +import type { WorkflowOpenSource } from '@/platform/telemetry/types' import { useToastStore } from '@/platform/updates/common/toastStore' import { useWorkflowService } from '@/platform/workflow/core/services/workflowService' import { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore' @@ -550,7 +551,7 @@ export class ComfyApp { event.dataTransfer.files.length && event.dataTransfer.files[0].type !== 'image/bmp' ) { - await this.handleFile(event.dataTransfer.files[0]) + await this.handleFile(event.dataTransfer.files[0], 'file_drop') } else { // Try loading the first URI in the transfer list const validTypes = ['text/uri-list', 'text/x-moz-url'] @@ -561,7 +562,10 @@ export class ComfyApp { const uri = event.dataTransfer.getData(match)?.split('\n')?.[0] if (uri) { const blob = await (await fetch(uri)).blob() - await this.handleFile(new File([blob], uri, { type: blob.type })) + await this.handleFile( + new File([blob], uri, { type: blob.type }), + 'file_drop' + ) } } } @@ -1040,12 +1044,19 @@ export class ComfyApp { clean: boolean = true, restore_view: boolean = true, workflow: string | null | ComfyWorkflow = null, - { - showMissingNodesDialog = true, - showMissingModelsDialog = true, - checkForRerouteMigration = false + options: { + showMissingNodesDialog?: boolean + showMissingModelsDialog?: boolean + checkForRerouteMigration?: boolean + openSource?: WorkflowOpenSource } = {} ) { + const { + showMissingNodesDialog = true, + showMissingModelsDialog = true, + checkForRerouteMigration = false, + openSource + } = options useWorkflowService().beforeLoadNewGraph() if (clean !== false) { @@ -1275,13 +1286,15 @@ export class ComfyApp { missingNodeTypes ) - // Track workflow import with missing node information - useTelemetry()?.trackWorkflowImported({ + const telemetryPayload = { missing_node_count: missingNodeTypes.length, missing_node_types: missingNodeTypes.map((node) => typeof node === 'string' ? node : node.type - ) - }) + ), + open_source: openSource ?? 'unknown' + } + useTelemetry()?.trackWorkflowOpened(telemetryPayload) + useTelemetry()?.trackWorkflowImported(telemetryPayload) await useWorkflowService().afterLoadNewGraph( workflow, this.graph.serialize() as unknown as ComfyWorkflowJSON @@ -1399,7 +1412,7 @@ export class ComfyApp { * Loads workflow data from the specified file * @param {File} file */ - async handleFile(file: File) { + async handleFile(file: File, openSource?: WorkflowOpenSource) { const removeExt = (f: string) => { if (!f) return f const p = f.lastIndexOf('.') @@ -1414,7 +1427,8 @@ export class ComfyApp { JSON.parse(pngInfo.workflow), true, true, - fileName + fileName, + { openSource } ) } else if (pngInfo?.prompt) { this.loadApiJson(JSON.parse(pngInfo.prompt), fileName) @@ -1434,7 +1448,9 @@ export class ComfyApp { const { workflow, prompt } = await getAvifMetadata(file) if (workflow) { - this.loadGraphData(JSON.parse(workflow), true, true, fileName) + this.loadGraphData(JSON.parse(workflow), true, true, fileName, { + openSource + }) } else if (prompt) { this.loadApiJson(JSON.parse(prompt), fileName) } else { @@ -1447,7 +1463,9 @@ export class ComfyApp { const prompt = pngInfo?.prompt || pngInfo?.Prompt if (workflow) { - this.loadGraphData(JSON.parse(workflow), true, true, fileName) + this.loadGraphData(JSON.parse(workflow), true, true, fileName, { + openSource + }) } else if (prompt) { this.loadApiJson(JSON.parse(prompt), fileName) } else { @@ -1456,7 +1474,7 @@ export class ComfyApp { } else if (file.type === 'audio/mpeg') { const { workflow, prompt } = await getMp3Metadata(file) if (workflow) { - this.loadGraphData(workflow, true, true, fileName) + this.loadGraphData(workflow, true, true, fileName, { openSource }) } else if (prompt) { this.loadApiJson(prompt, fileName) } else { @@ -1465,7 +1483,7 @@ export class ComfyApp { } else if (file.type === 'audio/ogg') { const { workflow, prompt } = await getOggMetadata(file) if (workflow) { - this.loadGraphData(workflow, true, true, fileName) + this.loadGraphData(workflow, true, true, fileName, { openSource }) } else if (prompt) { this.loadApiJson(prompt, fileName) } else { @@ -1477,7 +1495,9 @@ export class ComfyApp { const prompt = pngInfo?.prompt || pngInfo?.Prompt if (workflow) { - this.loadGraphData(JSON.parse(workflow), true, true, fileName) + this.loadGraphData(JSON.parse(workflow), true, true, fileName, { + openSource + }) } else if (prompt) { this.loadApiJson(JSON.parse(prompt), fileName) } else { @@ -1486,7 +1506,9 @@ export class ComfyApp { } else if (file.type === 'video/webm') { const webmInfo = await getFromWebmFile(file) if (webmInfo.workflow) { - this.loadGraphData(webmInfo.workflow, true, true, fileName) + this.loadGraphData(webmInfo.workflow, true, true, fileName, { + openSource + }) } else if (webmInfo.prompt) { this.loadApiJson(webmInfo.prompt, fileName) } else { @@ -1502,14 +1524,18 @@ export class ComfyApp { ) { const mp4Info = await getFromIsobmffFile(file) if (mp4Info.workflow) { - this.loadGraphData(mp4Info.workflow, true, true, fileName) + this.loadGraphData(mp4Info.workflow, true, true, fileName, { + openSource + }) } else if (mp4Info.prompt) { this.loadApiJson(mp4Info.prompt, fileName) } } else if (file.type === 'image/svg+xml' || file.name?.endsWith('.svg')) { const svgInfo = await getSvgMetadata(file) if (svgInfo.workflow) { - this.loadGraphData(svgInfo.workflow, true, true, fileName) + this.loadGraphData(svgInfo.workflow, true, true, fileName, { + openSource + }) } else if (svgInfo.prompt) { this.loadApiJson(svgInfo.prompt, fileName) } else { @@ -1521,7 +1547,9 @@ export class ComfyApp { ) { const gltfInfo = await getGltfBinaryMetadata(file) if (gltfInfo.workflow) { - this.loadGraphData(gltfInfo.workflow, true, true, fileName) + this.loadGraphData(gltfInfo.workflow, true, true, fileName, { + openSource + }) } else if (gltfInfo.prompt) { this.loadApiJson(gltfInfo.prompt, fileName) } else { @@ -1544,7 +1572,8 @@ export class ComfyApp { JSON.parse(readerResult), true, true, - fileName + fileName, + { openSource } ) } } @@ -1562,7 +1591,8 @@ export class ComfyApp { JSON.parse(info.workflow), true, true, - fileName + fileName, + { openSource } ) // @ts-expect-error } else if (info.prompt) { diff --git a/src/scripts/ui.ts b/src/scripts/ui.ts index 860e45caf..d3300a3ab 100644 --- a/src/scripts/ui.ts +++ b/src/scripts/ui.ts @@ -392,7 +392,7 @@ export class ComfyUI { parent: document.body, onchange: async () => { // @ts-expect-error fixme ts strict error - await app.handleFile(fileInput.files[0]) + await app.handleFile(fileInput.files[0], 'file_button') fileInput.value = '' } })