import { getPosthog } from './utils' export const captureEvent = ( event: string, props?: Record ) => { void getPosthog() .then((ph) => { ph.capture(event, props) }) .catch(() => { /* telemetry must never break the app */ }) if (import.meta.env.DEV) { console.warn(`[telemetry] ${event}`, props) } } export type LogCode = // media | 'join_preview_failure' | 'room_media_failure' | 'livekit_room_error' | 'device_switch_failure' | 'permission_poll_failure' | 'media_devices_error_event' // non-media families | 'participant_mute_api_failure' | 'permissions_api_failure' | 'effects_processor_failure' | 'clipboard_failure' | 'fullscreen_failure' | 'publish_sources_failure' | 'disconnect_failure' | 'generic_failure' export const reportError = ( logCode: LogCode, error: unknown, extraInfo: Record = {} ): void => { const e = error instanceof Error ? error : new Error(String(error)) void getPosthog() .then((ph) => { ph.captureException(e, { log_code: logCode, error_name: e.name, error_message: e.message, ...extraInfo, }) }) .catch(() => {}) if (import.meta.env.DEV) { console.warn(`[${logCode}]`, e, extraInfo) } } export interface DeviceSnapshot { cam_count: number mic_count: number out_count: number labels_visible: boolean saved_cam_present: boolean | null saved_mic_present: boolean | null saved_video_device_id_set: boolean saved_audio_device_id_set: boolean audio_enabled: boolean | null video_enabled: boolean | null cam_permission: PermissionState | 'unknown' mic_permission: PermissionState | 'unknown' } /** Reads the persisted LiveKit user choices without importing the store. */ const readPersistedChoices = (): { videoDeviceId?: string audioDeviceId?: string videoEnabled?: boolean audioEnabled?: boolean } => { try { return JSON.parse(localStorage.getItem('lk-user-choices') ?? '{}') } catch { return {} } } const queryPermission = async ( name: 'camera' | 'microphone' ): Promise => { try { const status = await navigator.permissions.query({ name: name as PermissionName, }) return status.state } catch { return 'unknown' } } export const deviceSnapshot = async (): Promise => { const choices = readPersistedChoices() let devices: MediaDeviceInfo[] = [] try { devices = await navigator.mediaDevices.enumerateDevices() } catch { /* snapshot stays partial */ } const ofKind = (k: MediaDeviceKind) => devices.filter((d) => d.kind === k) const present = (k: MediaDeviceKind, id?: string) => id ? ofKind(k).some((d) => d.deviceId === id) : null const [cam_permission, mic_permission] = await Promise.all([ queryPermission('camera'), queryPermission('microphone'), ]) return { cam_count: ofKind('videoinput').length, mic_count: ofKind('audioinput').length, out_count: ofKind('audiooutput').length, labels_visible: devices.some((d) => !!d.label), saved_cam_present: present('videoinput', choices.videoDeviceId), saved_mic_present: present('audioinput', choices.audioDeviceId), saved_video_device_id_set: !!choices.videoDeviceId, saved_audio_device_id_set: !!choices.audioDeviceId, audio_enabled: choices.audioEnabled ?? null, video_enabled: choices.videoEnabled ?? null, cam_permission, mic_permission, } } export const captureMediaEvent = async ( event: | 'media-device-error' | 'media-acquisition' | 'media-device-topology' | 'media-device-success' | 'device-not-found' | 'device-in-use' | 'permissions-denied' | 'screen-share-permission-denied' | 'silent-mic-detected' | 'silent-mic-analyser-unavailable' | 'silent-mic-recovered' | 'visit-room' | 'connection-event', props: Record ) => { captureEvent(event, { ...props, ...(await deviceSnapshot()) }) }