forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 💫
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}