Files
Cibello-app/packages/database/src/analytics.ts
T
Sven (AAMOS AI) 8f20b2ef1b feat(ops): /ops/v1/summary med ai/aktivering/engagemang/betalning/jobb/sakerhet + canary
- Nya toppnivåblock i /ops/v1/summary: ai_scan, aktivering, engagemang,
  betalning, jobb, sakerhet. Alla värden läses från DB/cache; null där data
  saknas, inga påhittade värden.
- AI-kostnad i USD mikrocent (native); intäkter fortsatt SEK-öre.
- product_analytics_events är källa för volym, latens, lyckandegrad,
  felfrekvens, aktivering, retention och engagemang.
- Nya händelser: scan_started (API) och scan_failed/scan_completed med
  latencyMs + felkod (worker). latencyMs flödar nu in i scan_completed.
- Safety canary-jobb varje timme: re-härleder allergener för alla recept,
  räknar överifierade publika recept och food-safety-lint; skriver EN rad till
  ops_safety_canary. Endpointen läser endast sista raden.
- Cache-refresh-jobb var 60 s skriver hela summariet till Redis; endpointen
  serverar cachen med 503 vid cache-miss.
- Bearer-token-skydd med OPS_TOKEN; HTTPS-tvång i produktion; ingen PII.
- Tester för endpoint, auth, cache-miss och safety canary.
2026-08-10 05:47:43 +07:00

43 lines
1.3 KiB
TypeScript

import { and, eq } from "drizzle-orm";
import type { AnalyticsEvent } from "@app/analytics";
import type { Database } from "./client.js";
import { schema } from "./index.js";
/**
* Track a product analytics event server-side if the user has opted in.
* Moved to @app/database so both API routes and workers can write events
* without depending on apps/api internals.
*/
export async function trackProductAnalytics(
db: Database,
userId: string,
event: AnalyticsEvent,
): Promise<void> {
const optedIn = await db
.select({ status: schema.userConsents.status })
.from(schema.userConsents)
.where(
and(
eq(schema.userConsents.userId, userId),
eq(schema.userConsents.kind, "product_analytics"),
),
)
.limit(1);
if (optedIn[0] && optedIn[0].status !== "granted") return;
await db.insert(schema.productAnalyticsEvents).values({
occurredAt: event.occurredAt ? new Date(event.occurredAt) : new Date(),
receivedAt: new Date(),
eventName: event.name,
anonymousId: event.anonymousId ?? null,
sessionId: event.sessionId ?? null,
userId,
householdId: event.householdId ?? null,
appVersion: event.appVersion ?? null,
platform: event.platform ?? null,
locale: event.locale ?? null,
experimentVariant: event.experimentVariant ?? null,
properties: event.properties ?? {},
});
}