/// import { type Cell, cell, Default, derive, handler, lift, recipe, str, } from "commontools"; interface FeatureUsageArgs { events: Default; defaultDelta: Default; } interface FeatureUsageInput { feature?: string; cohort?: string; count?: number; } interface FeatureUsageBucket { feature: string; cohort: string; count: number; } interface FeatureUsageEvent { feature?: string; cohort?: string; delta?: number; value?: number; } interface TopEntry { name: string; count: number; } interface MetricsData { featureTotals: Record; cohortTotals: Record; matrix: Record>; total: number; topFeature: TopEntry; topCohort: TopEntry; snapshot: UsageMetricsSnapshot; } interface UsageMetricsSnapshot { total: number; features: Record; cohorts: Record; featureCount: number; cohortCount: number; topFeature: string; topFeatureCount: number; topCohort: string; topCohortCount: number; } const sanitizeLabel = (input: unknown, fallback: string): string => { if (typeof input === "string") { const trimmed = input.trim(); if (trimmed.length > 0) return trimmed; } return fallback; }; const sanitizeCount = (input: unknown, fallback: number): number => { if (typeof input !== "number" || !Number.isFinite(input)) { return fallback; } const rounded = Math.round(input); return rounded < 0 ? 0 : rounded; }; const sanitizeEvents = (value: unknown): FeatureUsageBucket[] => { if (!Array.isArray(value)) return []; const aggregated = new Map(); for (let index = 0; index < value.length; index++) { const raw = value[index] as FeatureUsageInput | undefined; const fallbackFeature = `feature-${index + 1}`; const feature = sanitizeLabel(raw?.feature, fallbackFeature); const cohort = sanitizeLabel(raw?.cohort, "general"); const count = sanitizeCount(raw?.count, 0); const key = `${feature}::${cohort}`; const existing = aggregated.get(key); if (existing) { aggregated.set(key, { feature, cohort, count: existing.count + count, }); } else { aggregated.set(key, { feature, cohort, count }); } } const buckets = Array.from(aggregated.values()); buckets.sort((left, right) => { const featureOrder = left.feature.localeCompare(right.feature); if (featureOrder !== 0) return featureOrder; return left.cohort.localeCompare(right.cohort); }); return buckets; }; const computeFeatureTotals = ( buckets: readonly FeatureUsageBucket[], ): Record => { const totals: Record = {}; for (const bucket of buckets) { const { feature, count } = bucket; totals[feature] = (totals[feature] ?? 0) + count; } return totals; }; const computeCohortTotals = ( buckets: readonly FeatureUsageBucket[], ): Record => { const totals: Record = {}; for (const bucket of buckets) { const { cohort, count } = bucket; totals[cohort] = (totals[cohort] ?? 0) + count; } return totals; }; const computeMatrix = ( buckets: readonly FeatureUsageBucket[], ): Record> => { const output: Record> = {}; for (const bucket of buckets) { const featureEntry = output[bucket.feature] ?? {}; featureEntry[bucket.cohort] = bucket.count; output[bucket.feature] = featureEntry; } return output; }; const pickTopEntry = (totals: Record): TopEntry => { const keys = Object.keys(totals); if (keys.length === 0) return { name: "none", count: 0 }; let best = keys[0]; for (let index = 1; index < keys.length; index++) { const candidate = keys[index]; const value = totals[candidate] ?? 0; const bestValue = totals[best] ?? 0; if (value > bestValue) { best = candidate; continue; } if (value === bestValue && candidate.localeCompare(best) < 0) { best = candidate; } } return { name: best, count: totals[best] ?? 0 }; }; const formatTopFeature = (entry: TopEntry): string => { const name = entry.name; const count = entry.count; if (count === 0) return `${name} (0 events)`; const plural = count === 1 ? "event" : "events"; return `${name} (${count} ${plural})`; }; const formatCohortCount = (count: number): string => { const sanitized = Number.isFinite(count) && count > 0 ? Math.trunc(count) : 0; const plural = sanitized === 1 ? "cohort" : "cohorts"; return `${sanitized} ${plural}`; }; const recordFeatureUsage = handler( ( event: FeatureUsageEvent | undefined, context: { events: Cell; defaultDelta: Cell; lastEvent: Cell; }, ) => { const buckets = sanitizeEvents(context.events.get()); const fallbackFeature = `feature-${buckets.length + 1}`; const feature = sanitizeLabel(event?.feature, fallbackFeature); const cohort = sanitizeLabel(event?.cohort, "general"); const defaultDelta = sanitizeCount(context.defaultDelta.get(), 1) || 1; const delta = sanitizeCount(event?.delta, defaultDelta) || defaultDelta; const hasOverride = typeof event?.value === "number" && Number.isFinite(event.value); const override = hasOverride ? sanitizeCount(event?.value, delta) : undefined; const index = buckets.findIndex((entry) => entry.feature === feature && entry.cohort === cohort ); if (index >= 0) { const existing = buckets[index]; const next = override ?? existing.count + delta; buckets[index] = { feature, cohort, count: next }; } else { const initial = override ?? delta; buckets.push({ feature, cohort, count: initial }); } buckets.sort((left, right) => { const featureOrder = left.feature.localeCompare(right.feature); if (featureOrder !== 0) return featureOrder; return left.cohort.localeCompare(right.cohort); }); context.events.set(buckets.map((entry) => ({ feature: entry.feature, cohort: entry.cohort, count: entry.count, }))); const descriptor = override !== undefined ? `=${override}` : `+${delta}`; context.lastEvent.set(`${feature}>${cohort} ${descriptor}`); }, ); export const featureUsageAnalytics = recipe( "Feature Usage Analytics", ({ events, defaultDelta }) => { const lastEvent = cell("none"); const usageBuckets = lift((value: FeatureUsageInput[] | undefined) => sanitizeEvents(value) )(events); const defaultDeltaValue = lift((value: number | undefined) => { const sanitized = sanitizeCount(value, 1); return sanitized === 0 ? 1 : sanitized; })(defaultDelta); const metricsData = lift((list: FeatureUsageBucket[] | undefined) => { const buckets = Array.isArray(list) ? list : []; const featureTotals = computeFeatureTotals(buckets); const cohortTotals = computeCohortTotals(buckets); const matrix = computeMatrix(buckets); const total = buckets.reduce((sum, entry) => sum + entry.count, 0); const topFeature = pickTopEntry(featureTotals); const topCohort = pickTopEntry(cohortTotals); const snapshot: UsageMetricsSnapshot = { total, features: featureTotals, cohorts: cohortTotals, featureCount: Object.keys(featureTotals).length, cohortCount: Object.keys(cohortTotals).length, topFeature: topFeature.name, topFeatureCount: topFeature.count, topCohort: topCohort.name, topCohortCount: topCohort.count, }; return { featureTotals, cohortTotals, matrix, total, topFeature, topCohort, snapshot, } as MetricsData; })(usageBuckets); const featureTotals = derive(metricsData, (view) => view.featureTotals); const cohortTotals = derive(metricsData, (view) => view.cohortTotals); const matrix = derive(metricsData, (view) => view.matrix); const totalCount = derive(metricsData, (view) => view.total); const topFeatureEntry = derive(metricsData, (view) => view.topFeature); const topCohortEntry = derive(metricsData, (view) => view.topCohort); const metricsSnapshot = derive(metricsData, (view) => view.snapshot); const featureCount = derive( featureTotals, (totals) => Object.keys(totals).length, ); const cohortCount = derive( cohortTotals, (totals) => Object.keys(totals).length, ); const topFeatureLabel = lift((entry: TopEntry | undefined) => entry ? formatTopFeature(entry) : formatTopFeature({ name: "none", count: 0, }) )(topFeatureEntry); const cohortLabel = lift((count: number | undefined) => formatCohortCount(typeof count === "number" ? count : 0) )(cohortCount); const lastEventView = lift((value: string | undefined) => { if (typeof value === "string" && value.trim().length > 0) { return value; } return "none"; })(lastEvent); const statusLabel = str`Top feature ${topFeatureLabel} across ${cohortLabel}`; return { usage: usageBuckets, featureTotals, cohortTotals, matrix, totalCount, featureCount, cohortCount, topFeature: derive(topFeatureEntry, (entry) => entry.name), topFeatureCount: derive(topFeatureEntry, (entry) => entry.count), topCohort: derive(topCohortEntry, (entry) => entry.name), topCohortCount: derive(topCohortEntry, (entry) => entry.count), statusLabel, lastEvent: lastEventView, metricsSnapshot, controls: { record: recordFeatureUsage({ events, defaultDelta: defaultDeltaValue, lastEvent, }), }, }; }, );