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:
Christian Byrne
2026-01-20 13:35:54 -08:00
committed by GitHub
parent 79d3b2c291
commit 5df793b721
2 changed files with 177 additions and 0 deletions

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

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