Skip to content

Commit 1322a56

Browse files
Cloud/tracking v2 (#6400)
## Summary <!-- One sentence describing what changed and why. --> ## Changes - **What**: <!-- Core functionality added/modified --> - **Breaking**: <!-- Any breaking changes (if none, remove this line) --> - **Dependencies**: <!-- New dependencies (if none, remove this line) --> ## Review Focus <!-- Critical design decisions or edge cases that need attention --> <!-- If this PR fixes an issue, uncomment and update the line below --> <!-- Fixes #ISSUE_NUMBER --> ## Screenshots (if applicable) <!-- Add screenshots or video recording to help explain your changes --> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6400-Cloud-tracking-v2-29c6d73d365081a1ae32e9337f510a9e) by [Unito](https://www.unito.io) --------- Co-authored-by: Arjan Singh <[email protected]>
1 parent 6491db6 commit 1322a56

File tree

13 files changed

+1698
-8
lines changed

13 files changed

+1698
-8
lines changed

src/components/dialog/content/credit/CreditTopUpOption.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@ import Tag from 'primevue/tag'
4343
import { onBeforeUnmount, ref } from 'vue'
4444
4545
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
46+
import { useTelemetry } from '@/platform/telemetry'
4647
4748
const authActions = useFirebaseAuthActions()
49+
const telemetry = useTelemetry()
4850
4951
const {
5052
amount,
@@ -61,8 +63,11 @@ const didClickBuyNow = ref(false)
6163
const loading = ref(false)
6264
6365
const handleBuyNow = async () => {
66+
const creditAmount = editable ? customAmount.value : amount
67+
telemetry?.trackApiCreditTopupButtonPurchaseClicked(creditAmount)
68+
6469
loading.value = true
65-
await authActions.purchaseCredits(editable ? customAmount.value : amount)
70+
await authActions.purchaseCredits(creditAmount)
6671
loading.value = false
6772
didClickBuyNow.value = true
6873
}

src/components/searchbox/NodeSearchBox.vue

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
multiple
5252
:option-label="'display_name'"
5353
@complete="search($event.query)"
54-
@option-select="emit('addNode', $event.value)"
54+
@option-select="onAddNode($event.value)"
5555
@focused-option-changed="setHoverSuggestion($event)"
5656
>
5757
<template #option="{ option }">
@@ -78,6 +78,7 @@
7878
</template>
7979

8080
<script setup lang="ts">
81+
import { debounce } from 'es-toolkit/compat'
8182
import Button from 'primevue/button'
8283
import Dialog from 'primevue/dialog'
8384
import { computed, nextTick, onMounted, ref } from 'vue'
@@ -88,6 +89,7 @@ import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue
8889
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
8990
import NodeSearchItem from '@/components/searchbox/NodeSearchItem.vue'
9091
import { useSettingStore } from '@/platform/settings/settingStore'
92+
import { useTelemetry } from '@/platform/telemetry'
9193
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
9294
import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
9395
import type { FuseFilterWithValue } from '@/utils/fuseUtil'
@@ -96,6 +98,7 @@ import SearchFilterChip from '../common/SearchFilterChip.vue'
9698
9799
const settingStore = useSettingStore()
98100
const { t } = useI18n()
101+
const telemetry = useTelemetry()
99102
100103
const enableNodePreview = computed(() =>
101104
settingStore.get('Comfy.NodeSearchBoxImpl.NodePreview')
@@ -118,6 +121,14 @@ const placeholder = computed(() => {
118121
119122
const nodeDefStore = useNodeDefStore()
120123
const nodeFrequencyStore = useNodeFrequencyStore()
124+
125+
// Debounced search tracking (500ms as per implementation plan)
126+
const debouncedTrackSearch = debounce((query: string) => {
127+
if (query.trim()) {
128+
telemetry?.trackNodeSearch({ query })
129+
}
130+
}, 500)
131+
121132
const search = (query: string) => {
122133
const queryIsEmpty = query === '' && filters.length === 0
123134
currentQuery.value = query
@@ -128,10 +139,22 @@ const search = (query: string) => {
128139
limit: searchLimit
129140
})
130141
]
142+
143+
// Track search queries with debounce
144+
debouncedTrackSearch(query)
131145
}
132146
133147
const emit = defineEmits(['addFilter', 'removeFilter', 'addNode'])
134148
149+
// Track node selection and emit addNode event
150+
const onAddNode = (nodeDef: ComfyNodeDefImpl) => {
151+
telemetry?.trackNodeSearchResultSelected({
152+
node_type: nodeDef.name,
153+
last_query: currentQuery.value
154+
})
155+
emit('addNode', nodeDef)
156+
}
157+
135158
let inputElement: HTMLInputElement | null = null
136159
const reFocusInput = async () => {
137160
inputElement ??= document.getElementById(inputId) as HTMLInputElement

src/composables/useTemplateFiltering.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { refDebounced } from '@vueuse/core'
22
import Fuse from 'fuse.js'
3-
import { computed, ref } from 'vue'
3+
import { computed, ref, watch } from 'vue'
44
import type { Ref } from 'vue'
55

6+
import { useTelemetry } from '@/platform/telemetry'
67
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
8+
import { debounce } from 'es-toolkit/compat'
79

810
export function useTemplateFiltering(
911
templates: Ref<TemplateInfo[]> | TemplateInfo[]
@@ -212,6 +214,38 @@ export function useTemplateFiltering(
212214
const filteredCount = computed(() => filteredTemplates.value.length)
213215
const totalCount = computed(() => templatesArray.value.length)
214216

217+
// Template filter tracking (debounced to avoid excessive events)
218+
const debouncedTrackFilterChange = debounce(() => {
219+
useTelemetry()?.trackTemplateFilterChanged({
220+
search_query: searchQuery.value || undefined,
221+
selected_models: selectedModels.value,
222+
selected_use_cases: selectedUseCases.value,
223+
selected_licenses: selectedLicenses.value,
224+
sort_by: sortBy.value,
225+
filtered_count: filteredCount.value,
226+
total_count: totalCount.value
227+
})
228+
}, 500)
229+
230+
// Watch for filter changes and track them
231+
watch(
232+
[searchQuery, selectedModels, selectedUseCases, selectedLicenses, sortBy],
233+
() => {
234+
// Only track if at least one filter is active (to avoid tracking initial state)
235+
const hasActiveFilters =
236+
searchQuery.value.trim() !== '' ||
237+
selectedModels.value.length > 0 ||
238+
selectedUseCases.value.length > 0 ||
239+
selectedLicenses.value.length > 0 ||
240+
sortBy.value !== 'default'
241+
242+
if (hasActiveFilters) {
243+
debouncedTrackFilterChange()
244+
}
245+
},
246+
{ deep: true }
247+
)
248+
215249
return {
216250
// State
217251
searchQuery,

src/composables/useWorkflowTemplateSelectorDialog.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import WorkflowTemplateSelectorDialog from '@/components/custom/widget/WorkflowTemplateSelectorDialog.vue'
2+
import { useTelemetry } from '@/platform/telemetry'
23
import { useDialogService } from '@/services/dialogService'
34
import { useDialogStore } from '@/stores/dialogStore'
45

@@ -13,6 +14,8 @@ export const useWorkflowTemplateSelectorDialog = () => {
1314
}
1415

1516
function show() {
17+
useTelemetry()?.trackTemplateLibraryOpened({ source: 'command' })
18+
1619
dialogService.showLayoutDialog({
1720
key: DIALOG_KEY,
1821
component: WorkflowTemplateSelectorDialog,

src/platform/cloud/subscription/components/SubscribeButton.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const emit = defineEmits<{
3737
}>()
3838
3939
const { subscribe, isActiveSubscription, fetchStatus } = useSubscription()
40+
const telemetry = useTelemetry()
4041
4142
const isLoading = ref(false)
4243
const isPolling = ref(false)
@@ -62,6 +63,7 @@ const startPollingSubscriptionStatus = () => {
6263
6364
if (isActiveSubscription.value) {
6465
stopPolling()
66+
telemetry?.trackMonthlySubscriptionSucceeded()
6567
emit('subscribed')
6668
}
6769
} catch (error) {

src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,31 @@ import type { OverridedMixpanel } from 'mixpanel-browser'
33
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
44
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
55
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
6+
import { app } from '@/scripts/app'
7+
import { useNodeDefStore } from '@/stores/nodeDefStore'
8+
import { NodeSourceType } from '@/types/nodeSource'
9+
import { reduceAllNodes } from '@/utils/graphTraversalUtil'
10+
import { normalizeSurveyResponses } from '../../utils/surveyNormalization'
611

712
import type {
813
AuthMetadata,
14+
CreditTopupMetadata,
915
ExecutionContext,
1016
ExecutionErrorMetadata,
1117
ExecutionSuccessMetadata,
18+
NodeSearchMetadata,
19+
NodeSearchResultMetadata,
20+
PageVisibilityMetadata,
1221
RunButtonProperties,
1322
SurveyResponses,
23+
TabCountMetadata,
1424
TelemetryEventName,
1525
TelemetryEventProperties,
1626
TelemetryProvider,
17-
TemplateMetadata
27+
TemplateFilterMetadata,
28+
TemplateLibraryMetadata,
29+
TemplateMetadata,
30+
WorkflowImportMetadata
1831
} from '../../types'
1932
import { TelemetryEvents } from '../../types'
2033

@@ -123,6 +136,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
123136
this.trackEvent(TelemetryEvents.USER_AUTH_COMPLETED, metadata)
124137
}
125138

139+
trackUserLoggedIn(): void {
140+
this.trackEvent(TelemetryEvents.USER_LOGGED_IN)
141+
}
142+
126143
trackSubscription(event: 'modal_opened' | 'subscribe_clicked'): void {
127144
const eventName =
128145
event === 'modal_opened'
@@ -132,6 +149,20 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
132149
this.trackEvent(eventName)
133150
}
134151

152+
trackMonthlySubscriptionSucceeded(): void {
153+
this.trackEvent(TelemetryEvents.MONTHLY_SUBSCRIPTION_SUCCEEDED)
154+
}
155+
156+
trackApiCreditTopupButtonPurchaseClicked(amount: number): void {
157+
const metadata: CreditTopupMetadata = {
158+
credit_amount: amount
159+
}
160+
this.trackEvent(
161+
TelemetryEvents.API_CREDIT_TOPUP_BUTTON_PURCHASE_CLICKED,
162+
metadata
163+
)
164+
}
165+
135166
trackRunButton(options?: { subscribe_to_run?: boolean }): void {
136167
const executionContext = this.getExecutionContext()
137168

@@ -153,7 +184,21 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
153184
? TelemetryEvents.USER_SURVEY_OPENED
154185
: TelemetryEvents.USER_SURVEY_SUBMITTED
155186

156-
this.trackEvent(eventName, responses)
187+
// Apply normalization to survey responses
188+
const normalizedResponses = responses
189+
? normalizeSurveyResponses(responses)
190+
: undefined
191+
192+
this.trackEvent(eventName, normalizedResponses)
193+
194+
// If this is a survey submission, also set user properties with normalized data
195+
if (stage === 'submitted' && normalizedResponses && this.mixpanel) {
196+
try {
197+
this.mixpanel.people.set(normalizedResponses)
198+
} catch (error) {
199+
console.error('Failed to set survey user properties:', error)
200+
}
201+
}
157202
}
158203

159204
trackEmailVerification(stage: 'opened' | 'requested' | 'completed'): void {
@@ -178,6 +223,34 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
178223
this.trackEvent(TelemetryEvents.TEMPLATE_WORKFLOW_OPENED, metadata)
179224
}
180225

226+
trackTemplateLibraryOpened(metadata: TemplateLibraryMetadata): void {
227+
this.trackEvent(TelemetryEvents.TEMPLATE_LIBRARY_OPENED, metadata)
228+
}
229+
230+
trackWorkflowImported(metadata: WorkflowImportMetadata): void {
231+
this.trackEvent(TelemetryEvents.WORKFLOW_IMPORTED, metadata)
232+
}
233+
234+
trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void {
235+
this.trackEvent(TelemetryEvents.PAGE_VISIBILITY_CHANGED, metadata)
236+
}
237+
238+
trackTabCount(metadata: TabCountMetadata): void {
239+
this.trackEvent(TelemetryEvents.TAB_COUNT_TRACKING, metadata)
240+
}
241+
242+
trackNodeSearch(metadata: NodeSearchMetadata): void {
243+
this.trackEvent(TelemetryEvents.NODE_SEARCH, metadata)
244+
}
245+
246+
trackNodeSearchResultSelected(metadata: NodeSearchResultMetadata): void {
247+
this.trackEvent(TelemetryEvents.NODE_SEARCH_RESULT_SELECTED, metadata)
248+
}
249+
250+
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void {
251+
this.trackEvent(TelemetryEvents.TEMPLATE_FILTER_CHANGED, metadata)
252+
}
253+
181254
trackWorkflowExecution(): void {
182255
const context = this.getExecutionContext()
183256
this.trackEvent(TelemetryEvents.EXECUTION_START, context)
@@ -194,8 +267,28 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
194267
getExecutionContext(): ExecutionContext {
195268
const workflowStore = useWorkflowStore()
196269
const templatesStore = useWorkflowTemplatesStore()
270+
const nodeDefStore = useNodeDefStore()
197271
const activeWorkflow = workflowStore.activeWorkflow
198272

273+
// Calculate node metrics in a single traversal
274+
const nodeMetrics = reduceAllNodes(
275+
app.graph,
276+
(acc, node) => {
277+
const nodeDef = nodeDefStore.nodeDefsByName[node.type]
278+
const isCustomNode =
279+
nodeDef?.nodeSource?.type === NodeSourceType.CustomNodes
280+
const isApiNode = nodeDef?.api_node === true
281+
const isSubgraph = node.isSubgraphNode?.() === true
282+
283+
return {
284+
custom_node_count: acc.custom_node_count + (isCustomNode ? 1 : 0),
285+
api_node_count: acc.api_node_count + (isApiNode ? 1 : 0),
286+
subgraph_count: acc.subgraph_count + (isSubgraph ? 1 : 0)
287+
}
288+
},
289+
{ custom_node_count: 0, api_node_count: 0, subgraph_count: 0 }
290+
)
291+
199292
if (activeWorkflow?.filename) {
200293
const isTemplate = templatesStore.knownTemplateNames.has(
201294
activeWorkflow.filename
@@ -218,19 +311,22 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
218311
template_tags: englishMetadata?.tags ?? template?.tags,
219312
template_models: englishMetadata?.models ?? template?.models,
220313
template_use_case: englishMetadata?.useCase ?? template?.useCase,
221-
template_license: englishMetadata?.license ?? template?.license
314+
template_license: englishMetadata?.license ?? template?.license,
315+
...nodeMetrics
222316
}
223317
}
224318

225319
return {
226320
is_template: false,
227-
workflow_name: activeWorkflow.filename
321+
workflow_name: activeWorkflow.filename,
322+
...nodeMetrics
228323
}
229324
}
230325

231326
return {
232327
is_template: false,
233-
workflow_name: undefined
328+
workflow_name: undefined,
329+
...nodeMetrics
234330
}
235331
}
236332
}

0 commit comments

Comments
 (0)