♻️(frontend) encapsulate error tracking behind a telemetry module

Introduce a telemetry module that exposes a `reportError` helper.
Under the hood it forwards errors to PostHog, but the module is the
only place that knows about PostHog.

Replace `console.error` calls used for error reporting with
`reportError`, so the codebase now goes through a single, consistent
API for telemetry.

This normalizes how errors are reported and makes it straightforward
to swap PostHog for another backend later on, without touching every
call site.
This commit is contained in:
lebaudantoine
2026-08-07 15:02:28 +02:00
committed by aleb_the_flash
parent d810c9e0de
commit 48c0cb320e
43 changed files with 219 additions and 88 deletions
+4
View File
@@ -8,6 +8,10 @@ and this project adheres to
## [Unreleased] ## [Unreleased]
### Changed
- ♻️(frontend) encapsulate error tracking behind a telemetry module
## [1.25.2] - 2026-08-06 ## [1.25.2] - 2026-08-06
### Fixed ### Fixed
+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,9 @@ 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, {
context: 'copy_room_content',
})
} }
} }
@@ -67,7 +70,9 @@ 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, {
context: 'copy_room_url',
})
} }
} }
@@ -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,9 +80,9 @@ export function useRaisedHand({ participant }: useRaisedHandProps) {
try { try {
await raiseHand(!isHandRaised) await raiseHand(!isHandRaised)
} catch (e) { } catch (e) {
console.error( reportError('generic_failure', e, {
`Failed to toggle hand: ${e instanceof Error ? e.message : 'Unknown error'}` context: 'toggle_raised_hand',
) })
} }
} }
@@ -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,9 +36,9 @@ 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('generic_failure', error, {
`Failed to rename participant: ${error instanceof Error ? error.message : 'Unknown error'}` context: 'rename_participant',
) })
} }
} }
const handleOnCancel = () => { const handleOnCancel = () => {
-3
View File
@@ -112,9 +112,6 @@ export const Checkbox = ({
const descriptionId = useId() const descriptionId = useId()
if (isInvalid !== undefined) { if (isInvalid !== undefined) {
console.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: '[UserStore] 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)
} }
} }
} }