/** * Product analytics schema (spec ยง8). * Pseudonymous events stored in Postgres, separated from AAMOS memory. */ import { index, integer, jsonb, pgTable, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core"; import { createdAt } from "./_shared.js"; import { households } from "./households.js"; import { users } from "./users.js"; /** Event name stored as text so the taxonomy can grow without ALTER TYPE. */ const ANALYTICS_EVENT_NAME_LEN = 64; /** * Raw product analytics events. * No PII, no raw ingredient names, no free text, no images. */ export const productAnalyticsEvents = pgTable( "product_analytics_events", { id: uuid("id").primaryKey().defaultRandom(), occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(), receivedAt: timestamp("received_at", { withTimezone: true }).notNull().defaultNow(), eventName: varchar("event_name", { length: ANALYTICS_EVENT_NAME_LEN }).notNull(), /** Pseudonymous device/session identifiers. */ anonymousId: varchar("anonymous_id", { length: 64 }), sessionId: varchar("session_id", { length: 64 }), /** Foreign keys are nullable because events may arrive before login. */ userId: uuid("user_id").references(() => users.id, { onDelete: "set null" }), householdId: uuid("household_id").references(() => households.id, { onDelete: "set null" }), /** App version, platform, locale. */ appVersion: varchar("app_version", { length: 32 }), platform: varchar("platform", { length: 16 }), locale: varchar("locale", { length: 16 }), /** Experiment / feature flag context. */ experimentVariant: varchar("experiment_variant", { length: 128 }), /** Structured, safe properties only. */ properties: jsonb("properties").notNull().default({}), }, (t) => [ index("pa_events_name_occurred_idx").on(t.eventName, t.occurredAt), index("pa_events_household_occurred_idx").on(t.householdId, t.occurredAt), index("pa_events_user_occurred_idx").on(t.userId, t.occurredAt), index("pa_events_session_idx").on(t.sessionId), index("pa_events_received_idx").on(t.receivedAt), ], ); /** * Materialized-like daily aggregates per household. * Kept simple; dashboards can also query product_analytics_events directly. */ export const analyticsDailySnapshots = pgTable( "analytics_daily_snapshots", { id: uuid("id").primaryKey().defaultRandom(), date: varchar("date", { length: 10 }).notNull(), // YYYY-MM-DD UTC householdId: uuid("household_id").references(() => households.id, { onDelete: "cascade" }), eventName: varchar("event_name", { length: ANALYTICS_EVENT_NAME_LEN }).notNull(), count: integer("count").notNull().default(0), uniqueSessions: integer("unique_sessions").notNull().default(0), propertiesFingerprint: varchar("properties_fingerprint", { length: 64 }), }, (t) => [ index("pa_daily_household_date_idx").on(t.householdId, t.date), index("pa_daily_date_event_idx").on(t.date, t.eventName), ], );