just the currents, never the identities
at main 3.3 kB view raw
1/** 2 * Event validation for Driftline Analytics 3 */ 4 5import type { AnalyticsEvent, Environment, EventType } from "./types.ts"; 6 7const VALID_ENVS: Environment[] = ["dev", "prod"]; 8const VALID_TYPES: EventType[] = ["account", "view", "action"]; 9const UID_PATTERN = /^[a-f0-9]{12}$/; 10 11export type ValidationResult = 12 | { valid: true; events: AnalyticsEvent[] } 13 | { valid: false; error: string }; 14 15function isValidIsoDate(str: string): boolean { 16 const date = new Date(str); 17 return !isNaN(date.getTime()) && date.toISOString() === str; 18} 19 20function validateSingleEvent(event: unknown, index?: number): string | null { 21 const prefix = index !== undefined ? `Event[${index}]: ` : ""; 22 23 if (!event || typeof event !== "object") { 24 return `${prefix}Event must be an object`; 25 } 26 27 const e = event as Record<string, unknown>; 28 29 if (e.v !== 1) { 30 return `${prefix}Version (v) must be 1`; 31 } 32 33 if (typeof e.appView !== "string" || e.appView.length === 0) { 34 return `${prefix}appView must be a non-empty string`; 35 } 36 37 if (!VALID_ENVS.includes(e.env as Environment)) { 38 return `${prefix}env must be 'dev' or 'prod'`; 39 } 40 41 if (typeof e.ts !== "string" || !isValidIsoDate(e.ts)) { 42 return `${prefix}ts must be a valid ISO timestamp`; 43 } 44 45 if (typeof e.uid !== "string" || !UID_PATTERN.test(e.uid)) { 46 return `${prefix}uid must be a 12-character hex string`; 47 } 48 49 if (!VALID_TYPES.includes(e.type as EventType)) { 50 return `${prefix}type must be 'account', 'view', or 'action'`; 51 } 52 53 if (typeof e.name !== "string" || e.name.length === 0) { 54 return `${prefix}name must be a non-empty string`; 55 } 56 57 if (e.screen !== undefined && typeof e.screen !== "string") { 58 return `${prefix}screen must be a string if provided`; 59 } 60 61 if ( 62 e.props !== undefined && (typeof e.props !== "object" || e.props === null) 63 ) { 64 return `${prefix}props must be an object if provided`; 65 } 66 67 return null; 68} 69 70export function validateCollectRequest(body: unknown): ValidationResult { 71 if (!body || typeof body !== "object") { 72 return { valid: false, error: "Request body must be an object" }; 73 } 74 75 const b = body as Record<string, unknown>; 76 77 // Check if it's a batch request 78 if ("events" in b && Array.isArray(b.events)) { 79 if (b.events.length === 0) { 80 return { valid: false, error: "Events array cannot be empty" }; 81 } 82 83 if (b.events.length > 100) { 84 return { valid: false, error: "Maximum 100 events per request" }; 85 } 86 87 const events: AnalyticsEvent[] = []; 88 for (let i = 0; i < b.events.length; i++) { 89 const error = validateSingleEvent(b.events[i], i); 90 if (error) { 91 return { valid: false, error }; 92 } 93 events.push(b.events[i] as AnalyticsEvent); 94 } 95 96 return { valid: true, events }; 97 } 98 99 // Single event 100 const error = validateSingleEvent(body); 101 if (error) { 102 return { valid: false, error }; 103 } 104 105 return { valid: true, events: [body as AnalyticsEvent] }; 106} 107 108export function validateAppViewMatch( 109 events: AnalyticsEvent[], 110 expectedAppView: string, 111): string | null { 112 for (let i = 0; i < events.length; i++) { 113 if (events[i].appView !== expectedAppView) { 114 return `Event[${i}]: appView '${ 115 events[i].appView 116 }' does not match API key's app_view '${expectedAppView}'`; 117 } 118 } 119 return null; 120}