Move to separate file

This commit is contained in:
Eric Bailey
2026-01-20 15:38:51 -06:00
parent 5b8f8bb234
commit aadc71bb3b
2 changed files with 117 additions and 107 deletions
+109
View File
@@ -0,0 +1,109 @@
import {onAppStateChange} from '#/lib/appState'
import {isNetworkError} from '#/lib/strings/errors'
import {
type Attributes,
getGrowthBook,
getGrowthBookAttributes,
} from '#/logger/growthbook'
import {type Metrics} from '#/logger/metrics/events'
import {Sentry} from '#/logger/sentry/lib'
import * as env from '#/env'
type Event<M extends Metrics> = {
time: number
event: keyof M
payload: M[keyof M]
metadata: Attributes
}
const TRACKING_ENDPOINT = env.METRICS_API_HOST + '/track'
export class MetricsClient {
private started: boolean = false
private queue: Event<Metrics>[] = []
private failedQueue: Event<Metrics>[] = []
private flushInterval: NodeJS.Timeout | null = null
start() {
if (this.started) return
if (!getGrowthBook().ready) return
this.started = true
this.flushInterval = setInterval(() => {
this.flush()
}, 10_000)
onAppStateChange(state => {
if (state === 'active') {
this.retryFailedLogs()
} else {
this.flush()
}
})
}
track<E extends keyof Metrics>(event: E, payload: Metrics[E]) {
this.start()
this.queue.push({
time: Date.now(),
event,
payload,
metadata: getGrowthBookAttributes(),
})
if (this.queue.length > 100) {
this.flush()
}
}
flush() {
if (!this.queue.length) return
const events = this.queue.splice(0, this.queue.length)
this.queue = []
this.sendBatch(events)
}
private async sendBatch(events: Event<Metrics>[], isRetry: boolean = false) {
try {
const body = JSON.stringify(events)
if (env.IS_WEB && 'navigator' in globalThis && navigator.sendBeacon) {
navigator.sendBeacon(
TRACKING_ENDPOINT,
new Blob([body], {type: 'application/json'}),
)
} else {
const res = await fetch(TRACKING_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(events),
keepalive: true,
})
if (!res.ok) {
const error = await res.text().catch(() => 'Unknown error')
// construct a "network error" for `isNetworkError` to work
throw new Error(`${res.status} Failed to fetch — ${error}`)
}
}
} catch (e: any) {
if (isNetworkError(e)) {
if (isRetry) return // retry once
this.failedQueue.push(...events)
return
}
Sentry.captureException(`Failed to send metrics`, {
extra: {
safeMessage: e.toString(),
},
})
}
}
private retryFailedLogs() {
if (!this.failedQueue.length) return
const events = this.failedQueue.splice(0, this.failedQueue.length)
this.failedQueue = []
this.sendBatch(events, true)
}
}
+8 -107
View File
@@ -1,115 +1,16 @@
import {getCurrentState, onAppStateChange} from '#/lib/appState'
import {isNetworkError} from '#/lib/strings/errors'
import {
type Attributes,
getGrowthBook,
getGrowthBookAttributes,
} from '#/logger/growthbook'
import {type Metrics} from '#/logger/metrics/events'
import {Sentry} from '#/logger/sentry/lib'
import * as env from '#/env'
import {MetricsClient} from '#/logger/metrics/client'
export {type Metrics} from '#/logger/metrics/events'
type Event<M extends Metrics> = {
time: number
event: keyof M
payload: M[keyof M]
metadata: Attributes
}
/**
* Active metrics client
*/
export const metrics = new MetricsClient()
const TRACKING_ENDPOINT = env.METRICS_API_HOST + '/track'
export const metrics = new (class Tracker {
private started: boolean = false
private queue: Event<Metrics>[] = []
private failedQueue: Event<Metrics>[] = []
private flushInterval: NodeJS.Timeout | null = null
start() {
if (this.started) return
if (!getGrowthBook().ready) return
this.started = true
this.flushInterval = setInterval(() => {
this.flush()
}, 10_000)
onAppStateChange(state => {
if (state === 'active') {
this.retryFailedLogs()
} else {
this.flush()
}
})
}
track<E extends keyof Metrics>(event: E, payload: Metrics[E]) {
this.start()
this.queue.push({
time: Date.now(),
event,
payload,
metadata: getGrowthBookAttributes(),
})
if (this.queue.length > 100) {
this.flush()
}
}
flush() {
if (!this.queue.length) return
const events = this.queue.splice(0, this.queue.length)
this.queue = []
this.sendBatch(events)
}
private async sendBatch(events: Event<Metrics>[], isRetry: boolean = false) {
try {
const body = JSON.stringify(events)
if (env.IS_WEB && 'navigator' in globalThis && navigator.sendBeacon) {
const success = navigator.sendBeacon(
TRACKING_ENDPOINT,
new Blob([body], {type: 'application/json'}),
)
console.log({success})
} else {
const res = await fetch(TRACKING_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(events),
keepalive: true,
})
if (!res.ok) {
const error = await res.text().catch(() => 'Unknown error')
// construct a "network error" for `isNetworkError` to work
throw new Error(`${res.status} Failed to fetch — ${error}`)
}
}
} catch (e: any) {
if (isNetworkError(e)) {
if (isRetry) return // retry once
this.failedQueue.push(...events)
return
}
Sentry.captureException(`Failed to send metrics`, {
extra: {
safeMessage: e.toString(),
},
})
}
}
private retryFailedLogs() {
if (!this.failedQueue.length) return
const events = this.failedQueue.splice(0, this.failedQueue.length)
this.failedQueue = []
this.sendBatch(events, true)
}
})()
/**
* Passive metrics go here
*/
let lastActive = getCurrentState() === 'active' ? performance.now() : null
onAppStateChange(state => {