Bluesky app fork with some witchin' additions 馃挮
at readme-update 176 lines 4.7 kB view raw
1import {MetricsClient} from './client' 2 3let appStateCallback: (state: string) => void 4 5jest.mock('#/lib/appState', () => ({ 6 onAppStateChange: jest.fn(cb => { 7 appStateCallback = cb 8 return {remove: jest.fn()} 9 }), 10})) 11 12jest.mock('#/logger', () => ({ 13 Logger: { 14 create: () => ({ 15 info: jest.fn(), 16 debug: jest.fn(), 17 error: jest.fn(), 18 }), 19 Context: {Metric: 'metric'}, 20 }, 21})) 22 23jest.mock('#/env', () => ({ 24 METRICS_API_HOST: 'https://test.metrics.api', 25 IS_WEB: false, 26})) 27 28type TestEvents = { 29 click: {button: string} 30 view: {screen: string} 31} 32 33describe('MetricsClient', () => { 34 let fetchMock: jest.Mock 35 let fetchRequests: {body: any}[] 36 37 beforeEach(() => { 38 jest.useFakeTimers({advanceTimers: true}) 39 fetchRequests = [] 40 fetchMock = jest.fn().mockImplementation(async (_url, options) => { 41 const body = JSON.parse(options.body) 42 fetchRequests.push({body}) 43 return {ok: true, status: 200} 44 }) 45 global.fetch = fetchMock 46 }) 47 48 afterEach(() => { 49 jest.useRealTimers() 50 jest.clearAllMocks() 51 }) 52 53 it('flushes events on interval', async () => { 54 const client = new MetricsClient<TestEvents>() 55 client.track('click', {button: 'submit'}) 56 client.track('view', {screen: 'home'}) 57 58 expect(fetchRequests).toHaveLength(0) 59 60 // Advance past the 10 second interval 61 await jest.advanceTimersByTimeAsync(10_000) 62 63 expect(fetchRequests).toHaveLength(1) 64 expect(fetchRequests[0].body.events).toHaveLength(2) 65 expect(fetchRequests[0].body.events[0].event).toBe('click') 66 expect(fetchRequests[0].body.events[1].event).toBe('view') 67 }) 68 69 it('flushes when maxBatchSize is exceeded', async () => { 70 const client = new MetricsClient<TestEvents>() 71 client.maxBatchSize = 5 72 73 // Add events up to maxBatchSize (should not flush yet) 74 for (let i = 0; i < 5; i++) { 75 client.track('click', {button: `btn-${i}`}) 76 } 77 78 expect(fetchRequests).toHaveLength(0) 79 80 // One more event should trigger flush (> maxBatchSize) 81 client.track('click', {button: 'btn-trigger'}) 82 83 // Allow microtasks to run 84 await jest.advanceTimersByTimeAsync(0) 85 86 expect(fetchRequests).toHaveLength(1) 87 expect(fetchRequests[0].body.events).toHaveLength(6) 88 }) 89 90 it('retries failed events once on 500 response', async () => { 91 let requestCount = 0 92 93 fetchMock.mockImplementation(async (_url, options) => { 94 requestCount++ 95 const body = JSON.parse(options.body) 96 97 if (requestCount === 1) { 98 // First request fails with 500 - "Failed to fetch" triggers isNetworkError 99 return { 100 ok: false, 101 status: 500, 102 text: async () => 'Internal Server Error', 103 } 104 } 105 106 // Retry succeeds 107 fetchRequests.push({body}) 108 return {ok: true, status: 200} 109 }) 110 111 const client = new MetricsClient<TestEvents>() 112 client.track('click', {button: 'submit'}) 113 114 // Trigger flush via interval 115 await jest.advanceTimersByTimeAsync(10_000) 116 117 expect(requestCount).toBe(1) 118 expect(fetchRequests).toHaveLength(0) 119 120 // Simulate app coming to foreground to trigger retry 121 appStateCallback('active') 122 await jest.advanceTimersByTimeAsync(0) 123 124 expect(requestCount).toBe(2) 125 expect(fetchRequests).toHaveLength(1) 126 expect(fetchRequests[0].body.events).toHaveLength(1) 127 expect(fetchRequests[0].body.events[0].event).toBe('click') 128 }) 129 130 it('does not retry more than once', async () => { 131 let requestCount = 0 132 133 fetchMock.mockImplementation(async () => { 134 requestCount++ 135 // Always fail with network-like error 136 return { 137 ok: false, 138 status: 500, 139 text: async () => 'Internal Server Error', 140 } 141 }) 142 143 const client = new MetricsClient<TestEvents>() 144 client.track('click', {button: 'submit'}) 145 146 // First flush fails 147 await jest.advanceTimersByTimeAsync(10_000) 148 149 expect(requestCount).toBe(1) 150 151 // Retry also fails 152 appStateCallback('active') 153 await jest.advanceTimersByTimeAsync(0) 154 155 expect(requestCount).toBe(2) 156 157 // Another foreground event should not retry again (events are dropped) 158 appStateCallback('active') 159 await jest.advanceTimersByTimeAsync(0) 160 161 expect(requestCount).toBe(2) // No additional requests 162 }) 163 164 it('flushes when app goes to background', async () => { 165 const client = new MetricsClient<TestEvents>() 166 client.track('click', {button: 'submit'}) 167 168 expect(fetchRequests).toHaveLength(0) 169 170 // Simulate app going to background 171 appStateCallback('background') 172 await jest.advanceTimersByTimeAsync(0) 173 174 expect(fetchRequests).toHaveLength(1) 175 }) 176})