Fas 1a: analytics tracker, ingestion endpoint, admin dashboards

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 22:15:37 +07:00
parent d627014425
commit 1250640b1d
16 changed files with 4544 additions and 6450 deletions
+44
View File
@@ -0,0 +1,44 @@
import { z } from "zod";
import { PRODUCT_ANALYTICS_EVENT_NAMES } from "@app/shared-types";
export const MAX_ANALYTICS_BATCH_SIZE = 50;
export const MAX_ANALYTICS_PROPERTY_SIZE = 8 * 1024; // 8 KiB per event properties JSON
const eventNameSchema = z.enum(PRODUCT_ANALYTICS_EVENT_NAMES);
const analyticsEventSchema = z.object({
name: eventNameSchema,
occurredAt: z.string().refine((v) => !Number.isNaN(Date.parse(v))).optional(),
anonymousId: z.string().max(64).optional(),
sessionId: z.string().max(64).optional(),
householdId: z.string().uuid().optional(),
appVersion: z.string().max(32).optional(),
platform: z.enum(["ios", "android", "web"]).optional(),
locale: z.string().max(16).optional(),
experimentVariant: z.string().max(128).optional(),
properties: z.record(z.string(), z.unknown()).default({}),
});
export const analyticsBatchInputSchema = z.object({
events: z
.array(analyticsEventSchema)
.min(1, "At least one event is required")
.max(MAX_ANALYTICS_BATCH_SIZE, `Max ${MAX_ANALYTICS_BATCH_SIZE} events per batch`),
});
export type AnalyticsBatchInput = z.infer<typeof analyticsBatchInputSchema>;
/**
* Validate that the JSON-stringified properties of each event fit within the
* per-event byte budget. Call this after zod parsing.
*/
export function validateAnalyticsPropertySize(events: AnalyticsBatchInput["events"]): void {
for (let i = 0; i < events.length; i++) {
const size = new Blob([JSON.stringify(events[i]?.properties ?? {})]).size;
if (size > MAX_ANALYTICS_PROPERTY_SIZE) {
throw new Error(
`Event ${i} properties exceed ${MAX_ANALYTICS_PROPERTY_SIZE} bytes (${size})`,
);
}
}
}
+1
View File
@@ -12,3 +12,4 @@ export * from "./recommendations.js";
export * from "./memory.js";
export * from "./subscriptions.js";
export * from "./locale.js";
export * from "./analytics.js";