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

Configure Feed

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

at main 141 lines 3.3 kB view raw
1import crypto from 'node:crypto' 2 3import {httpLogger} from './logger.js' 4 5/** 6 * New metrics events should be added here 7 */ 8type Events = { 9 redirect: { 10 link: string 11 whitelisted: 'unknown' | 'yes' 12 blocked: boolean 13 warned: boolean 14 utm_source?: string 15 utm_medium?: string 16 utm_campaign?: string 17 utm_content?: string 18 utm_term?: string 19 } 20 invalid_redirect: { 21 link: string 22 } 23} 24 25type Event<M extends Record<string, any>> = { 26 time: number 27 event: keyof M 28 payload: M[keyof M] 29 metadata: Record<string, any> 30} 31 32export type Config = { 33 trackingEndpoint?: string 34} 35 36/** 37 * This MetricsClient is duplicated from both `social-app` and `atproto` 38 * codebases. 39 */ 40export class MetricsClient<M extends Record<string, any> = Events> { 41 maxBatchSize = 100 42 43 private disabled: boolean = false 44 private started: boolean = false 45 private queue: Event<M>[] = [] 46 private flushInterval: NodeJS.Timeout | null = null 47 constructor(private config: Config) { 48 this.disabled = !config.trackingEndpoint 49 } 50 51 start() { 52 if (this.disabled) return 53 if (this.started) return 54 this.started = true 55 this.flushInterval = setInterval(() => { 56 this.flush() 57 }, 10_000) 58 } 59 60 stop() { 61 if (this.flushInterval) { 62 clearInterval(this.flushInterval) 63 this.flushInterval = null 64 } 65 this.flush() 66 } 67 68 track<E extends keyof M>(event: E, payload: M[E]) { 69 if (this.disabled) return 70 71 this.start() 72 73 /** 74 * deviceId is required for sharding events in Middleman. To avoid a hot 75 * shard, we generate a random anonymous IDs for this client. 76 * 77 * @see https://github.com/bluesky-social/tango/blob/d5819cde419d13e0d2cf837f4b30d48529d64060/middleman/handlers_tracking.go#L195 78 */ 79 const anonId = `anon-${crypto.randomUUID()}` 80 81 /** 82 * Event structure is like this to ensure compat with Middleman, which 83 * receives events like this from other codebases, including `social-app`. 84 */ 85 const e = { 86 source: 'blink', 87 time: Date.now(), 88 event, 89 payload, 90 metadata: { 91 base: { 92 deviceId: anonId, 93 sessionId: anonId, 94 }, 95 session: { 96 did: undefined, 97 }, 98 }, 99 } 100 this.queue.push(e) 101 102 if (this.queue.length > this.maxBatchSize) { 103 this.flush() 104 } 105 } 106 107 flush() { 108 if (this.disabled) return 109 if (!this.queue.length) return 110 const events = this.queue.splice(0, this.queue.length) 111 this.sendBatch(events) 112 } 113 114 private async sendBatch(events: Event<M>[]) { 115 if (this.disabled || !this.config.trackingEndpoint) return 116 117 try { 118 const res = await fetch(this.config.trackingEndpoint, { 119 method: 'POST', 120 headers: { 121 'Content-Type': 'application/json', 122 }, 123 body: JSON.stringify({events}), 124 keepalive: true, 125 }) 126 127 if (!res.ok) { 128 const errorText = await res.text().catch(() => 'Unknown error') 129 httpLogger.error( 130 {err: new Error(`${res.status} Failed to fetch - ${errorText}`)}, 131 'Failed to send metrics', 132 ) 133 } else { 134 // Drain response body to allow connection reuse. 135 await res.text().catch(() => {}) 136 } 137 } catch (err) { 138 httpLogger.error({err}, 'Failed to send metrics') 139 } 140 } 141}