feat: add useFeatureUsageTracker composable for survey eligibility

This commit is contained in:
bymyself
2026-01-19 19:24:59 -08:00
parent b5f91977c8
commit 03ffc6b4c3
2 changed files with 163 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
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 () => {
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
const { usage, trackUsage } = useFeatureUsageTracker('test-feature')
const beforeFirst = Date.now()
trackUsage()
const afterFirst = Date.now()
const firstUsed = usage.value?.firstUsed ?? 0
expect(firstUsed).toBeGreaterThanOrEqual(beforeFirst)
expect(firstUsed).toBeLessThanOrEqual(afterFirst)
trackUsage()
expect(usage.value?.firstUsed).toBe(firstUsed)
})
it('updates lastUsed on each use', async () => {
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
const { usage, trackUsage } = useFeatureUsageTracker('test-feature')
trackUsage()
const firstLastUsed = usage.value?.lastUsed ?? 0
await new Promise((r) => setTimeout(r, 10))
trackUsage()
expect(usage.value?.lastUsed).toBeGreaterThan(firstLastUsed)
})
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 () => {
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
const { trackUsage } = useFeatureUsageTracker('persisted-feature')
trackUsage()
// useStorage flushes async, wait a tick
await new Promise((r) => setTimeout(r, 0))
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}')
expect(stored['persisted-feature']?.useCount).toBe(1)
})
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)
})
})

View 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
}
}