8f20b2ef1b
- 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.
43 lines
1.3 KiB
TypeScript
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 ?? {},
|
|
});
|
|
}
|