mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
feat: add feature usage tracker for nightly surveys (#8175)
Introduces `useFeatureUsageTracker` composable that tracks how many times a user has used a specific feature, along with first and last usage timestamps. Data persists to localStorage using `@vueuse/core`'s `useStorage`. This composable provides the foundation for triggering surveys after a configurable number of feature uses. Includes comprehensive unit tests. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8175-feat-add-feature-usage-tracker-for-nightly-surveys-2ee6d73d36508118859ece6fcf17561d) by [Unito](https://www.unito.io)
This commit is contained in:
131
src/platform/surveys/useFeatureUsageTracker.test.ts
Normal file
131
src/platform/surveys/useFeatureUsageTracker.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const STORAGE_KEY = 'Comfy.FeatureUsage'
|
||||
|
||||
describe('useFeatureUsageTracker', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('initializes with zero count for new feature', async () => {
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const { useCount } = useFeatureUsageTracker('test-feature')
|
||||
|
||||
expect(useCount.value).toBe(0)
|
||||
})
|
||||
|
||||
it('increments count on trackUsage', async () => {
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const { useCount, trackUsage } = useFeatureUsageTracker('test-feature')
|
||||
|
||||
expect(useCount.value).toBe(0)
|
||||
|
||||
trackUsage()
|
||||
expect(useCount.value).toBe(1)
|
||||
|
||||
trackUsage()
|
||||
expect(useCount.value).toBe(2)
|
||||
})
|
||||
|
||||
it('sets firstUsed only on first use', async () => {
|
||||
vi.useFakeTimers()
|
||||
const firstTs = 1000000
|
||||
vi.setSystemTime(firstTs)
|
||||
try {
|
||||
const { useFeatureUsageTracker } =
|
||||
await import('./useFeatureUsageTracker')
|
||||
const { usage, trackUsage } = useFeatureUsageTracker('test-feature')
|
||||
|
||||
trackUsage()
|
||||
expect(usage.value?.firstUsed).toBe(firstTs)
|
||||
|
||||
vi.setSystemTime(firstTs + 5000)
|
||||
trackUsage()
|
||||
expect(usage.value?.firstUsed).toBe(firstTs)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('updates lastUsed on each use', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { useFeatureUsageTracker } =
|
||||
await import('./useFeatureUsageTracker')
|
||||
const { usage, trackUsage } = useFeatureUsageTracker('test-feature')
|
||||
|
||||
trackUsage()
|
||||
const firstLastUsed = usage.value?.lastUsed ?? 0
|
||||
|
||||
vi.advanceTimersByTime(10)
|
||||
trackUsage()
|
||||
|
||||
expect(usage.value?.lastUsed).toBeGreaterThan(firstLastUsed)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('reset clears feature data', async () => {
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const { useCount, trackUsage, reset } =
|
||||
useFeatureUsageTracker('test-feature')
|
||||
|
||||
trackUsage()
|
||||
trackUsage()
|
||||
expect(useCount.value).toBe(2)
|
||||
|
||||
reset()
|
||||
expect(useCount.value).toBe(0)
|
||||
})
|
||||
|
||||
it('tracks multiple features independently', async () => {
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const featureA = useFeatureUsageTracker('feature-a')
|
||||
const featureB = useFeatureUsageTracker('feature-b')
|
||||
|
||||
featureA.trackUsage()
|
||||
featureA.trackUsage()
|
||||
featureB.trackUsage()
|
||||
|
||||
expect(featureA.useCount.value).toBe(2)
|
||||
expect(featureB.useCount.value).toBe(1)
|
||||
})
|
||||
|
||||
it('persists to localStorage', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { useFeatureUsageTracker } =
|
||||
await import('./useFeatureUsageTracker')
|
||||
const { trackUsage } = useFeatureUsageTracker('persisted-feature')
|
||||
|
||||
trackUsage()
|
||||
await vi.runAllTimersAsync()
|
||||
|
||||
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}')
|
||||
expect(stored['persisted-feature']?.useCount).toBe(1)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('loads existing data from localStorage', async () => {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
'existing-feature': { useCount: 5, firstUsed: 1000, lastUsed: 2000 }
|
||||
})
|
||||
)
|
||||
|
||||
vi.resetModules()
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const { useCount } = useFeatureUsageTracker('existing-feature')
|
||||
|
||||
expect(useCount.value).toBe(5)
|
||||
})
|
||||
})
|
||||
46
src/platform/surveys/useFeatureUsageTracker.ts
Normal file
46
src/platform/surveys/useFeatureUsageTracker.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface FeatureUsage {
|
||||
useCount: number
|
||||
firstUsed: number
|
||||
lastUsed: number
|
||||
}
|
||||
|
||||
type FeatureUsageRecord = Record<string, FeatureUsage>
|
||||
|
||||
const STORAGE_KEY = 'Comfy.FeatureUsage'
|
||||
|
||||
/**
|
||||
* Tracks feature usage for survey eligibility.
|
||||
* Persists to localStorage.
|
||||
*/
|
||||
export function useFeatureUsageTracker(featureId: string) {
|
||||
const usageData = useStorage<FeatureUsageRecord>(STORAGE_KEY, {})
|
||||
|
||||
const usage = computed(() => usageData.value[featureId])
|
||||
|
||||
const useCount = computed(() => usage.value?.useCount ?? 0)
|
||||
|
||||
function trackUsage() {
|
||||
const now = Date.now()
|
||||
const existing = usageData.value[featureId]
|
||||
|
||||
usageData.value[featureId] = {
|
||||
useCount: (existing?.useCount ?? 0) + 1,
|
||||
firstUsed: existing?.firstUsed ?? now,
|
||||
lastUsed: now
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
delete usageData.value[featureId]
|
||||
}
|
||||
|
||||
return {
|
||||
usage,
|
||||
useCount,
|
||||
trackUsage,
|
||||
reset
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user