Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 234 additions & 0 deletions scripts/survey-data-migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
#!/usr/bin/env node

/**
* Survey Data Migration Script
*
* One-time utility to normalize existing Mixpanel user properties
* for industry and use case fields. This addresses the proliferation
* of one-off categories that make analytics difficult.
*
* Usage: pnpm ts-node scripts/survey-data-migration.ts
*
* IMPORTANT: This script requires Mixpanel Data Management API access
* and should be run with appropriate credentials in production.
*/

/* eslint-disable no-console */

import {
normalizeIndustry,
normalizeUseCase
} from '../src/platform/telemetry/utils/surveyNormalization'

interface MixpanelUser {
$distinct_id: string
$properties: {
industry?: string
useCase?: string
[key: string]: any
}
}

interface MigrationStats {
totalUsers: number
industryNormalized: number
useCaseNormalized: number
uncategorizedIndustries: Set<string>
uncategorizedUseCases: Set<string>
}

/**
* Simulate the data migration process
* In production, this would integrate with Mixpanel Data Management API
*/
function simulateMigration(users: MixpanelUser[]): MigrationStats {
const stats: MigrationStats = {
totalUsers: users.length,
industryNormalized: 0,
useCaseNormalized: 0,
uncategorizedIndustries: new Set<string>(),
uncategorizedUseCases: new Set<string>()
}

users.forEach((user) => {
let needsUpdate = false
const updates: Record<string, any> = {}

// Process industry normalization
if (user.$properties.industry) {
const normalized = normalizeIndustry(user.$properties.industry)

if (normalized !== user.$properties.industry) {
updates.industry_normalized = normalized
updates.industry_raw = user.$properties.industry
stats.industryNormalized++
needsUpdate = true

if (normalized.startsWith('Uncategorized:')) {
stats.uncategorizedIndustries.add(user.$properties.industry)
}
}
}

// Process use case normalization
if (user.$properties.useCase) {
const normalized = normalizeUseCase(user.$properties.useCase)

if (normalized !== user.$properties.useCase) {
updates.useCase_normalized = normalized
updates.useCase_raw = user.$properties.useCase
stats.useCaseNormalized++
needsUpdate = true

if (normalized.startsWith('Uncategorized:')) {
stats.uncategorizedUseCases.add(user.$properties.useCase)
}
}
}

// In production, this would make API calls to update user properties
if (needsUpdate) {
console.log(`Would update user ${user.$distinct_id}:`, updates)
}
})

return stats
}

/**
* Generate sample data for testing normalization rules
*/
function generateSampleData(): MixpanelUser[] {
return [
{
$distinct_id: 'user1',
$properties: {
industry: 'Film and television production',
useCase: 'Creating concept art for movies'
}
},
{
$distinct_id: 'user2',
$properties: {
industry: 'Marketing & Social Media',
useCase: 'YouTube thumbnail generation'
}
},
{
$distinct_id: 'user3',
$properties: {
industry: 'Software Development',
useCase: 'Product mockup creation'
}
},
{
$distinct_id: 'user4',
$properties: {
industry: 'Indie Game Studio',
useCase: 'Game asset generation'
}
},
{
$distinct_id: 'user5',
$properties: {
industry: 'Architecture firm',
useCase: 'Building visualization'
}
},
{
$distinct_id: 'user6',
$properties: {
industry: 'Custom Jewelry Design',
useCase: 'Product photography'
}
},
{
$distinct_id: 'user7',
$properties: {
industry: 'Medical Research',
useCase: 'Scientific visualization'
}
},
{
$distinct_id: 'user8',
$properties: {
industry: 'Unknown Creative Field',
useCase: 'Personal art projects'
}
}
]
}

/**
* Production implementation would use Mixpanel Data Management API
* Example API structure (not actual implementation):
*/
async function productionMigration() {
console.log('🔧 Production Migration Process:')
console.log('1. Export user profiles via Mixpanel Data Management API')
console.log('2. Apply normalization to industry and useCase fields')
console.log(
'3. Create new properties: industry_normalized, useCase_normalized'
)
console.log('4. Preserve original values as: industry_raw, useCase_raw')
console.log('5. Batch update user profiles')
console.log('6. Generate uncategorized response report for review')

/*
Example API calls:

// 1. Export users
const users = await mixpanel.people.query({
where: 'properties["industry"] != null or properties["useCase"] != null'
})

// 2. Process and update
for (const user of users) {
const normalizedData = normalizeSurveyResponses(user.properties)
await mixpanel.people.set(user.distinct_id, normalizedData)
}
*/
}

/**
* Main migration runner
*/
function main() {
console.log('📊 Survey Data Migration Utility')
console.log('================================\n')

// Run simulation with sample data
console.log('🧪 Running simulation with sample data...\n')
const sampleUsers = generateSampleData()
const stats = simulateMigration(sampleUsers)

// Display results
console.log('📈 Migration Results:')
console.log(`Total users processed: ${stats.totalUsers}`)
console.log(`Industry fields normalized: ${stats.industryNormalized}`)
console.log(`Use case fields normalized: ${stats.useCaseNormalized}`)

if (stats.uncategorizedIndustries.size > 0) {
console.log('\n❓ Uncategorized Industries (need review):')
Array.from(stats.uncategorizedIndustries).forEach((industry) => {
console.log(` • ${industry}`)
})
}

if (stats.uncategorizedUseCases.size > 0) {
console.log('\n❓ Uncategorized Use Cases (need review):')
Array.from(stats.uncategorizedUseCases).forEach((useCase) => {
console.log(` • ${useCase}`)
})
}

console.log('\n' + '='.repeat(50))
void productionMigration()
}

// Run if called directly
if (require.main === module) {
main()
}

export { simulateMigration, generateSampleData, MigrationStats }
7 changes: 6 additions & 1 deletion src/components/dialog/content/credit/CreditTopUpOption.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ import Tag from 'primevue/tag'
import { onBeforeUnmount, ref } from 'vue'

import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useTelemetry } from '@/platform/telemetry'

const authActions = useFirebaseAuthActions()
const telemetry = useTelemetry()

const {
amount,
Expand All @@ -61,8 +63,11 @@ const didClickBuyNow = ref(false)
const loading = ref(false)

const handleBuyNow = async () => {
const creditAmount = editable ? customAmount.value : amount
telemetry?.trackApiCreditTopupButtonPurchaseClicked(creditAmount)

loading.value = true
await authActions.purchaseCredits(editable ? customAmount.value : amount)
await authActions.purchaseCredits(creditAmount)
loading.value = false
didClickBuyNow.value = true
}
Expand Down
25 changes: 24 additions & 1 deletion src/components/searchbox/NodeSearchBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
multiple
:option-label="'display_name'"
@complete="search($event.query)"
@option-select="emit('addNode', $event.value)"
@option-select="onAddNode($event.value)"
@focused-option-changed="setHoverSuggestion($event)"
>
<template #option="{ option }">
Expand All @@ -78,6 +78,7 @@
</template>

<script setup lang="ts">
import { debounce } from 'es-toolkit/compat'
import Button from 'primevue/button'
import Dialog from 'primevue/dialog'
import { computed, nextTick, onMounted, ref } from 'vue'
Expand All @@ -88,6 +89,7 @@ import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
import NodeSearchItem from '@/components/searchbox/NodeSearchItem.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
import type { FuseFilterWithValue } from '@/utils/fuseUtil'
Expand All @@ -96,6 +98,7 @@ import SearchFilterChip from '../common/SearchFilterChip.vue'

const settingStore = useSettingStore()
const { t } = useI18n()
const telemetry = useTelemetry()

const enableNodePreview = computed(() =>
settingStore.get('Comfy.NodeSearchBoxImpl.NodePreview')
Expand All @@ -118,6 +121,14 @@ const placeholder = computed(() => {

const nodeDefStore = useNodeDefStore()
const nodeFrequencyStore = useNodeFrequencyStore()

// Debounced search tracking (500ms as per implementation plan)
const debouncedTrackSearch = debounce((query: string) => {
if (query.trim()) {
telemetry?.trackNodeSearch({ query })
}
}, 500)

const search = (query: string) => {
const queryIsEmpty = query === '' && filters.length === 0
currentQuery.value = query
Expand All @@ -128,10 +139,22 @@ const search = (query: string) => {
limit: searchLimit
})
]

// Track search queries with debounce
debouncedTrackSearch(query)
}

const emit = defineEmits(['addFilter', 'removeFilter', 'addNode'])

// Track node selection and emit addNode event
const onAddNode = (nodeDef: ComfyNodeDefImpl) => {
telemetry?.trackNodeSearchResultSelected({
node_type: nodeDef.name,
last_query: currentQuery.value
})
emit('addNode', nodeDef)
}

let inputElement: HTMLInputElement | null = null
const reFocusInput = async () => {
inputElement ??= document.getElementById(inputId) as HTMLInputElement
Expand Down
36 changes: 35 additions & 1 deletion src/composables/useTemplateFiltering.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { refDebounced } from '@vueuse/core'
import Fuse from 'fuse.js'
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import type { Ref } from 'vue'

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

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

// Template filter tracking (debounced to avoid excessive events)
const debouncedTrackFilterChange = debounce(() => {
useTelemetry()?.trackTemplateFilterChanged({
search_query: searchQuery.value || undefined,
selected_models: selectedModels.value,
selected_use_cases: selectedUseCases.value,
selected_licenses: selectedLicenses.value,
sort_by: sortBy.value,
filtered_count: filteredCount.value,
total_count: totalCount.value
})
}, 500)

// Watch for filter changes and track them
watch(
[searchQuery, selectedModels, selectedUseCases, selectedLicenses, sortBy],
() => {
// Only track if at least one filter is active (to avoid tracking initial state)
const hasActiveFilters =
searchQuery.value.trim() !== '' ||
selectedModels.value.length > 0 ||
selectedUseCases.value.length > 0 ||
selectedLicenses.value.length > 0 ||
sortBy.value !== 'default'

if (hasActiveFilters) {
debouncedTrackFilterChange()
}
},
{ deep: true }
)

return {
// State
searchQuery,
Expand Down
3 changes: 3 additions & 0 deletions src/composables/useWorkflowTemplateSelectorDialog.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import WorkflowTemplateSelectorDialog from '@/components/custom/widget/WorkflowTemplateSelectorDialog.vue'
import { useTelemetry } from '@/platform/telemetry'
import { useDialogService } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'

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

function show() {
useTelemetry()?.trackTemplateLibraryOpened({ source: 'command' })

dialogService.showLayoutDialog({
key: DIALOG_KEY,
component: WorkflowTemplateSelectorDialog,
Expand Down
Loading
Loading