From aadc71bb3b1fac3590bc2a00dcb23aca7b5512f0 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 20 Jan 2026 15:38:51 -0600 Subject: [PATCH] Move to separate file --- src/logger/metrics/client.ts | 109 +++++++++++++++++++++++++++++++++ src/logger/metrics/index.ts | 115 +++-------------------------------- 2 files changed, 117 insertions(+), 107 deletions(-) create mode 100644 src/logger/metrics/client.ts diff --git a/src/logger/metrics/client.ts b/src/logger/metrics/client.ts new file mode 100644 index 0000000000..86f984ecb0 --- /dev/null +++ b/src/logger/metrics/client.ts @@ -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 = { + 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[] = [] + private failedQueue: Event[] = [] + 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(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[], 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) + } +} diff --git a/src/logger/metrics/index.ts b/src/logger/metrics/index.ts index 22bb2d946c..74e4774857 100644 --- a/src/logger/metrics/index.ts +++ b/src/logger/metrics/index.ts @@ -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 = { - 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[] = [] - private failedQueue: Event[] = [] - 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(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[], 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 => {