diff --git a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts index c06f9fdcb..4da27c807 100644 --- a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts +++ b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts @@ -371,6 +371,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 ed5c28ecc..57cfa5a57 100644 --- a/src/platform/telemetry/types.ts +++ b/src/platform/telemetry/types.ts @@ -95,8 +95,22 @@ export interface TemplateMetadata { 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 */ @@ -181,6 +195,7 @@ export interface TelemetryProvider { // Workflow management events trackWorkflowImported(metadata: WorkflowImportMetadata): void + trackWorkflowOpened(metadata: WorkflowImportMetadata): void // Page visibility events trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void @@ -233,6 +248,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 edabfb56a..28e7a2823 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' @@ -619,7 +620,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'] @@ -630,7 +631,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' + ) } } } @@ -1126,12 +1130,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) { @@ -1361,6 +1372,15 @@ export class ComfyApp { 'afterConfigureGraph', missingNodeTypes ) + 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 @@ -1478,7 +1498,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('.') @@ -1493,7 +1513,8 @@ export class ComfyApp { JSON.parse(pngInfo.workflow), true, true, - fileName + fileName, + { openSource } ) } else if (pngInfo?.prompt) { this.loadApiJson(JSON.parse(pngInfo.prompt), fileName) @@ -1513,7 +1534,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 { @@ -1526,7 +1549,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 { @@ -1535,7 +1560,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 { @@ -1544,7 +1569,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 { @@ -1556,7 +1581,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 { @@ -1565,7 +1592,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 { @@ -1581,14 +1610,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 { @@ -1600,7 +1633,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 { @@ -1623,7 +1658,8 @@ export class ComfyApp { JSON.parse(readerResult), true, true, - fileName + fileName, + { openSource } ) } } @@ -1641,7 +1677,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 e72f3662c..0c2b3534c 100644 --- a/src/scripts/ui.ts +++ b/src/scripts/ui.ts @@ -398,7 +398,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 = '' } })