Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at theme-changes 183 lines 5.2 kB view raw
1import assert from 'node:assert' 2import {afterEach, beforeEach, describe, it, mock} from 'node:test' 3 4import {httpLogger} from './logger.js' 5import {MetricsClient} from './metrics.js' 6 7type TestEvents = { 8 click: {button: string} 9 view: {screen: string} 10} 11 12describe('MetricsClient', () => { 13 let fetchMock: ReturnType<typeof mock.fn> 14 let fetchRequests: {body: any}[] 15 let client: MetricsClient<TestEvents> 16 let loggerErrorMock: ReturnType<typeof mock.fn> 17 18 beforeEach(() => { 19 mock.timers.enable({apis: ['setInterval', 'setTimeout']}) 20 fetchRequests = [] 21 fetchMock = mock.fn(async (_url: any, options: any) => { 22 const body = JSON.parse(options.body) 23 fetchRequests.push({body}) 24 return {ok: true, status: 200, text: async () => ''} 25 }) 26 ;(globalThis as any).fetch = fetchMock 27 loggerErrorMock = mock.fn() 28 httpLogger.error = loggerErrorMock as any 29 }) 30 31 afterEach(() => { 32 client?.stop() 33 mock.timers.reset() 34 mock.restoreAll() 35 }) 36 37 it('flushes events on interval', async () => { 38 client = new MetricsClient<TestEvents>({ 39 trackingEndpoint: 'https://test.metrics.api', 40 }) 41 client.track('click', {button: 'submit'}) 42 client.track('view', {screen: 'home'}) 43 44 assert.strictEqual(fetchRequests.length, 0) 45 46 mock.timers.tick(10_000) 47 await flush() 48 49 assert.strictEqual(fetchRequests.length, 1) 50 assert.strictEqual(fetchRequests[0].body.events.length, 2) 51 assert.strictEqual(fetchRequests[0].body.events[0].event, 'click') 52 assert.strictEqual(fetchRequests[0].body.events[1].event, 'view') 53 }) 54 55 it('flushes when maxBatchSize is exceeded', async () => { 56 client = new MetricsClient<TestEvents>({ 57 trackingEndpoint: 'https://test.metrics.api', 58 }) 59 client.maxBatchSize = 5 60 61 for (let i = 0; i < 5; i++) { 62 client.track('click', {button: `btn-${i}`}) 63 } 64 65 assert.strictEqual(fetchRequests.length, 0) 66 67 client.track('click', {button: 'btn-trigger'}) 68 await flush() 69 70 assert.strictEqual(fetchRequests.length, 1) 71 assert.strictEqual(fetchRequests[0].body.events.length, 6) 72 }) 73 74 it('logs error on failed request', async () => { 75 fetchMock.mock.mockImplementation(async () => { 76 return { 77 ok: false, 78 status: 500, 79 text: async () => 'Internal Server Error', 80 } 81 }) 82 83 client = new MetricsClient<TestEvents>({ 84 trackingEndpoint: 'https://test.metrics.api', 85 }) 86 client.track('click', {button: 'submit'}) 87 88 mock.timers.tick(10_000) 89 await flush() 90 91 assert.strictEqual(fetchMock.mock.callCount(), 1) 92 assert.strictEqual(loggerErrorMock.mock.callCount(), 1) 93 const call = loggerErrorMock.mock.calls[0] 94 const arg = call.arguments[0] as {err: Error} 95 assert.ok(arg.err instanceof Error) 96 assert.strictEqual(call.arguments[1], 'Failed to send metrics') 97 }) 98 99 it('handles fetch text() error gracefully', async () => { 100 fetchMock.mock.mockImplementation(async () => { 101 return { 102 ok: false, 103 status: 500, 104 text: async () => { 105 throw new Error('Failed to read response') 106 }, 107 } 108 }) 109 110 client = new MetricsClient<TestEvents>({ 111 trackingEndpoint: 'https://test.metrics.api', 112 }) 113 client.track('click', {button: 'submit'}) 114 115 mock.timers.tick(10_000) 116 await flush() 117 118 assert.strictEqual(fetchMock.mock.callCount(), 1) 119 assert.strictEqual(loggerErrorMock.mock.callCount(), 1) 120 const call = loggerErrorMock.mock.calls[0] 121 const arg = call.arguments[0] as {err: Error} 122 assert.ok(arg.err instanceof Error) 123 assert.match(arg.err.message, /Unknown error/) 124 assert.strictEqual(call.arguments[1], 'Failed to send metrics') 125 }) 126 127 it('flushes when stop() is called', async () => { 128 client = new MetricsClient<TestEvents>({ 129 trackingEndpoint: 'https://test.metrics.api', 130 }) 131 client.track('click', {button: 'submit'}) 132 133 assert.strictEqual(fetchRequests.length, 0) 134 135 client.stop() 136 await flush() 137 138 assert.strictEqual(fetchRequests.length, 1) 139 assert.strictEqual(fetchRequests[0].body.events.length, 1) 140 assert.strictEqual(fetchRequests[0].body.events[0].event, 'click') 141 }) 142 143 it('does not send if trackingEndpoint is not configured', async () => { 144 client = new MetricsClient<TestEvents>({}) 145 client.track('click', {button: 'submit'}) 146 147 mock.timers.tick(10_000) 148 await flush() 149 150 assert.strictEqual(fetchMock.mock.callCount(), 0) 151 }) 152 153 it('start() is idempotent', async () => { 154 client = new MetricsClient<TestEvents>({ 155 trackingEndpoint: 'https://test.metrics.api', 156 }) 157 158 client.track('click', {button: 'submit'}) 159 client.start() 160 client.start() 161 162 mock.timers.tick(10_000) 163 await flush() 164 165 assert.strictEqual(fetchRequests.length, 1) 166 }) 167 168 it('does not flush if queue is empty', async () => { 169 client = new MetricsClient<TestEvents>({ 170 trackingEndpoint: 'https://test.metrics.api', 171 }) 172 client.start() 173 174 mock.timers.tick(10_000) 175 await flush() 176 177 assert.strictEqual(fetchMock.mock.callCount(), 0) 178 }) 179}) 180 181function flush() { 182 return new Promise(r => setImmediate(r)) 183}