[feat] Add comprehensive telemetry instrumentation for user behavior tracking

Implements unified telemetry service with fail-safe error handling:
- Template browsing and usage tracking
- Workflow creation method comparison
- Node addition source tracking
- UI interaction events (menus, sidebar, focus mode)
- Queue management operations
- Settings preference changes
- Advanced gesture/shortcut tracking

Features environment-variable sampling control and dual Electron/cloud support.
All telemetry functions are completely fail-safe and will never break normal app functionality.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
bymyself
2025-09-18 21:10:47 -07:00
parent 5c0eef8d3f
commit 8417acd75c
14 changed files with 608 additions and 6 deletions

View File

@@ -46,6 +46,7 @@ import { useDialogService } from '@/services/dialogService'
import { useExtensionService } from '@/services/extensionService'
import { useLitegraphService } from '@/services/litegraphService'
import { useSubgraphService } from '@/services/subgraphService'
import { TelemetryEvents, trackTypedEvent } from '@/services/telemetryService'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import { useCommandStore } from '@/stores/commandStore'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
@@ -545,7 +546,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], 'drag_drop')
} else {
// Try loading the first URI in the transfer list
const validTypes = ['text/uri-list', 'text/x-moz-url']
@@ -556,7 +557,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 }),
'drag_drop'
)
}
}
}
@@ -1450,7 +1454,10 @@ export class ComfyApp {
* Loads workflow data from the specified file
* @param {File} file
*/
async handleFile(file: File) {
async handleFile(
file: File,
source: 'drag_drop' | 'file_dialog' = 'file_dialog'
) {
const removeExt = (f: string) => {
if (!f) return f
const p = f.lastIndexOf('.')
@@ -1458,9 +1465,51 @@ export class ComfyApp {
return f.substring(0, p)
}
const fileName = removeExt(file.name)
// Track workflow opening based on file type and source
// Completely fail-safe - will never throw errors or break app flow
const trackWorkflowOpening = (fileType: string, hasWorkflow: boolean) => {
try {
if (!hasWorkflow) return
if (fileType.startsWith('image/')) {
trackTypedEvent(
TelemetryEvents.WORKFLOW_OPENED_FROM_DRAG_DROP_IMAGE,
{
file_type: fileType,
source_method: source,
file_name: fileName
}
)
} else if (
fileType === 'application/json' ||
fileName.endsWith('.json')
) {
trackTypedEvent(TelemetryEvents.WORKFLOW_OPENED_FROM_DRAG_DROP_JSON, {
file_type: fileType,
source_method: source,
file_name: fileName
})
} else {
// Other file types (audio, video, etc.)
trackTypedEvent(TelemetryEvents.WORKFLOW_OPENED_FROM_FILE_DIALOG, {
file_type: fileType,
source_method: source,
file_name: fileName
})
}
} catch (error) {
// Absolutely silent failure - telemetry must never break file loading
if (import.meta.env.DEV) {
console.warn('[Telemetry] trackWorkflowOpening failed:', error)
}
}
}
if (file.type === 'image/png') {
const pngInfo = await getPngMetadata(file)
if (pngInfo?.workflow) {
trackWorkflowOpening(file.type, true)
await this.loadGraphData(
JSON.parse(pngInfo.workflow),
true,
@@ -1468,10 +1517,12 @@ export class ComfyApp {
fileName
)
} else if (pngInfo?.prompt) {
trackWorkflowOpening(file.type, true)
this.loadApiJson(JSON.parse(pngInfo.prompt), fileName)
} else if (pngInfo?.parameters) {
// Note: Not putting this in `importA1111` as it is mostly not used
// by external callers, and `importA1111` has no access to `app`.
trackWorkflowOpening(file.type, true)
useWorkflowService().beforeLoadNewGraph()
importA1111(this.graph, pngInfo.parameters)
useWorkflowService().afterLoadNewGraph(
@@ -1485,8 +1536,10 @@ export class ComfyApp {
const { workflow, prompt } = await getAvifMetadata(file)
if (workflow) {
trackWorkflowOpening(file.type, true)
this.loadGraphData(JSON.parse(workflow), true, true, fileName)
} else if (prompt) {
trackWorkflowOpening(file.type, true)
this.loadApiJson(JSON.parse(prompt), fileName)
} else {
this.showErrorOnFileLoad(file)
@@ -1498,8 +1551,10 @@ export class ComfyApp {
const prompt = pngInfo?.prompt || pngInfo?.Prompt
if (workflow) {
trackWorkflowOpening(file.type, true)
this.loadGraphData(JSON.parse(workflow), true, true, fileName)
} else if (prompt) {
trackWorkflowOpening(file.type, true)
this.loadApiJson(JSON.parse(prompt), fileName)
} else {
this.showErrorOnFileLoad(file)
@@ -1507,8 +1562,10 @@ export class ComfyApp {
} else if (file.type === 'audio/mpeg') {
const { workflow, prompt } = await getMp3Metadata(file)
if (workflow) {
trackWorkflowOpening(file.type, true)
this.loadGraphData(workflow, true, true, fileName)
} else if (prompt) {
trackWorkflowOpening(file.type, true)
this.loadApiJson(prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
@@ -1516,8 +1573,10 @@ export class ComfyApp {
} else if (file.type === 'audio/ogg') {
const { workflow, prompt } = await getOggMetadata(file)
if (workflow) {
trackWorkflowOpening(file.type, true)
this.loadGraphData(workflow, true, true, fileName)
} else if (prompt) {
trackWorkflowOpening(file.type, true)
this.loadApiJson(prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
@@ -1528,8 +1587,10 @@ export class ComfyApp {
const prompt = pngInfo?.prompt || pngInfo?.Prompt
if (workflow) {
trackWorkflowOpening(file.type, true)
this.loadGraphData(JSON.parse(workflow), true, true, fileName)
} else if (prompt) {
trackWorkflowOpening(file.type, true)
this.loadApiJson(JSON.parse(prompt), fileName)
} else {
this.showErrorOnFileLoad(file)
@@ -1537,8 +1598,10 @@ export class ComfyApp {
} else if (file.type === 'video/webm') {
const webmInfo = await getFromWebmFile(file)
if (webmInfo.workflow) {
trackWorkflowOpening(file.type, true)
this.loadGraphData(webmInfo.workflow, true, true, fileName)
} else if (webmInfo.prompt) {
trackWorkflowOpening(file.type, true)
this.loadApiJson(webmInfo.prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
@@ -1553,15 +1616,19 @@ export class ComfyApp {
) {
const mp4Info = await getFromIsobmffFile(file)
if (mp4Info.workflow) {
trackWorkflowOpening(file.type, true)
this.loadGraphData(mp4Info.workflow, true, true, fileName)
} else if (mp4Info.prompt) {
trackWorkflowOpening(file.type, true)
this.loadApiJson(mp4Info.prompt, fileName)
}
} else if (file.type === 'image/svg+xml' || file.name?.endsWith('.svg')) {
const svgInfo = await getSvgMetadata(file)
if (svgInfo.workflow) {
trackWorkflowOpening(file.type, true)
this.loadGraphData(svgInfo.workflow, true, true, fileName)
} else if (svgInfo.prompt) {
trackWorkflowOpening(file.type, true)
this.loadApiJson(svgInfo.prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
@@ -1572,8 +1639,10 @@ export class ComfyApp {
) {
const gltfInfo = await getGltfBinaryMetadata(file)
if (gltfInfo.workflow) {
trackWorkflowOpening(file.type, true)
this.loadGraphData(gltfInfo.workflow, true, true, fileName)
} else if (gltfInfo.prompt) {
trackWorkflowOpening(file.type, true)
this.loadApiJson(gltfInfo.prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
@@ -1587,10 +1656,13 @@ export class ComfyApp {
const readerResult = reader.result as string
const jsonContent = JSON.parse(readerResult)
if (jsonContent?.templates) {
// Template data, not a workflow
this.loadTemplateData(jsonContent)
} else if (this.isApiJson(jsonContent)) {
trackWorkflowOpening(file.type, true)
this.loadApiJson(jsonContent, fileName)
} else {
trackWorkflowOpening(file.type, true)
await this.loadGraphData(
JSON.parse(readerResult),
true,
@@ -1608,6 +1680,7 @@ export class ComfyApp {
// TODO define schema to LatentMetadata
// @ts-expect-error
if (info.workflow) {
trackWorkflowOpening(file.type || 'application/octet-stream', true)
await this.loadGraphData(
// @ts-expect-error
JSON.parse(info.workflow),
@@ -1617,6 +1690,7 @@ export class ComfyApp {
)
// @ts-expect-error
} else if (info.prompt) {
trackWorkflowOpening(file.type || 'application/octet-stream', true)
// @ts-expect-error
this.loadApiJson(JSON.parse(info.prompt))
} else {