wip encapsulate exception capture

This commit is contained in:
lebaudantoine
2026-08-07 15:02:28 +02:00
parent 46a4955fe0
commit 993efec027
42 changed files with 221 additions and 85 deletions
+5 -1
View File
@@ -2,6 +2,7 @@ import { Button } from '@/primitives'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useMediaDeviceSelect } from '@livekit/components-react' import { useMediaDeviceSelect } from '@livekit/components-react'
import { reportError } from '@/features/analytics/telemetry'
export const SoundTester = () => { export const SoundTester = () => {
const { t } = useTranslation('settings') const { t } = useTranslation('settings')
@@ -15,7 +16,10 @@ export const SoundTester = () => {
try { try {
await audioRef?.current?.setSinkId(deviceId) await audioRef?.current?.setSinkId(deviceId)
} catch (error) { } catch (error) {
console.error(`Error setting sinkId: ${error}`) reportError(
'device_switch_failure',
new Error(`Error setting sinkId: ${error}`)
)
} }
} }
updateActiveId(activeDeviceId) updateActiveId(activeDeviceId)
@@ -1,15 +1,8 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { useLocation } from 'wouter' import { useLocation } from 'wouter'
import { type PostHog } from 'posthog-js'
import { type ApiUser } from '@/features/auth/api/ApiUser' import { type ApiUser } from '@/features/auth/api/ApiUser'
import { useUser } from '@/features/auth/api/useUser' import { useUser } from '@/features/auth/api/useUser'
import { getPosthog } from '../utils'
let posthog: PostHog | null = null
const getPosthog = async () => {
if (!posthog) posthog = (await import('posthog-js')).default
return posthog
}
export const startAnalyticsSession = (data: ApiUser) => { export const startAnalyticsSession = (data: ApiUser) => {
getPosthog().then((ph) => { getPosthog().then((ph) => {
@@ -0,0 +1,38 @@
import { getPosthog } from './utils'
export type LogCode =
// media
| 'join_preview_failure'
| 'livekit_room_error'
| 'device_switch_failure'
| 'permission_poll_failure'
// 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<string, unknown> = {}
): 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)
}
}
@@ -0,0 +1,8 @@
import type { PostHog } from 'posthog-js'
let posthog: PostHog | null = null
export const getPosthog = async () => {
if (!posthog) posthog = (await import('posthog-js')).default
return posthog
}
@@ -15,6 +15,7 @@ import { css } from '@/styled-system/css'
import { useConfig } from '@/api/useConfig' import { useConfig } from '@/api/useConfig'
import { LoginButton } from '@/components/LoginButton' import { LoginButton } from '@/components/LoginButton'
import { LoadingScreen } from '@/components/LoadingScreen' import { LoadingScreen } from '@/components/LoadingScreen'
import { reportError } from '@/features/analytics/telemetry'
const Columns = ({ children }: { children?: ReactNode }) => { const Columns = ({ children }: { children?: ReactNode }) => {
return ( return (
@@ -160,7 +161,9 @@ const Home = () => {
window.location.replace(data.external_home_url) window.location.replace(data.external_home_url)
} catch (error) { } catch (error) {
setRedirectFailed(true) setRedirectFailed(true)
console.error('Site is not reachable:', error) reportError('generic_failure', error, {
context: 'Site is not reachable:',
})
} }
} }
} }
@@ -4,6 +4,7 @@ import { NotificationDuration } from './NotificationDuration'
import type { Participant } from 'livekit-client' import type { Participant } from 'livekit-client'
import type { NotificationPayload } from './NotificationPayload' import type { NotificationPayload } from './NotificationPayload'
import type { RecordingMode } from '@/features/recording' import type { RecordingMode } from '@/features/recording'
import { reportError } from '@/features/analytics/telemetry'
export const notifyAutoMutedOnJoin = () => { export const notifyAutoMutedOnJoin = () => {
toastQueue.add( toastQueue.add(
@@ -55,7 +56,9 @@ export const decodeNotificationDataReceived = (
return parsed as NotificationPayload return parsed as NotificationPayload
} catch (error) { } catch (error) {
// Handle errors appropriately for your application // Handle errors appropriately for your application
console.error('Failed to decode notification payload:', error) reportError('generic_failure', error, {
context: 'Failed to decode notification payload:',
})
return return
} }
} }
@@ -1,5 +1,6 @@
import type { Participant } from 'livekit-client' import type { Participant } from 'livekit-client'
import { useLowerHandParticipant } from './lowerHandParticipant' import { useLowerHandParticipant } from './lowerHandParticipant'
import { reportError } from '@/features/analytics/telemetry'
export const useLowerHandParticipants = () => { export const useLowerHandParticipants = () => {
const { lowerHandParticipant } = useLowerHandParticipant() const { lowerHandParticipant } = useLowerHandParticipant()
@@ -11,7 +12,9 @@ export const useLowerHandParticipants = () => {
) )
return Promise.all(promises) return Promise.all(promises)
} catch (error) { } catch (error) {
console.error('An error occurred while lowering hands :', error) reportError('generic_failure', error, {
context: 'An error occurred while lowering hands :',
})
throw new Error('An error occurred while lowering hands.', { throw new Error('An error occurred while lowering hands.', {
cause: error, cause: error,
}) })
@@ -1,6 +1,7 @@
import { fetchApi } from '@/api/fetchApi' import { fetchApi } from '@/api/fetchApi'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { AssignableParticipantRole } from '@/features/rooms/api/ApiRoom' import { AssignableParticipantRole } from '@/features/rooms/api/ApiRoom'
import { reportError } from '@/features/analytics/telemetry'
export const useParticipantRole = () => { export const useParticipantRole = () => {
const data = useRoomData() const data = useRoomData()
@@ -22,8 +23,11 @@ export const useParticipantRole = () => {
}), }),
}) })
} catch (error) { } catch (error) {
console.error( reportError(
`Failed to update participant's role ${identity}: ${error instanceof Error ? error.message : 'Unknown error'}` 'generic_failure',
new Error(
`Failed to update participant's role ${identity}: ${error instanceof Error ? error.message : 'Unknown error'}`
)
) )
} }
} }
@@ -10,6 +10,7 @@ import {
} from '../../participants/api/listWaitingParticipants' } from '../../participants/api/listWaitingParticipants'
import { decodeNotificationDataReceived } from '@/features/notifications/utils' import { decodeNotificationDataReceived } from '@/features/notifications/utils'
import { NotificationType } from '@/features/notifications/NotificationType' import { NotificationType } from '@/features/notifications/NotificationType'
import { reportError } from '@/features/analytics/telemetry'
export const POLL_INTERVAL_MS = 1000 export const POLL_INTERVAL_MS = 1000
@@ -87,7 +88,7 @@ export const useWaitingParticipants = () => {
await refetchWaiting() await refetchWaiting()
} catch (e) { } catch (e) {
console.error(e) reportError('generic_failure', e)
setListEnabled(true) setListEnabled(true)
} }
} }
@@ -3,6 +3,7 @@ import { useCallback, useMemo } from 'react'
import { flushSync } from 'react-dom' import { flushSync } from 'react-dom'
import { documentPictureInPictureStore } from '@/stores/documentPictureInPicture' import { documentPictureInPictureStore } from '@/stores/documentPictureInPicture'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { reportError } from '@/features/analytics/telemetry'
export const IS_PIP_SUPPORTED = export const IS_PIP_SUPPORTED =
typeof globalThis !== 'undefined' && 'documentPictureInPicture' in globalThis typeof globalThis !== 'undefined' && 'documentPictureInPicture' in globalThis
@@ -86,7 +87,9 @@ export const usePictureInPicture = () => {
documentPictureInPictureStore.window = ref(pipWindow) documentPictureInPictureStore.window = ref(pipWindow)
} catch (error) { } catch (error) {
// Avoid unhandled rejections if the user blocks or closes the request. // Avoid unhandled rejections if the user blocks or closes the request.
console.error('Failed to open Picture-in-Picture window', error) reportError('generic_failure', error, {
context: 'Failed to open Picture-in-Picture window',
})
return null return null
} }
}, },
@@ -29,6 +29,7 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner' import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import { FeatureFlags } from '@/features/analytics/enums' import { FeatureFlags } from '@/features/analytics/enums'
import { LimitDescription } from './LimitDescription' import { LimitDescription } from './LimitDescription'
import { reportError } from '@/features/analytics/telemetry'
export const ScreenRecordingSidePanel = () => { export const ScreenRecordingSidePanel = () => {
const { data } = useConfig() const { data } = useConfig()
@@ -106,7 +107,9 @@ export const ScreenRecordingSidePanel = () => {
}) })
} }
} catch (error) { } catch (error) {
console.error('Failed to handle recording:', error) reportError('generic_failure', error, {
context: 'Failed to handle recording:',
})
} }
} }
@@ -35,6 +35,7 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner' import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import { LimitDescription } from './LimitDescription' import { LimitDescription } from './LimitDescription'
import { openSettingsDialog } from '@/stores/settings' import { openSettingsDialog } from '@/stores/settings'
import { reportError } from '@/features/analytics/telemetry'
export const TranscriptSidePanel = () => { export const TranscriptSidePanel = () => {
const { data } = useConfig() const { data } = useConfig()
@@ -127,7 +128,9 @@ export const TranscriptSidePanel = () => {
}) })
} }
} catch (error) { } catch (error) {
console.error('Failed to handle transcript:', error) reportError('generic_failure', error, {
context: 'Failed to handle transcript:',
})
} }
} }
@@ -1,5 +1,6 @@
import { useRoomInfo } from '@livekit/components-react' import { useRoomInfo } from '@livekit/components-react'
import { useMemo } from 'react' import { useMemo } from 'react'
import { reportError } from '@/features/analytics/telemetry'
export const useRoomMetadata = () => { export const useRoomMetadata = () => {
const { metadata } = useRoomInfo() const { metadata } = useRoomInfo()
@@ -8,7 +9,9 @@ export const useRoomMetadata = () => {
try { try {
return JSON.parse(metadata) return JSON.parse(metadata)
} catch (error) { } catch (error) {
console.error('Failed to parse room metadata:', error) reportError('generic_failure', error, {
context: 'Failed to parse room metadata:',
})
return undefined return undefined
} }
} else { } else {
@@ -9,6 +9,7 @@ import { fetchApi } from '@/api/fetchApi'
import { useIsAdminOrOwner } from '../livekit/hooks/useIsAdminOrOwner' import { useIsAdminOrOwner } from '../livekit/hooks/useIsAdminOrOwner'
import { useCallback } from 'react' import { useCallback } from 'react'
import { reportError } from '@/features/analytics/telemetry'
export const useMuteParticipant = () => { export const useMuteParticipant = () => {
const apiRoomData = useRoomData() const apiRoomData = useRoomData()
@@ -31,7 +32,10 @@ export const useMuteParticipant = () => {
// Guard against undefined token for non-admin users // Guard against undefined token for non-admin users
if (!isAdminOrOwner && !apiRoomData.livekit.token) { if (!isAdminOrOwner && !apiRoomData.livekit.token) {
console.error('Cannot mute participant: missing auth token') reportError(
'participant_mute_api_failure',
new Error('Cannot mute participant: missing auth token')
)
return return
} }
@@ -53,8 +57,11 @@ export const useMuteParticipant = () => {
} }
) )
} catch (error) { } catch (error) {
console.error( reportError(
`Failed to mute participant ${participant.identity}: ${error instanceof Error ? error.message : 'Unknown error'}` 'participant_mute_api_failure',
new Error(
`Failed to mute participant ${participant.identity}: ${error instanceof Error ? error.message : 'Unknown error'}`
)
) )
return return
} }
@@ -65,8 +72,11 @@ export const useMuteParticipant = () => {
destinationIdentities: [participant.identity], destinationIdentities: [participant.identity],
}) })
} catch (e) { } catch (e) {
console.error( reportError(
`Failed to notify muted participant ${participant.identity}: ${e}` 'participant_mute_api_failure',
new Error(
`Failed to notify muted participant ${participant.identity}: ${e}`
)
) )
} }
@@ -1,5 +1,6 @@
import type { Participant } from 'livekit-client' import type { Participant } from 'livekit-client'
import { useMuteParticipant } from './muteParticipant' import { useMuteParticipant } from './muteParticipant'
import { reportError } from '@/features/analytics/telemetry'
export const useMuteParticipants = () => { export const useMuteParticipants = () => {
const { muteParticipant } = useMuteParticipant() const { muteParticipant } = useMuteParticipant()
@@ -11,7 +12,9 @@ export const useMuteParticipants = () => {
) )
return Promise.all(promises) return Promise.all(promises)
} catch (error) { } catch (error) {
console.error('An error occurred while muting participants :', error) reportError('participant_mute_api_failure', error, {
context: 'An error occurred while muting participants :',
})
throw new Error('An error occurred while muting participants.', { throw new Error('An error occurred while muting participants.', {
cause: error, cause: error,
}) })
@@ -1,6 +1,7 @@
import type { Participant, Track } from 'livekit-client' import type { Participant, Track } from 'livekit-client'
import { fetchApi } from '@/api/fetchApi' import { fetchApi } from '@/api/fetchApi'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { reportError } from '@/features/analytics/telemetry'
type Source = Track.Source type Source = Track.Source
export const useParticipantPermissions = () => { export const useParticipantPermissions = () => {
@@ -32,8 +33,11 @@ export const useParticipantPermissions = () => {
}), }),
}) })
} catch (error) { } catch (error) {
console.error( reportError(
`Failed to update participant's permissions ${participant.identity}: ${error instanceof Error ? error.message : 'Unknown error'}` 'permissions_api_failure',
new Error(
`Failed to update participant's permissions ${participant.identity}: ${error instanceof Error ? error.message : 'Unknown error'}`
)
) )
} }
} }
@@ -1,5 +1,6 @@
import type { Participant, Track } from 'livekit-client' import type { Participant, Track } from 'livekit-client'
import { useParticipantPermissions } from './updateParticipantPermissions' import { useParticipantPermissions } from './updateParticipantPermissions'
import { reportError } from '@/features/analytics/telemetry'
type Source = Track.Source type Source = Track.Source
export const useUpdateParticipantsPermissions = () => { export const useUpdateParticipantsPermissions = () => {
@@ -15,7 +16,9 @@ export const useUpdateParticipantsPermissions = () => {
) )
return Promise.all(promises) return Promise.all(promises)
} catch (error) { } catch (error) {
console.error('An error occurred while updating permissions :', error) reportError('permissions_api_failure', error, {
context: 'An error occurred while updating permissions :',
})
throw new Error('An error occurred while updating permissions.', { throw new Error('An error occurred while updating permissions.', {
cause: error, cause: error,
}) })
@@ -26,6 +26,7 @@ import { css } from '@/styled-system/css'
import { BackgroundProcessorFactory } from '../livekit/components/blur' import { BackgroundProcessorFactory } from '../livekit/components/blur'
import { LocalUserChoices } from '@/stores/userChoices' import { LocalUserChoices } from '@/stores/userChoices'
import { MediaDeviceErrorAlert } from './MediaDeviceErrorAlert' import { MediaDeviceErrorAlert } from './MediaDeviceErrorAlert'
import { reportError } from '@/features/analytics/telemetry'
import { usePostHog } from 'posthog-js/react' import { usePostHog } from 'posthog-js/react'
import { useConfig } from '@/api/useConfig' import { useConfig } from '@/api/useConfig'
import { isFireFox } from '@/utils/livekit' import { isFireFox } from '@/utils/livekit'
@@ -37,7 +38,6 @@ import { notifyAutoMutedOnJoin } from '@/features/notifications/utils'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { userPreferencesStore } from '@/stores/userPreferences' import { userPreferencesStore } from '@/stores/userPreferences'
import { userStore } from '@/stores/user' import { userStore } from '@/stores/user'
import { asError } from '../utils/error'
export const Conference = ({ export const Conference = ({
roomId, roomId,
@@ -236,7 +236,9 @@ export const Conference = ({
backgroundColor: 'primaryDark.50 !important', backgroundColor: 'primaryDark.50 !important',
})} })}
onError={(e) => { onError={(e) => {
posthog.captureException(asError(e)) reportError('livekit_room_error', e, {
failure: MediaDeviceFailure.getFailure(e) ?? 'not-a-device-error',
})
}} }}
onConnected={async () => { onConnected={async () => {
if (!apiConfig) return if (!apiConfig) return
@@ -36,6 +36,7 @@ import { useLoginHint } from '@/hooks/useLoginHint'
import { openPermissionsDialog } from '@/stores/permissions' import { openPermissionsDialog } from '@/stores/permissions'
import { useResolveInitiallyDefaultDeviceId } from '../livekit/hooks/useResolveInitiallyDefaultDeviceId' import { useResolveInitiallyDefaultDeviceId } from '../livekit/hooks/useResolveInitiallyDefaultDeviceId'
import { isSafari } from '@/utils/livekit' import { isSafari } from '@/utils/livekit'
import { reportError } from '@/features/analytics/telemetry'
import { import {
type LocalUserChoices, type LocalUserChoices,
@@ -54,7 +55,8 @@ import { useSnapshot } from 'valtio'
import { useUser } from '@/features/auth/api/useUser' import { useUser } from '@/features/auth/api/useUser'
import { useConfig } from '@/api/useConfig' import { useConfig } from '@/api/useConfig'
const onError = (e: Error) => console.error('ERROR', e) const onError = (e: Error) =>
reportError('join_preview_failure', e, { path: 'join_preview' })
const Effects = ({ const Effects = ({
videoTrack, videoTrack,
@@ -1,6 +1,7 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { permissionsStore } from '@/stores/permissions' import { permissionsStore } from '@/stores/permissions'
import { isSafari } from '@/utils/livekit' import { isSafari } from '@/utils/livekit'
import { reportError } from '@/features/analytics/telemetry'
const POLLING_TIME = 500 const POLLING_TIME = 500
@@ -88,7 +89,9 @@ export const useWatchPermissions = () => {
} }
} catch (error) { } catch (error) {
if (!isCancelled) { if (!isCancelled) {
console.error('Error polling permissions:', error) reportError('permission_poll_failure', error, {
context: 'Error polling permissions:',
})
} }
} }
}, POLLING_TIME) }, POLLING_TIME)
@@ -142,7 +145,9 @@ export const useWatchPermissions = () => {
} }
} catch (error) { } catch (error) {
if (!isCancelled) { if (!isCancelled) {
console.error('Error checking permissions:', error) reportError('permission_poll_failure', error, {
context: 'Error checking permissions:',
})
} }
} finally { } finally {
if (!isCancelled) { if (!isCancelled) {
@@ -13,6 +13,7 @@ import { usePermissionsManager } from '../hooks/usePermissionsManager'
import { useEffect } from 'react' import { useEffect } from 'react'
import { closeSidePanel } from '@/stores/layout' import { closeSidePanel } from '@/stores/layout'
import { useIsAdminOrOwner } from '../hooks/useIsAdminOrOwner' import { useIsAdminOrOwner } from '../hooks/useIsAdminOrOwner'
import { reportError } from '@/features/analytics/telemetry'
export const Admin = () => { export const Admin = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'admin' }) const { t } = useTranslation('rooms', { keyPrefix: 'admin' })
@@ -205,7 +206,7 @@ export const Admin = () => {
patchRoom({ patchRoom({
roomId, roomId,
room: { access_level: value as ApiAccessLevel }, room: { access_level: value as ApiAccessLevel },
}).catch((e) => console.error(e)) }).catch((e) => reportError('generic_failure', e))
} }
items={[ items={[
{ {
@@ -4,6 +4,7 @@ import { RiCameraSwitchLine } from '@remixicon/react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import type { ButtonProps } from 'react-aria-components' import type { ButtonProps } from 'react-aria-components'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { reportError } from '@/features/analytics/telemetry'
enum FacingMode { enum FacingMode {
USER = 'user', USER = 'user',
@@ -103,7 +104,11 @@ export const CameraSwitchButton = (props: Partial<ButtonProps>) => {
setActiveMediaDevice(device.deviceId) setActiveMediaDevice(device.deviceId)
setFacingMode(target) setFacingMode(target)
} else { } else {
console.error('Cannot get user device with facingMode ' + target) reportError(
'device_switch_failure',
new Error('Cannot get user device with facingMode ' + target),
{ path: 'switch_device', kind: 'videoinput', facing_mode: target }
)
} }
} }
return ( return (
@@ -3,6 +3,7 @@ import { Button } from '@/primitives'
import { RiPhoneFill } from '@remixicon/react' import { RiPhoneFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ConnectionState } from 'livekit-client' import { ConnectionState } from 'livekit-client'
import { reportError } from '@/features/analytics/telemetry'
export const LeaveButton = () => { export const LeaveButton = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls' }) const { t } = useTranslation('rooms', { keyPrefix: 'controls' })
@@ -15,11 +16,11 @@ export const LeaveButton = () => {
tooltip={t('leave')} tooltip={t('leave')}
aria-label={t('leave')} aria-label={t('leave')}
onPress={() => { onPress={() => {
room room.disconnect(true).catch((e) =>
.disconnect(true) reportError('disconnect_failure', e, {
.catch((e) => context: 'An error occurred while disconnecting:',
console.error('An error occurred while disconnecting:', e) })
) )
}} }}
data-attr="controls-leave" data-attr="controls-leave"
> >
@@ -33,6 +33,7 @@ import { useConfig } from '@/api/useConfig.ts'
import { proxy, useSnapshot } from 'valtio' import { proxy, useSnapshot } from 'valtio'
import { Spinner } from '@/primitives/Spinner.tsx' import { Spinner } from '@/primitives/Spinner.tsx'
import { userChoicesStore, saveProcessorConfig } from '@/stores/userChoices' import { userChoicesStore, saveProcessorConfig } from '@/stores/userChoices'
import { reportError } from '@/features/analytics/telemetry'
enum BlurRadius { enum BlurRadius {
NONE = 0, NONE = 0,
@@ -238,7 +239,9 @@ export const EffectsConfiguration = ({
updateEffectStatusMessage(config, wasSelectedBeforeToggle) updateEffectStatusMessage(config, wasSelectedBeforeToggle)
} catch (error) { } catch (error) {
console.error('Error applying effect:', error) reportError('effects_processor_failure', error, {
context: 'Error applying effect:',
})
} finally { } finally {
// Without setTimeout the DOM is not refreshing when updating the options. // Without setTimeout the DOM is not refreshing when updating the options.
setTimeout(() => setProcessorPending(false)) setTimeout(() => setProcessorPending(false))
@@ -5,6 +5,7 @@ import { RiGlassesLine, RiGoblet2Fill } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FaceLandmarksProcessor } from '../blur/FaceLandmarksProcessor' import { FaceLandmarksProcessor } from '../blur/FaceLandmarksProcessor'
import type { LocalVideoTrack } from 'livekit-client' import type { LocalVideoTrack } from 'livekit-client'
import { reportError } from '@/features/analytics/telemetry'
export type FunnyEffectsProps = { export type FunnyEffectsProps = {
videoTrack: LocalVideoTrack videoTrack: LocalVideoTrack
@@ -55,7 +56,9 @@ export const FunnyEffects = ({
await videoTrack.setProcessor(newProcessor) await videoTrack.setProcessor(newProcessor)
} }
} catch (e) { } catch (e) {
console.error('could not update processor', e) reportError('effects_processor_failure', e, {
context: 'could not update processor',
})
} finally { } finally {
onPending(false) onPending(false)
} }
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from 'react'
import { formatPinCode } from '@/features/rooms/utils/telephony' import { formatPinCode } from '@/features/rooms/utils/telephony'
import type { ApiRoom } from '@/features/rooms/api/ApiRoom' import type { ApiRoom } from '@/features/rooms/api/ApiRoom'
import { getRouteUrl } from '@/navigation/getRouteUrl' import { getRouteUrl } from '@/navigation/getRouteUrl'
import { reportError } from '@/features/analytics/telemetry'
const COPY_SUCCESS_TIMEOUT = 3000 const COPY_SUCCESS_TIMEOUT = 3000
@@ -58,7 +59,7 @@ export const useCopyRoomToClipboard = (room: ApiRoom | undefined) => {
await navigator.clipboard.writeText(content) await navigator.clipboard.writeText(content)
setIsCopied(true) setIsCopied(true)
} catch (error) { } catch (error) {
console.error(error) reportError('clipboard_failure', error)
} }
} }
@@ -67,7 +68,7 @@ export const useCopyRoomToClipboard = (room: ApiRoom | undefined) => {
await navigator.clipboard.writeText(roomUrl) await navigator.clipboard.writeText(roomUrl)
setIsRoomUrlCopied(true) setIsRoomUrlCopied(true)
} catch (error) { } catch (error) {
console.error(error) reportError('clipboard_failure', error)
} }
} }
@@ -4,6 +4,7 @@
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { type TrackReferenceOrPlaceholder } from '@livekit/components-core' import { type TrackReferenceOrPlaceholder } from '@livekit/components-core'
import { reportError } from '@/features/analytics/telemetry'
export function useFullScreen({ export function useFullScreen({
trackRef, trackRef,
@@ -56,7 +57,9 @@ export function useFullScreen({
await docEl.msRequestFullscreen() await docEl.msRequestFullscreen()
} }
} catch (error) { } catch (error) {
console.error('Error entering fullscreen:', error) reportError('fullscreen_failure', error, {
context: 'Error entering fullscreen:',
})
} }
} }
@@ -70,7 +73,9 @@ export function useFullScreen({
await document.msExitFullscreen() await document.msExitFullscreen()
} }
} catch (error) { } catch (error) {
console.error('Error exiting fullscreen:', error) reportError('fullscreen_failure', error, {
context: 'Error exiting fullscreen:',
})
} }
} }
@@ -1,6 +1,7 @@
import { usePatchRoom } from '@/features/rooms/api/patchRoom' import { usePatchRoom } from '@/features/rooms/api/patchRoom'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { useCallback } from 'react' import { useCallback } from 'react'
import { reportError } from '@/features/analytics/telemetry'
export const usePermissionsManager = () => { export const usePermissionsManager = () => {
const { mutateAsync: patchRoom } = usePatchRoom() const { mutateAsync: patchRoom } = usePatchRoom()
@@ -28,7 +29,9 @@ export const usePermissionsManager = () => {
return { configuration: newConfiguration } return { configuration: newConfiguration }
} catch (error) { } catch (error) {
console.error('Failed to update muting permission:', error) reportError('permissions_api_failure', error, {
context: 'Failed to update muting permission:',
})
return { success: false, error } return { success: false, error }
} }
}, },
@@ -12,6 +12,7 @@ import {
NotificationType, NotificationType,
useNotifyParticipants, useNotifyParticipants,
} from '@/features/notifications' } from '@/features/notifications'
import { reportError } from '@/features/analytics/telemetry'
export const updatePublishSources = ( export const updatePublishSources = (
currentSources: Source[], currentSources: Source[],
@@ -108,7 +109,9 @@ export const usePublishSourcesManager = () => {
return { configuration: newConfiguration } return { configuration: newConfiguration }
} catch (error) { } catch (error) {
console.error(`Failed to update ${sources}:`, error) reportError('publish_sources_failure', error, {
context: `Failed to update ${sources}:`,
})
return { success: false, error } return { success: false, error }
} }
}, },
@@ -8,6 +8,7 @@ import {
import { isLocal } from '@/utils/livekit' import { isLocal } from '@/utils/livekit'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useRaiseHand } from '@/features/rooms/api/updateRaiseHand' import { useRaiseHand } from '@/features/rooms/api/updateRaiseHand'
import { reportError } from '@/features/analytics/telemetry'
type useRaisedHandProps = { type useRaisedHandProps = {
participant: Participant participant: Participant
@@ -79,8 +80,11 @@ export function useRaisedHand({ participant }: useRaisedHandProps) {
try { try {
await raiseHand(!isHandRaised) await raiseHand(!isHandRaised)
} catch (e) { } catch (e) {
console.error( reportError(
`Failed to toggle hand: ${e instanceof Error ? e.message : 'Unknown error'}` 'generic_failure',
new Error(
`Failed to toggle hand: ${e instanceof Error ? e.message : 'Unknown error'}`
)
) )
} }
} }
@@ -11,6 +11,7 @@ import { SidePanel } from '../components/SidePanel'
import { RecordingProvider } from '@/features/recording' import { RecordingProvider } from '@/features/recording'
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal' import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
import { ConnectionObserver } from '../components/ConnectionObserver' import { ConnectionObserver } from '../components/ConnectionObserver'
import { reportError } from '@/features/analytics/telemetry'
import { MediaStateObserver } from '../components/MediaStateObserver' import { MediaStateObserver } from '../components/MediaStateObserver'
import { RoomMetadataSynchronizer } from '../components/RoomMetadataSynchronizer' import { RoomMetadataSynchronizer } from '../components/RoomMetadataSynchronizer'
import { useRoomPageTitle } from '../hooks/useRoomPageTitle' import { useRoomPageTitle } from '../hooks/useRoomPageTitle'
@@ -91,7 +92,10 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
</RoomContentArea> </RoomContentArea>
<ControlBar <ControlBar
onDeviceError={(e) => { onDeviceError={(e) => {
console.error(e) reportError('device_switch_failure', e.error, {
at: 'ControlBar.onDeviceError',
source: e.source,
})
if ( if (
e.source == Track.Source.ScreenShare && e.source == Track.Source.ScreenShare &&
e.error.toString() == e.error.toString() ==
@@ -1,9 +0,0 @@
export const asError = (value: unknown): Error => {
if (value instanceof Error) return value
if (value instanceof Event) {
return new Error(
`Unhandled event "${value.type}" from ${value.target?.constructor.name ?? 'unknown'}`
)
}
return new Error(String(value))
}
@@ -5,6 +5,7 @@ import { useUser } from '@/features/auth/api/useUser'
import { Spinner } from '@/primitives/Spinner' import { Spinner } from '@/primitives/Spinner'
import { CallbackIdHandler } from '../utils/CallbackIdHandler' import { CallbackIdHandler } from '../utils/CallbackIdHandler'
import { PopupWindow } from '../utils/PopupWindow' import { PopupWindow } from '../utils/PopupWindow'
import { reportError } from '@/features/analytics/telemetry'
const callbackIdHandler = new CallbackIdHandler() const callbackIdHandler = new CallbackIdHandler()
const popupWindow = new PopupWindow() const popupWindow = new PopupWindow()
@@ -52,7 +53,9 @@ const CreatePopup = () => {
popupWindow.close() popupWindow.close()
}) })
} catch (error) { } catch (error) {
console.error('Failed to create meeting room:', error) reportError('generic_failure', error, {
context: 'Failed to create meeting room:',
})
} }
} }
if (isLoggedIn && callbackId) { if (isLoggedIn && callbackId) {
@@ -15,6 +15,7 @@ import { usePatchRoom } from '@/features/rooms/api/patchRoom'
import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom' import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom'
import { updatePublishSources } from '@/features/rooms/livekit/hooks/usePublishSourcesManager' import { updatePublishSources } from '@/features/rooms/livekit/hooks/usePublishSourcesManager'
import { isSubsetOf } from '@/features/rooms/utils/isSubsetOf' import { isSubsetOf } from '@/features/rooms/utils/isSubsetOf'
import { reportError } from '@/features/analytics/telemetry'
type Source = Track.Source type Source = Track.Source
@@ -108,7 +109,7 @@ const SettingsPopup = () => {
patchRoom({ patchRoom({
roomId: roomSlug, roomId: roomSlug,
room: { configuration: newConfiguration }, room: { configuration: newConfiguration },
}).catch((e) => console.error(e)) }).catch((e) => reportError('generic_failure', e))
} }
const updateSource = (sources: Source[], enabled: boolean) => { const updateSource = (sources: Source[], enabled: boolean) => {
@@ -329,7 +330,7 @@ const SettingsPopup = () => {
patchRoom({ patchRoom({
roomId: roomSlug, roomId: roomSlug,
room: { access_level: value as ApiAccessLevel }, room: { access_level: value as ApiAccessLevel },
}).catch((e) => console.error(e)) }).catch((e) => reportError('generic_failure', e))
} }
items={[ items={[
{ {
@@ -1,5 +1,6 @@
import { authUrl } from '@/features/auth/utils/authUrl' import { authUrl } from '@/features/auth/utils/authUrl'
import { PopupMessageType, CallbackCreationRoomData } from './types' import { PopupMessageType, CallbackCreationRoomData } from './types'
import { reportError } from '@/features/analytics/telemetry'
export class PopupWindow { export class PopupWindow {
private sendMessageToManager( private sendMessageToManager(
@@ -9,7 +10,7 @@ export class PopupWindow {
callback?: () => void callback?: () => void
) { ) {
if (!window.opener) { if (!window.opener) {
console.error('No manager window found') reportError('generic_failure', new Error('No manager window found'))
window.close() window.close()
return return
} }
@@ -11,6 +11,7 @@ import { useRenameParticipant } from '@/features/rooms/api/renameParticipant'
import { saveUsername } from '@/stores/user' import { saveUsername } from '@/stores/user'
import { logout } from '@/features/auth/utils/logout' import { logout } from '@/features/auth/utils/logout'
import { useConfig } from '@/api/useConfig' import { useConfig } from '@/api/useConfig'
import { reportError } from '@/features/analytics/telemetry'
export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> & export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
Pick<TabPanelProps, 'id'> Pick<TabPanelProps, 'id'>
@@ -35,8 +36,11 @@ export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => {
saveUsername(name) saveUsername(name)
onOpenChange?.(false) // only close on success onOpenChange?.(false) // only close on success
} catch (error) { } catch (error) {
console.error( reportError(
`Failed to rename participant: ${error instanceof Error ? error.message : 'Unknown error'}` 'generic_failure',
new Error(
`Failed to rename participant: ${error instanceof Error ? error.message : 'Unknown error'}`
)
) )
} }
} }
+6 -2
View File
@@ -8,6 +8,7 @@ import { type StyledVariantProps } from '@/styled-system/types'
import { styled } from '@/styled-system/jsx' import { styled } from '@/styled-system/jsx'
import { FieldErrors } from './FieldErrors' import { FieldErrors } from './FieldErrors'
import { FieldDescription } from './FieldDescription' import { FieldDescription } from './FieldDescription'
import { reportError } from '@/features/analytics/telemetry'
// styled taken from example at https://react-spectrum.adobe.com/react-aria/Checkbox.html // styled taken from example at https://react-spectrum.adobe.com/react-aria/Checkbox.html
const StyledCheckbox = styled(RACCheckbox, { const StyledCheckbox = styled(RACCheckbox, {
@@ -112,8 +113,11 @@ export const Checkbox = ({
const descriptionId = useId() const descriptionId = useId()
if (isInvalid !== undefined) { if (isInvalid !== undefined) {
console.error( reportError(
'Checkbox: passing isInvalid is not supported, use the validate prop instead' 'generic_failure',
new Error(
'Checkbox: passing isInvalid is not supported, use the validate prop instead'
)
) )
return null return null
} }
+4 -4
View File
@@ -1,6 +1,7 @@
import { proxy, subscribe } from 'valtio' import { proxy, subscribe } from 'valtio'
import { STORAGE_KEYS } from '@/utils/storageKeys' import { STORAGE_KEYS } from '@/utils/storageKeys'
import { deserializeToProxyMap } from '@/utils/valtio' import { deserializeToProxyMap } from '@/utils/valtio'
import { reportError } from '@/features/analytics/telemetry'
export type UiFont = export type UiFont =
| 'default' | 'default'
@@ -154,10 +155,9 @@ function getAccessibilityState(): AccessibilityState {
return DEFAULT_STATE return DEFAULT_STATE
} catch (error: unknown) { } catch (error: unknown) {
console.error( reportError('generic_failure', error, {
'[AccessibilityStore] Failed to parse stored settings:', context: '[AccessibilityStore] Failed to parse stored settings:',
error })
)
return DEFAULT_STATE return DEFAULT_STATE
} }
} }
+4 -4
View File
@@ -3,6 +3,7 @@ import { proxyMap } from 'valtio/utils'
import { deserializeToProxyMap, serializeProxyMap } from '@/utils/valtio' import { deserializeToProxyMap, serializeProxyMap } from '@/utils/valtio'
import { STORAGE_KEYS } from '@/utils/storageKeys' import { STORAGE_KEYS } from '@/utils/storageKeys'
import { NotificationType } from '@/features/notifications/NotificationType' import { NotificationType } from '@/features/notifications/NotificationType'
import { reportError } from '@/features/analytics/telemetry'
type State = { type State = {
soundNotifications: Map<NotificationType, boolean> soundNotifications: Map<NotificationType, boolean>
@@ -41,10 +42,9 @@ function getNotificationsState(): State {
), ),
} }
} catch (error: unknown) { } catch (error: unknown) {
console.error( reportError('generic_failure', error, {
'[NotificationsStore] Failed to parse stored settings:', context: '[NotificationsStore] Failed to parse stored settings:',
error })
)
return DEFAULT_STATE return DEFAULT_STATE
} }
} }
+4 -4
View File
@@ -1,5 +1,6 @@
import { proxy, subscribe } from 'valtio' import { proxy, subscribe } from 'valtio'
import { STORAGE_KEYS } from '@/utils/storageKeys' import { STORAGE_KEYS } from '@/utils/storageKeys'
import { reportError } from '@/features/analytics/telemetry'
type State = { type State = {
username: string username: string
@@ -18,10 +19,9 @@ function getUserState(): State {
...parsed, ...parsed,
} }
} catch (error: unknown) { } catch (error: unknown) {
console.error( reportError('generic_failure', error, {
'[UserPreferencesStore] Failed to parse stored settings:', context: '[UserPreferencesStore] Failed to parse stored settings:',
error })
)
return DEFAULT_STATE return DEFAULT_STATE
} }
} }
+4 -4
View File
@@ -1,5 +1,6 @@
import { proxy, subscribe } from 'valtio' import { proxy, subscribe } from 'valtio'
import { STORAGE_KEYS } from '@/utils/storageKeys' import { STORAGE_KEYS } from '@/utils/storageKeys'
import { reportError } from '@/features/analytics/telemetry'
type State = { type State = {
is_idle_disconnect_modal_enabled: boolean is_idle_disconnect_modal_enabled: boolean
@@ -21,10 +22,9 @@ function getUserPreferencesState(): State {
...parsed, ...parsed,
} }
} catch (error: unknown) { } catch (error: unknown) {
console.error( reportError('generic_failure', error, {
'[UserPreferencesStore] Failed to parse stored settings:', context: '[UserPreferencesStore] Failed to parse stored settings:',
error })
)
return DEFAULT_STATE return DEFAULT_STATE
} }
} }
+2 -1
View File
@@ -13,6 +13,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import clsx from 'clsx' import clsx from 'clsx'
import { reportError } from '@/features/analytics/telemetry'
/** /**
* Calls all functions in the order they were chained with the same arguments. * Calls all functions in the order they were chained with the same arguments.
@@ -25,7 +26,7 @@ export function chain(...callbacks: any[]): (...args: any[]) => void {
try { try {
callback(...args) callback(...args)
} catch (e) { } catch (e) {
console.error(e) reportError('generic_failure', e)
} }
} }
} }