feat: send missing node data to ClickHouse (#10132)

## Summary
- When users open/import a workflow with missing nodes, we already track
this via Mixpanel
- This adds a parallel fire-and-forget POST to
`/api/internal/cloud_analytics` so the data also lands in **ClickHouse**
as `frontend:missing_nodes_detected` events
- Payload includes `missing_class_types[]`, `missing_count`, and
`source` (file_button/file_drop/template/unknown)

## Motivation
The frontend is where the **high-value** missing node signal lives —
most users see "missing nodes" and never submit. The backend only
catches the rare case where someone submits anyway. This change captures
both sides.

Companion cloud PR: https://github.com/Comfy-Org/cloud/pull/2886

## Changes
- `MixpanelTelemetryProvider.ts`: Added
`reportMissingNodesToClickHouse()` private method, called from
`trackWorkflowImported()` and `trackWorkflowOpened()`
- Only fires when `missing_node_count > 0`
- Fire-and-forget (`.catch(() => {})`) — no impact on user experience
- Uses existing `api.fetchApi()` which handles auth automatically

## Test plan
- [ ] Open a workflow with missing nodes → verify
`frontend:missing_nodes_detected` event appears in ClickHouse
- [ ] Open a workflow with no missing nodes → verify no event is sent
(check network tab)
- [ ] Verify Mixpanel tracking still works as before

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10132-feat-send-missing-node-data-to-ClickHouse-3266d73d365081559db5ed3efde33e95)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Deep Mehta
2026-03-23 15:39:04 -07:00
committed by GitHub
parent 657ae6a6c3
commit 2838edbb53
2 changed files with 50 additions and 2 deletions

View File

@@ -25,13 +25,15 @@ export async function initTelemetry(): Promise<void> {
{ MixpanelTelemetryProvider },
{ GtmTelemetryProvider },
{ ImpactTelemetryProvider },
{ PostHogTelemetryProvider }
{ PostHogTelemetryProvider },
{ ClickHouseTelemetryProvider }
] = await Promise.all([
import('./TelemetryRegistry'),
import('./providers/cloud/MixpanelTelemetryProvider'),
import('./providers/cloud/GtmTelemetryProvider'),
import('./providers/cloud/ImpactTelemetryProvider'),
import('./providers/cloud/PostHogTelemetryProvider')
import('./providers/cloud/PostHogTelemetryProvider'),
import('./providers/cloud/ClickHouseTelemetryProvider')
])
const registry = new TelemetryRegistry()
@@ -39,6 +41,7 @@ export async function initTelemetry(): Promise<void> {
registry.registerProvider(new GtmTelemetryProvider())
registry.registerProvider(new ImpactTelemetryProvider())
registry.registerProvider(new PostHogTelemetryProvider())
registry.registerProvider(new ClickHouseTelemetryProvider())
setTelemetryRegistry(registry)
})()

View File

@@ -0,0 +1,45 @@
import { api } from '@/scripts/api'
import type { TelemetryProvider, WorkflowImportMetadata } from '../../types'
/**
* ClickHouse Telemetry Provider - Cloud Build Implementation
*
* Sends observability events to the cloud backend's ClickHouse pipeline
* via POST /api/internal/cloud_analytics. Currently tracks missing node
* data when users open/import workflows with unsupported nodes.
*
* This provider is separate from Mixpanel because ClickHouse is the
* canonical store for post-hoc analytics (per observability philosophy).
*
* CRITICAL: OSS Build Safety
* This file is tree-shaken away in OSS builds (DISTRIBUTION unset).
*/
export class ClickHouseTelemetryProvider implements TelemetryProvider {
trackWorkflowImported(metadata: WorkflowImportMetadata): void {
this.reportMissingNodes(metadata)
}
trackWorkflowOpened(metadata: WorkflowImportMetadata): void {
this.reportMissingNodes(metadata)
}
private reportMissingNodes(metadata: WorkflowImportMetadata): void {
if (metadata.missing_node_count <= 0) return
api
.fetchApi('/internal/cloud_analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event_name: 'node_missing',
event_data: {
missing_class_types: metadata.missing_node_types,
missing_count: metadata.missing_node_count,
source: metadata.open_source ?? 'unknown'
}
})
})
.catch(() => {})
}
}