1import { describe, expect, it, vi } from 'vitest'; 2import config from '../wrangler.json'; 3import { dataPoint, event, EVENTS } from './events'; 4 5const relayFields = { 6 blobs: { outcome: 'forwarded', colo: 'DFW' }, 7 doubles: { status: 200, upstreamMs: 412, requestBytes: 1830, sectionInS: -0.5, attempt: 0 }, 8}; 9 10describe('the event schema', () => { 11 it("fills each event's columns from blob1 and double2, with no gap or repeat", () => { 12 for (const [name, def] of Object.entries(EVENTS)) { 13 const columns = (fields: Record<string, { column: string }>) => Object.values(fields).map((f) => f.column); 14 const blobs = columns(def.blobs); 15 const doubles = columns(def.doubles); 16 expect(blobs, name).toEqual(blobs.map((_, i) => `blob${i + 1}`)); 17 expect(doubles, name).toEqual(doubles.map((_, i) => `double${i + 2}`)); 18 // Analytics Engine takes 20 of each, and double1 is the known-mask 19 expect(blobs.length, name).toBeLessThanOrEqual(20); 20 expect(doubles.length, name).toBeLessThanOrEqual(19); 21 // the index is the name, at most 96 bytes 22 expect(new TextEncoder().encode(name).byteLength, name).toBeLessThanOrEqual(96); 23 } 24 }); 25 26 it('is written by production and dev alike, to one dataset', () => { 27 const expected = [{ binding: 'EVENTS', dataset: 'jevstrudel_events' }]; 28 expect(config.analytics_engine_datasets).toEqual(expected); 29 expect(config.env.dev.analytics_engine_datasets).toEqual(expected); 30 }); 31}); 32 33describe('dataPoint: named fields to columns', () => { 34 it('puts the name in index1 and each field in its column', () => { 35 expect(dataPoint('jev.relay', relayFields)).toEqual({ 36 indexes: ['jev.relay'], 37 blobs: ['forwarded', 'DFW'], 38 doubles: [0b11111, 200, 412, 1830, -0.5, 0], 39 }); 40 }); 41 42 it('writes an unknown double as 0, with its bit in double1 clear', () => { 43 const point = dataPoint('jev.relay', { 44 blobs: { outcome: 'refused', colo: null }, 45 doubles: { status: 429, upstreamMs: null, requestBytes: null, sectionInS: Number.NaN, attempt: 2 }, 46 }); 47 expect(point).toEqual({ 48 indexes: ['jev.relay'], 49 blobs: ['refused', null], 50 doubles: [0b10001, 429, 0, 0, 0, 2], 51 }); 52 // the bit for double k is 2^(k - 2), as the schema says 53 const known = point.doubles![0]; 54 expect(known & (1 << (2 - 2))).not.toBe(0); // double2, status 55 expect(known & (1 << (3 - 2))).toBe(0); // double3, upstreamMs 56 }); 57}); 58 59describe('event: never throws into the request', () => { 60 it('writes the data point', () => { 61 const writeDataPoint = vi.fn(); 62 event({ EVENTS: { writeDataPoint } }, 'jev.relay', relayFields); 63 expect(writeDataPoint).toHaveBeenCalledWith(dataPoint('jev.relay', relayFields)); 64 }); 65 66 it('logs a failed write instead of raising it', () => { 67 const error = vi.spyOn(console, 'error').mockImplementation(() => {}); 68 const writeDataPoint = () => { 69 throw new Error('too many data points'); 70 }; 71 expect(() => event({ EVENTS: { writeDataPoint } }, 'jev.relay', relayFields)).not.toThrow(); 72 expect(error).toHaveBeenCalledWith({ event: 'events.write', name: 'jev.relay', error: 'too many data points' }); 73 error.mockRestore(); 74 }); 75});