🐛(telemetry) tag in-room permission failures with their own path

Since `ToggleDevice` renders on both the join screen and in the
room, `requestDevicePermission` was reporting in-room denials
through the join-preview handler, inflating the `join_preview_failure`
funnel.

Rename `onJoinPreviewError` to `onMediaPermissionError` and thread
a `path` parameter through, derived from `ToggleDevice`'s existing
`context` prop. In-room failures are now reported under a new
`room_media_failure` code, keeping `join_preview_failure` intact
for existing dashboards.
This commit is contained in:
lebaudantoine
2026-08-11 20:38:11 +02:00
committed by aleb_the_flash
parent 7d1ce5f215
commit ff7a1a4f33
3 changed files with 27 additions and 12 deletions
@@ -19,6 +19,7 @@ export const captureEvent = (
export type LogCode = export type LogCode =
// media // media
| 'join_preview_failure' | 'join_preview_failure'
| 'room_media_failure'
| 'livekit_room_error' | 'livekit_room_error'
| 'device_switch_failure' | 'device_switch_failure'
| 'permission_poll_failure' | 'permission_poll_failure'
@@ -121,7 +121,10 @@ export const ToggleDevice = <T extends ToggleSource>({
if (isRequestingPermission.current) return if (isRequestingPermission.current) return
isRequestingPermission.current = true isRequestingPermission.current = true
try { try {
const granted = await requestDevicePermission(kind) const granted = await requestDevicePermission(
kind,
context === 'join' ? 'join_preview' : 'room'
)
if (granted) { if (granted) {
toggle() toggle()
} else { } else {
@@ -44,7 +44,13 @@ const PERMISSION_KIND: Record<'audioinput' | 'videoinput', PermissionKind> = {
videoinput: 'camera', videoinput: 'camera',
} }
export const onJoinPreviewError = (e: Error, kind?: PermissionKind) => { type MediaPath = 'join_preview' | 'room'
const onMediaPermissionError = (
e: Error,
kind?: PermissionKind,
path: MediaPath = 'join_preview'
) => {
if ( if (
MediaDeviceFailure.getFailure(e) === MediaDeviceFailure.PermissionDenied MediaDeviceFailure.getFailure(e) === MediaDeviceFailure.PermissionDenied
) { ) {
@@ -55,7 +61,7 @@ export const onJoinPreviewError = (e: Error, kind?: PermissionKind) => {
notePermissionDeniedFromGum(kind) notePermissionDeniedFromGum(kind)
} }
captureMediaEvent('permissions-denied', { captureMediaEvent('permissions-denied', {
path: 'join_preview', path,
kind, kind,
denied_scope: scope, denied_scope: scope,
os: getOS(), os: getOS(),
@@ -71,20 +77,24 @@ export const onJoinPreviewError = (e: Error, kind?: PermissionKind) => {
if (system) { if (system) {
noteSystemPermissionDenied(kind) noteSystemPermissionDenied(kind)
captureMediaEvent('permissions-denied', { captureMediaEvent('permissions-denied', {
path: 'join_preview', path,
kind, kind,
denied_scope: 'system', denied_scope: 'system',
os: getOS(), os: getOS(),
}) })
return return
} }
captureMediaEvent('device-not-found', { path: 'join_preview', kind }) captureMediaEvent('device-not-found', { path, kind })
}) })
return return
} }
// "Other" and "Device in use" are still reported as errors, as they are not handled on the join screen. // "Other" and "Device in use" are still reported as errors, as they are not handled on the join screen.
reportError('join_preview_failure', e, { path: 'join_preview', kind }) reportError(
path === 'room' ? 'room_media_failure' : 'join_preview_failure',
e,
{ path, kind }
)
} }
// Module-level: effect dependencies, must be referentially stable. // Module-level: effect dependencies, must be referentially stable.
@@ -95,7 +105,8 @@ const stopAll = (stream: MediaStream) =>
stream.getTracks().forEach((track) => track.stop()) stream.getTracks().forEach((track) => track.stop())
export const requestDevicePermission = async ( export const requestDevicePermission = async (
kind: 'audioinput' | 'videoinput' kind: 'audioinput' | 'videoinput',
path: MediaPath = 'join_preview'
): Promise<boolean> => { ): Promise<boolean> => {
try { try {
const track = const track =
@@ -106,7 +117,7 @@ export const requestDevicePermission = async (
noteGumSuccess(PERMISSION_KIND[kind]) noteGumSuccess(PERMISSION_KIND[kind])
return true return true
} catch (error) { } catch (error) {
onJoinPreviewError(error as Error, PERMISSION_KIND[kind]) onMediaPermissionError(error as Error, PERMISSION_KIND[kind], path)
return false return false
} }
} }
@@ -156,7 +167,7 @@ function useWarmupPermissions(): WarmupState {
!isSystemPermissionError(error) !isSystemPermissionError(error)
) { ) {
// Retrying after a dismissal would show a second dialog. // Retrying after a dismissal would show a second dialog.
onJoinPreviewError(error as Error) onMediaPermissionError(error as Error)
bothReady() bothReady()
return return
} }
@@ -171,7 +182,7 @@ function useWarmupPermissions(): WarmupState {
stopAll(stream) stopAll(stream)
noteGumSuccess('microphone') noteGumSuccess('microphone')
}) })
.catch((e) => onJoinPreviewError(e as Error, 'microphone')) .catch((e) => onMediaPermissionError(e as Error, 'microphone'))
.finally(() => .finally(() =>
setState((current) => ({ ...current, audioReady: true })) setState((current) => ({ ...current, audioReady: true }))
) )
@@ -181,7 +192,7 @@ function useWarmupPermissions(): WarmupState {
stopAll(stream) stopAll(stream)
noteGumSuccess('camera') noteGumSuccess('camera')
}) })
.catch((e) => onJoinPreviewError(e as Error, 'camera')) .catch((e) => onMediaPermissionError(e as Error, 'camera'))
.finally(() => .finally(() =>
setState((current) => ({ ...current, videoReady: true })) setState((current) => ({ ...current, videoReady: true }))
) )
@@ -224,7 +235,7 @@ function useLocalTrack<T extends LocalAudioTrack | LocalVideoTrack>({
setTrack(newTrack) setTrack(newTrack)
}) })
.catch((error) => { .catch((error) => {
onJoinPreviewError(error as Error, permissionKind) onMediaPermissionError(error as Error, permissionKind)
onFailure() onFailure()
}) })
return () => { return () => {