/** * WioEX Stream SDK - Anonymous Telemetry * * Collects anonymous usage statistics to improve the SDK. * Users can opt-out by setting WIOEX_TELEMETRY_DISABLED=1 */ interface TelemetryPayload { sdk_version: string; sdk_type: 'stream'; runtime: string; os: string; first_install: boolean; timestamp: number; anonymous_id: string; } export class Telemetry { private static getTelemetryUrl(baseUrl?: string): string { return `${baseUrl ?? 'https://api.wioex.com'}/sdk/usage`; } private static readonly DISABLE_ENV = 'WIOEX_TELEMETRY_DISABLED'; private static readonly SDK_VERSION = '1.7.0'; private static readonly STORAGE_KEY = 'wioex_sdk_telemetry_reported'; /** * Report first usage (fire-and-forget, never throws) */ public static reportFirstUsage(): void { // Non-blocking, fire-and-forget void this.sendTelemetry(); } /** * Send telemetry data (async, silent fail) */ private static async sendTelemetry(): Promise { try { // Check if telemetry is disabled if (this.isTelemetryDisabled()) { return; } // Check if already reported if (this.hasReported()) { return; } // Build payload const payload = this.buildPayload(); // Send to server (with timeout) await this.sendWithTimeout(payload, 5000); // Mark as reported this.markAsReported(); } catch { // Silent fail - telemetry errors should never affect SDK functionality } } /** * Check if telemetry is disabled */ private static isTelemetryDisabled(): boolean { // Check environment variable if (typeof process !== 'undefined' && process.env?.[this.DISABLE_ENV] === '1') { return true; } // Check window variable (browser) if (typeof window !== 'undefined') { type WindowWithTelemetry = typeof window & { WIOEX_TELEMETRY_DISABLED?: string }; if ((window as WindowWithTelemetry).WIOEX_TELEMETRY_DISABLED === '1') { return true; } } return false; } /** * Check if telemetry was already reported */ private static hasReported(): boolean { if (this.isBrowser()) { try { return localStorage.getItem(this.STORAGE_KEY) === '1'; } catch { return false; } } else { // Node.js: Check file in home directory try { const fs = require('fs') as typeof import('fs'); const os = require('os') as typeof import('os'); const path = require('path') as typeof import('path'); const flagFile = path.join(os.homedir(), '.wioex', this.STORAGE_KEY); return fs.existsSync(flagFile); } catch { return false; } } } /** * Mark telemetry as reported */ private static markAsReported(): void { if (this.isBrowser()) { try { localStorage.setItem(this.STORAGE_KEY, '1'); } catch { // Ignore } } else { // Node.js: Create flag file try { const fs = require('fs') as typeof import('fs'); const os = require('os') as typeof import('os'); const path = require('path') as typeof import('path'); const wioexDir = path.join(os.homedir(), '.wioex'); const flagFile = path.join(wioexDir, this.STORAGE_KEY); // Create directory if not exists if (!fs.existsSync(wioexDir)) { fs.mkdirSync(wioexDir, { recursive: true }); } // Create flag file fs.writeFileSync(flagFile, '1', 'utf-8'); } catch { // Ignore } } } /** * Build telemetry payload */ private static buildPayload(): TelemetryPayload { return { sdk_version: this.SDK_VERSION, sdk_type: 'stream', runtime: this.getRuntime(), os: this.getOS(), first_install: true, timestamp: Date.now(), anonymous_id: this.getAnonymousId(), }; } /** * Get runtime information */ private static getRuntime(): string { // Browser if (typeof window !== 'undefined' && typeof navigator !== 'undefined') { const ua = navigator.userAgent; if (ua.includes('Chrome')) return 'Browser/Chrome'; if (ua.includes('Firefox')) return 'Browser/Firefox'; if (ua.includes('Safari')) return 'Browser/Safari'; if (ua.includes('Edge')) return 'Browser/Edge'; return 'Browser/Unknown'; } // Node.js if (typeof process !== 'undefined' && process.versions?.node) { return `Node.js/${process.versions.node}`; } return 'Unknown'; } /** * Get operating system */ private static getOS(): string { // Browser if (typeof window !== 'undefined' && typeof navigator !== 'undefined') { const ua = navigator.userAgent; if (ua.includes('Windows')) return 'Windows'; if (ua.includes('Mac')) return 'macOS'; if (ua.includes('Linux')) return 'Linux'; if (ua.includes('Android')) return 'Android'; if (ua.includes('iOS') || ua.includes('iPhone') || ua.includes('iPad')) return 'iOS'; return 'Unknown'; } // Node.js if (typeof process !== 'undefined' && process.platform) { const platform = process.platform; if (platform === 'win32') return 'Windows'; if (platform === 'darwin') return 'macOS'; if (platform === 'linux') return 'Linux'; return platform; } return 'Unknown'; } /** * Generate anonymous ID (machine-based, not user-based) */ private static getAnonymousId(): string { // Simple hash based on runtime + random const seed = `${this.getRuntime()}-${this.getOS()}-${Date.now()}`; let hash = 0; for (let i = 0; i < seed.length; i++) { const char = seed.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(16).padStart(16, '0').substring(0, 16); } /** * Check if running in browser */ private static isBrowser(): boolean { return typeof window !== 'undefined' && typeof window.document !== 'undefined'; } /** * Send telemetry with timeout */ private static async sendWithTimeout(payload: TelemetryPayload, timeout: number): Promise { const controller = typeof AbortController !== 'undefined' ? new AbortController() : undefined; const timeoutId = setTimeout(() => { controller?.abort(); }, timeout); try { if (typeof fetch !== 'undefined') { // Modern fetch API const fetchOptions: RequestInit = { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': `WioEX-Stream-SDK/${this.SDK_VERSION}`, }, body: JSON.stringify(payload), }; // Add signal only if controller exists (to satisfy exactOptionalPropertyTypes) if (controller) { fetchOptions.signal = controller.signal; } const response = await fetch(this.getTelemetryUrl(), fetchOptions); // Don't throw on non-2xx status codes if (!response.ok) { // Silent fail return; } } else { // Node.js fallback (older versions without fetch) await this.sendWithHttps(payload); } } finally { clearTimeout(timeoutId); } } /** * Fallback for Node.js without fetch */ private static async sendWithHttps(payload: TelemetryPayload): Promise { const https = await import('https'); const { URL } = await import('url'); return new Promise((resolve) => { const parsedUrl = new URL(this.getTelemetryUrl()); const data = JSON.stringify(payload); const req = https.request( { hostname: parsedUrl.hostname, port: parsedUrl.port || 443, path: parsedUrl.pathname + parsedUrl.search, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data), 'User-Agent': `WioEX-Stream-SDK/${this.SDK_VERSION}`, }, timeout: 5000, }, (res) => { // Consume response data to free up memory res.on('data', () => { // Ignore }); res.on('end', () => { resolve(); }); } ); req.on('error', () => { // Silent fail resolve(); }); req.on('timeout', () => { req.destroy(); resolve(); }); req.write(data); req.end(); }); } }