CoolFace
Apppublic

Leon4gr45/builder

sourceHugging Facemitupdated 2d agoView on Hugging Face
0likes
tracker.ts223 linesDownload Raw Back to telemetry
1import { TelemetryEvent, TelemetryEventName, TelemetryEventProperties } from './events';2import {3  TELEMETRY_ENDPOINT,4  TELEMETRY_TOKEN,5  TELEMETRY_ENABLED,6  TELEMETRY_DEBUG,7  FLUSH_INTERVAL_MS,8  MAX_BATCH_SIZE,9  MAX_RETRIES,10  RETRY_BASE_MS,11  HEARTBEAT_INTERVAL_MS,12  detectDeploymentType,13  getAppVersion,14  detectOsPlatform,15  getManagedContext,16} from './config';17import { configManager } from '@/lib/config/storage';18 19const VISITOR_ID_KEY = 'osw-telemetry-vid';20 21function getOrCreateVisitorId(): string {22  try {23    let id = localStorage.getItem(VISITOR_ID_KEY);24    if (!id) {25      id = crypto.randomUUID();26      localStorage.setItem(VISITOR_ID_KEY, id);27    }28    return id;29  } catch {30    return 'unknown';31  }32}33 34export class TelemetryTracker {35  private queue: TelemetryEvent[] = [];36  private flushTimer: ReturnType<typeof setInterval> | null = null;37  private heartbeatTimer: ReturnType<typeof setInterval> | null = null;38  private optedIn = true;39  private initialized = false;40  private sessionStartTime = 0;41  private flushing = false;42  private visitorId = 'unknown';43  private deploymentType = 'browser';44  private osPlatform = 'unknown';45  private appVersion = 'unknown';46  private managedContext: Record<string, string> | null = null;47 48  init(): void {49    try {50      if (typeof window === 'undefined') return;51      if (this.initialized) return;52      if (!TELEMETRY_ENABLED) return;53 54      this.optedIn = configManager.getSettings().telemetryOptIn !== false;55      this.visitorId = getOrCreateVisitorId();56      this.deploymentType = detectDeploymentType();57      this.osPlatform = detectOsPlatform();58      this.appVersion = getAppVersion();59      this.managedContext = getManagedContext();60      this.sessionStartTime = Date.now();61      this.initialized = true;62 63      this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);64 65      this.heartbeatTimer = setInterval(() => {66        if (document.visibilityState === 'visible') {67          this.track('heartbeat', { uptime_ms: Date.now() - this.sessionStartTime });68        }69      }, HEARTBEAT_INTERVAL_MS);70 71      window.addEventListener('beforeunload', this.handleUnload);72      document.addEventListener('visibilitychange', this.handleVisibility);73 74      this.debug('Telemetry initialized', { optedIn: this.optedIn });75    } catch {76      // silently ignore77    }78  }79 80  track(event: TelemetryEventName, properties?: TelemetryEventProperties): void {81    try {82      if (!this.initialized || !this.optedIn || !TELEMETRY_ENABLED) return;83 84      const entry: TelemetryEvent = {85        event,86        timestamp: Date.now(),87        fields: {88          vid: this.visitorId,89          deployment_type: this.deploymentType,90          os_platform: this.osPlatform,91          app_version: this.appVersion,92          ...(this.managedContext ?? {}),93          ...(properties ?? {}),94        },95      };96 97      this.queue.push(entry);98      this.debug('track', entry);99 100      if (this.queue.length >= MAX_BATCH_SIZE) {101        this.flush();102      }103    } catch {104      // silently ignore105    }106  }107 108  setOptIn(value: boolean): void {109    try {110      if (!value && this.optedIn) {111        this.track('telemetry_disabled');112        if (this.flushing) {113          this.beaconFlush();114        } else {115          this.flush();116        }117      }118      this.optedIn = value;119      configManager.setSetting('telemetryOptIn', value);120      if (!value) {121        this.queue = [];122        try { localStorage.removeItem(VISITOR_ID_KEY); } catch {}123        this.visitorId = 'unknown';124      } else {125        this.visitorId = getOrCreateVisitorId();126      }127    } catch {128      // silently ignore129    }130  }131 132  async flush(): Promise<void> {133    try {134      if (this.queue.length === 0 || this.flushing) return;135      this.flushing = true;136 137      const batch = this.queue.splice(0);138      let attempt = 0;139      let success = false;140 141      while (attempt < MAX_RETRIES && !success) {142        try {143          const headers: Record<string, string> = { 'Content-Type': 'application/json' };144          if (TELEMETRY_TOKEN) {145            headers['Authorization'] = `Bearer ${TELEMETRY_TOKEN}`;146          }147          const res = await fetch(TELEMETRY_ENDPOINT, {148            method: 'POST',149            headers,150            body: JSON.stringify({ events: batch }),151            credentials: 'omit',152          });153          if (res.ok) {154            success = true;155            this.debug(`Flushed ${batch.length} events`);156          } else {157            attempt++;158            if (attempt < MAX_RETRIES) {159              await this.sleep(RETRY_BASE_MS * Math.pow(2, attempt - 1));160            }161          }162        } catch {163          attempt++;164          if (attempt < MAX_RETRIES) {165            await this.sleep(RETRY_BASE_MS * Math.pow(2, attempt - 1));166          }167        }168      }169 170      if (!success) {171        this.debug(`Dropped ${batch.length} events after ${MAX_RETRIES} retries`);172      }173    } catch {174      // silently ignore175    } finally {176      this.flushing = false;177    }178  }179 180  private handleUnload = () => {181    this.beaconFlush();182  };183 184  private handleVisibility = () => {185    if (document.visibilityState === 'hidden') {186      this.beaconFlush();187    }188  };189 190  private beaconFlush(): void {191    if (this.queue.length === 0) return;192    try {193      const body: Record<string, unknown> = { events: this.queue.splice(0) };194      if (TELEMETRY_TOKEN) {195        body.token = TELEMETRY_TOKEN;196      }197      const json = JSON.stringify(body);198      // Use fetch with keepalive instead of sendBeacon to avoid CORS199      // issues (sendBeacon sends with credentials: 'include' by default,200      // which is incompatible with Access-Control-Allow-Origin: *)201      fetch(TELEMETRY_ENDPOINT, {202        method: 'POST',203        headers: { 'Content-Type': 'application/json' },204        body: json,205        keepalive: true,206      }).catch(() => {});207    } catch {208      // silently ignore209    }210  }211 212  private sleep(ms: number): Promise<void> {213    return new Promise((resolve) => setTimeout(resolve, ms));214  }215 216  private debug(...args: unknown[]): void {217    if (TELEMETRY_DEBUG) {218      // eslint-disable-next-line no-console219      console.debug('[telemetry]', ...args);220    }221  }222}223