jevstrudel.git / worker / src / events.test.ts
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});