Files
FrenoCorp/src/lib/analytics/kpi-service.ts
Michael Freno 67c3881dcf Add waitlist schema for marketing (FRE-635)
- Created waitlist_signups and waitlist_events tables
- Supports email, name, source tracking, and status management
- Enables VIP supporter list for Product Hunt launch
- Migration 0002_chemical_shocker.sql generated
- Fixed brand color in product-hunt-assets-brief.md (#518ac8)
2026-04-26 06:21:20 -04:00

123 lines
3.7 KiB
TypeScript

import { eq, and, gte, lte, desc } from "drizzle-orm";
import { kpiSnapshots } from "../../db/schema";
import type { DrizzleDB } from "../../db/config/migrations";
import type { NewKPISnapshot, KPISnapshot } from "../../db/schema";
export type KPIKey =
| "mau"
| "paying_users"
| "mrr"
| "conversion_rate"
| "churn_rate"
| "cac"
| "ltv"
| "nps"
| "viral_coefficient";
export const KPI_THRESHOLDS: Record<KPIKey, { warning: number; critical: number; direction: "higher" | "lower" }> = {
mau: { warning: 1000, critical: 500, direction: "higher" },
paying_users: { warning: 100, critical: 50, direction: "higher" },
mrr: { warning: 5000, critical: 2000, direction: "higher" },
conversion_rate: { warning: 2, critical: 1, direction: "higher" },
churn_rate: { warning: 5, critical: 3, direction: "lower" },
cac: { warning: 12, critical: 15, direction: "lower" },
ltv: { warning: 100, critical: 80, direction: "higher" },
nps: { warning: 40, critical: 20, direction: "higher" },
viral_coefficient: { warning: 0.3, critical: 0.1, direction: "higher" },
};
export async function recordKPI(
db: DrizzleDB,
kpiKey: KPIKey,
value: number,
periodStart: Date,
periodEnd: Date,
metadata?: Record<string, unknown>
): Promise<KPISnapshot> {
const snapshot: NewKPISnapshot = {
kpiKey,
kpiValue: value,
periodStart,
periodEnd,
metadata: metadata ? JSON.stringify(metadata) : null,
};
const result = await db.insert(kpiSnapshots).values(snapshot).returning();
return result[0]!;
}
export async function getLatestKPI(
db: DrizzleDB,
kpiKey: KPIKey
): Promise<KPISnapshot | undefined> {
const rows = await db
.select()
.from(kpiSnapshots)
.where(eq(kpiSnapshots.kpiKey, kpiKey))
.orderBy(desc(kpiSnapshots.createdAt))
.limit(1);
return rows[0];
}
export async function getKPIHistory(
db: DrizzleDB,
kpiKey: KPIKey,
periodStart?: Date,
periodEnd?: Date
): Promise<KPISnapshot[]> {
const conditions: import("drizzle-orm").SQL[] = [eq(kpiSnapshots.kpiKey, kpiKey)];
if (periodStart) {
conditions.push(gte(kpiSnapshots.periodStart, periodStart));
}
if (periodEnd) {
conditions.push(lte(kpiSnapshots.periodEnd, periodEnd));
}
return await db
.select()
.from(kpiSnapshots)
.where(and(...conditions))
.orderBy(kpiSnapshots.periodStart);
}
export async function getAllLatestKPIs(db: DrizzleDB): Promise<Record<KPIKey, KPISnapshot | undefined>> {
const result: Record<KPIKey, KPISnapshot | undefined> = {} as Record<KPIKey, KPISnapshot | undefined>;
const keys = Object.keys(KPI_THRESHOLDS) as KPIKey[];
for (const key of keys) {
result[key] = await getLatestKPI(db, key);
}
return result;
}
export function checkKPIAgainstThreshold(
kpiKey: KPIKey,
value: number
): { breached: boolean; severity: "warning" | "critical" | null } {
const thresholds = KPI_THRESHOLDS[kpiKey];
if (!thresholds) return { breached: false, severity: null };
const { warning, critical, direction } = thresholds;
const isHigher = direction === "higher";
if (isHigher) {
if (value <= critical) return { breached: true, severity: "critical" };
if (value <= warning) return { breached: true, severity: "warning" };
} else {
if (value >= critical) return { breached: true, severity: "critical" };
if (value >= warning) return { breached: true, severity: "warning" };
}
return { breached: false, severity: null };
}
export function getKPIStatus(
kpiKey: KPIKey,
value: number
): "healthy" | "warning" | "critical" {
const { breached, severity } = checkKPIAgainstThreshold(kpiKey, value);
if (!breached) return "healthy";
return severity === "critical" ? "critical" : "warning";
}