1// Telemetry: event(env, name, { blobs, doubles }) writes one data point to 2// Workers Analytics Engine (the EVENTS binding) for an event declared in 3// events-schema.ts, which says what every column means and how the dataset 4// is laid out. The fields are named, and typed from the schema, so an event 5// can only be written with exactly the fields it declares; their columns 6// come from the schema, never from the call. 7// 8// event(env, 'jev.relay', { 9// blobs: { outcome: 'forwarded', colo: 'DFW' }, 10// doubles: { status: 200, upstreamMs: 412, requestBytes: 1830, sectionInS: 3.2, attempt: 0 }, 11// }); 12// 13// A null blob is written as null; a null (or non-finite) double as 0 with 14// its bit in double1 left clear. Writing never throws into the request: 15// Analytics Engine writes in the background, and a failure to hand it the 16// point is logged, not raised. Read it back with `nix run .#events` 17// (tools/events/). 18import { EVENTS } from './events-schema'; 19 20export { EVENTS }; 21type Schema = typeof EVENTS; 22export type EventName = keyof Schema; 23export type EventFields<N extends EventName> = { 24 blobs: { [K in keyof Schema[N]['blobs']]: string | null }; 25 doubles: { [K in keyof Schema[N]['doubles']]: number | null }; 26}; 27 28type Column = { column: string }; 29const position = (column: string, kind: 'blob' | 'double') => { 30 const m = new RegExp(`^${kind}(\\d+)$`).exec(column); 31 if (!m) throw new Error(`not a ${kind} column: ${column}`); 32 return Number(m[1]); 33}; 34 35// The data point for an event, as Analytics Engine takes it. 36export function dataPoint<N extends EventName>(name: N, fields: EventFields<N>): AnalyticsEngineDataPoint { 37 const schema = EVENTS[name] as unknown as { blobs: Record<string, Column>; doubles: Record<string, Column> }; 38 const given = fields as unknown as { blobs: Record<string, string | null>; doubles: Record<string, number | null> }; 39 const blobs: (string | null)[] = []; 40 for (const [field, { column }] of Object.entries(schema.blobs)) blobs[position(column, 'blob') - 1] = given.blobs[field] ?? null; 41 const doubles: number[] = [0]; 42 let known = 0; 43 for (const [field, { column }] of Object.entries(schema.doubles)) { 44 const k = position(column, 'double'); 45 const value = given.doubles[field]; 46 const isKnown = typeof value === 'number' && Number.isFinite(value); 47 doubles[k - 1] = isKnown ? value : 0; 48 if (isKnown) known |= 1 << (k - 2); 49 } 50 doubles[0] = known; 51 return { indexes: [name], blobs, doubles }; 52} 53 54export function event<N extends EventName>( 55 env: { EVENTS: AnalyticsEngineDataset }, 56 name: N, 57 fields: EventFields<N>, 58): void { 59 try { 60 env.EVENTS.writeDataPoint(dataPoint(name, fields)); 61 } catch (e) { 62 console.error({ event: 'events.write', name, error: (e as Error).message }); 63 } 64}