mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-12 19:56:53 +00:00
📈(frontend) capture media diagnostics on media errors
Attach a media diagnostics snapshot to the room event handler for media exceptions. The snapshot captures the state of the user's setup at the moment of the error (available devices, permission state, active tracks, etc.), so support has enough context to troubleshoot user issues without asking them to reproduce.
This commit is contained in:
committed by
aleb_the_flash
parent
186d16c46f
commit
8615bf879c
@@ -8,6 +8,10 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- 📈(frontend) capture media diagnostics on media errors
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️(frontend) encapsulate error tracking behind a telemetry module
|
||||
|
||||
@@ -52,3 +52,89 @@ export const reportError = (
|
||||
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<PermissionState | 'unknown'> => {
|
||||
try {
|
||||
const status = await navigator.permissions.query({
|
||||
name: name as PermissionName,
|
||||
})
|
||||
return status.state
|
||||
} catch {
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
export const deviceSnapshot = async (): Promise<DeviceSnapshot> => {
|
||||
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',
|
||||
props: Record<string, unknown>
|
||||
) => {
|
||||
captureEvent(event, { ...props, ...(await deviceSnapshot()) })
|
||||
}
|
||||
|
||||
@@ -26,7 +26,11 @@ import { css } from '@/styled-system/css'
|
||||
import { BackgroundProcessorFactory } from '../livekit/components/blur'
|
||||
import { LocalUserChoices } from '@/stores/userChoices'
|
||||
import { MediaDeviceErrorAlert } from './MediaDeviceErrorAlert'
|
||||
import { captureEvent, reportError } from '@/features/analytics/telemetry'
|
||||
import {
|
||||
captureEvent,
|
||||
reportError,
|
||||
captureMediaEvent,
|
||||
} from '@/features/analytics/telemetry'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { isFireFox } from '@/utils/livekit'
|
||||
import { useIsMobile } from '@/utils/useIsMobile'
|
||||
@@ -235,6 +239,7 @@ export const Conference = ({
|
||||
})}
|
||||
onError={(e) => {
|
||||
reportError('livekit_room_error', e, {
|
||||
path: 'connect_publish',
|
||||
failure: MediaDeviceFailure.getFailure(e) ?? 'not-a-device-error',
|
||||
})
|
||||
}}
|
||||
@@ -297,8 +302,19 @@ export const Conference = ({
|
||||
}
|
||||
}}
|
||||
onMediaDeviceFailure={(e, kind) => {
|
||||
if (e == MediaDeviceFailure.DeviceInUse && !!kind) {
|
||||
setMediaDeviceError({ error: e, kind })
|
||||
if (!e || !kind) return
|
||||
void captureMediaEvent('media-device-error', {
|
||||
log_code: 'media_devices_error_event',
|
||||
path: 'connect_publish',
|
||||
failure: e,
|
||||
kind,
|
||||
})
|
||||
switch (e) {
|
||||
case MediaDeviceFailure.DeviceInUse:
|
||||
setMediaDeviceError({ error: e, kind })
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user