Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 119 lines 3.2 kB view raw
1import {onAppStateChange} from '#/lib/appState' 2// import {isNetworkError} from '#/lib/strings/errors' 3import {Logger} from '#/logger' 4// import * as env from '#/env' 5 6type Event<M extends Record<string, any>> = { 7 source: 'app' 8 time: number 9 event: keyof M 10 payload: M[keyof M] 11 metadata: Record<string, any> 12} 13 14// const TRACKING_ENDPOINT = env.METRICS_API_HOST + '/t' 15const logger = Logger.create(Logger.Context.Metric, {}) 16 17export class MetricsClient<M extends Record<string, any>> { 18 maxBatchSize = 100 19 20 private started: boolean = false 21 private queue: Event<M>[] = [] 22 private failedQueue: Event<M>[] = [] 23 private flushInterval: NodeJS.Timeout | null = null 24 25 start() { 26 if (this.started) return 27 this.started = true 28 this.flushInterval = setInterval(() => { 29 this.flush() 30 }, 10_000) 31 onAppStateChange(state => { 32 if (state === 'active') { 33 this.retryFailedLogs() 34 } else { 35 this.flush() 36 } 37 }) 38 } 39 40 track<E extends keyof M>( 41 event: E, 42 payload: M[E], 43 metadata: Record<string, any> = {}, 44 ) { 45 this.start() 46 47 const e: Event<M> = { 48 source: 'app', 49 time: Date.now(), 50 event, 51 payload, 52 metadata, 53 } 54 this.queue.push(e) 55 56 logger.debug(`event: ${e.event as string}`, e) 57 58 if (this.queue.length > this.maxBatchSize) { 59 this.flush() 60 } 61 } 62 63 flush() { 64 if (!this.queue.length) return 65 const events = this.queue.splice(0, this.queue.length) 66 this.sendBatch(events) 67 } 68 69 private async sendBatch(events: Event<M>[], isRetry: boolean = false) { 70 logger.debug(`sendBatch: ${events.length}`, { 71 isRetry, 72 }) 73 74 // Witchsky: we don't need this :3 75 // try { 76 // const body = JSON.stringify({events}) 77 // if (env.IS_WEB && 'navigator' in globalThis && navigator.sendBeacon) { 78 // const success = navigator.sendBeacon( 79 // TRACKING_ENDPOINT, 80 // new Blob([body], {type: 'application/json'}), 81 // ) 82 // if (!success) { 83 // // construct a "network error" for `isNetworkError` to work 84 // throw new Error(`Failed to fetch: sendBeacon returned false`) 85 // } 86 // } else { 87 // const res = await fetch(TRACKING_ENDPOINT, { 88 // method: 'POST', 89 // headers: { 90 // 'Content-Type': 'application/json', 91 // }, 92 // body: JSON.stringify({events}), 93 // keepalive: true, 94 // }) 95 96 // if (!res.ok) { 97 // const error = await res.text().catch(() => 'Unknown error') 98 // // construct a "network error" for `isNetworkError` to work 99 // throw new Error(`${res.status} Failed to fetch — ${error}`) 100 // } 101 // } 102 // } catch (e: any) { 103 // if (isNetworkError(e)) { 104 // if (isRetry) return // retry once 105 // this.failedQueue.push(...events) 106 // return 107 // } 108 // logger.error(`Failed to send metrics`, { 109 // safeMessage: e.toString(), 110 // }) 111 // } 112 } 113 114 private retryFailedLogs() { 115 if (!this.failedQueue.length) return 116 const events = this.failedQueue.splice(0, this.failedQueue.length) 117 this.sendBatch(events, true) 118 } 119}