Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 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})