From 48c0cb320e55ed5e8d260a07a0c7d598051ec08b Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Fri, 7 Aug 2026 15:02:28 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20encapsulate=20er?= =?UTF-8?q?ror=20tracking=20behind=20a=20telemetry=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 4 ++ src/frontend/src/components/SoundTester.tsx | 6 ++- .../features/analytics/hooks/useAnalytics.ts | 9 +---- .../src/features/analytics/telemetry.ts | 38 +++++++++++++++++++ src/frontend/src/features/analytics/utils.ts | 8 ++++ .../src/features/home/routes/Home.tsx | 5 ++- .../src/features/notifications/utils.ts | 5 ++- .../participants/api/lowerHandParticipants.ts | 5 ++- .../participants/api/updateParticipantRole.ts | 8 +++- .../hooks/useWaitingParticipants.ts | 3 +- .../features/pip/hooks/usePictureInPicture.ts | 5 ++- .../components/ScreenRecordingSidePanel.tsx | 5 ++- .../components/TranscriptSidePanel.tsx | 5 ++- .../recording/hooks/useRoomMetadata.ts | 5 ++- .../src/features/rooms/api/muteParticipant.ts | 20 +++++++--- .../features/rooms/api/muteParticipants.ts | 5 ++- .../rooms/api/updateParticipantPermissions.ts | 8 +++- .../api/updateParticipantsPermissions.ts | 5 ++- .../features/rooms/components/Conference.tsx | 6 ++- .../src/features/rooms/components/Join.tsx | 4 +- .../rooms/hooks/useWatchPermissions.ts | 9 ++++- .../rooms/livekit/components/Admin.tsx | 3 +- .../controls/CameraSwitchButton.tsx | 7 +++- .../components/controls/LeaveButton.tsx | 11 +++--- .../effects/EffectsConfiguration.tsx | 5 ++- .../components/effects/FunnyEffects.tsx | 5 ++- .../livekit/hooks/useCopyRoomToClipboard.ts | 9 ++++- .../rooms/livekit/hooks/useFullScreen.ts | 9 ++++- .../livekit/hooks/usePermissionsManager.ts | 5 ++- .../livekit/hooks/usePublishSourcesManager.ts | 5 ++- .../rooms/livekit/hooks/useRaisedHand.ts | 7 ++-- .../rooms/livekit/prefabs/VideoConference.tsx | 6 ++- .../src/features/rooms/utils/error.ts | 9 ----- .../src/features/sdk/routes/CreatePopup.tsx | 5 ++- .../src/features/sdk/routes/SettingsPopup.tsx | 5 ++- .../src/features/sdk/utils/PopupWindow.ts | 3 +- .../settings/components/tabs/AccountTab.tsx | 7 ++-- src/frontend/src/primitives/Checkbox.tsx | 3 -- src/frontend/src/stores/accessibility.ts | 8 ++-- src/frontend/src/stores/notifications.ts | 8 ++-- src/frontend/src/stores/user.ts | 8 ++-- src/frontend/src/stores/userPreferences.ts | 8 ++-- src/frontend/src/utils/mergeProps.ts | 3 +- 43 files changed, 219 insertions(+), 88 deletions(-) create mode 100644 src/frontend/src/features/analytics/telemetry.ts create mode 100644 src/frontend/src/features/analytics/utils.ts delete mode 100644 src/frontend/src/features/rooms/utils/error.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ec0fb3c..f73cdd09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to ## [Unreleased] +### Changed + +- ♻️(frontend) encapsulate error tracking behind a telemetry module + ## [1.25.2] - 2026-08-06 ### Fixed diff --git a/src/frontend/src/components/SoundTester.tsx b/src/frontend/src/components/SoundTester.tsx index cdaba624..5080a1a4 100644 --- a/src/frontend/src/components/SoundTester.tsx +++ b/src/frontend/src/components/SoundTester.tsx @@ -2,6 +2,7 @@ import { Button } from '@/primitives' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useMediaDeviceSelect } from '@livekit/components-react' +import { reportError } from '@/features/analytics/telemetry' export const SoundTester = () => { const { t } = useTranslation('settings') @@ -15,7 +16,10 @@ export const SoundTester = () => { try { await audioRef?.current?.setSinkId(deviceId) } catch (error) { - console.error(`Error setting sinkId: ${error}`) + reportError( + 'device_switch_failure', + new Error(`Error setting sinkId: ${error}`) + ) } } updateActiveId(activeDeviceId) diff --git a/src/frontend/src/features/analytics/hooks/useAnalytics.ts b/src/frontend/src/features/analytics/hooks/useAnalytics.ts index c0b241af..feb6d6e1 100644 --- a/src/frontend/src/features/analytics/hooks/useAnalytics.ts +++ b/src/frontend/src/features/analytics/hooks/useAnalytics.ts @@ -1,15 +1,8 @@ import { useEffect } from 'react' import { useLocation } from 'wouter' -import { type PostHog } from 'posthog-js' import { type ApiUser } from '@/features/auth/api/ApiUser' import { useUser } from '@/features/auth/api/useUser' - -let posthog: PostHog | null = null - -const getPosthog = async () => { - if (!posthog) posthog = (await import('posthog-js')).default - return posthog -} +import { getPosthog } from '../utils' export const startAnalyticsSession = (data: ApiUser) => { getPosthog().then((ph) => { diff --git a/src/frontend/src/features/analytics/telemetry.ts b/src/frontend/src/features/analytics/telemetry.ts new file mode 100644 index 00000000..2f68a12b --- /dev/null +++ b/src/frontend/src/features/analytics/telemetry.ts @@ -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 = {} +): 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) + } +} diff --git a/src/frontend/src/features/analytics/utils.ts b/src/frontend/src/features/analytics/utils.ts new file mode 100644 index 00000000..00711733 --- /dev/null +++ b/src/frontend/src/features/analytics/utils.ts @@ -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 +} diff --git a/src/frontend/src/features/home/routes/Home.tsx b/src/frontend/src/features/home/routes/Home.tsx index 64b1be50..f9f11752 100644 --- a/src/frontend/src/features/home/routes/Home.tsx +++ b/src/frontend/src/features/home/routes/Home.tsx @@ -15,6 +15,7 @@ import { css } from '@/styled-system/css' import { useConfig } from '@/api/useConfig' import { LoginButton } from '@/components/LoginButton' import { LoadingScreen } from '@/components/LoadingScreen' +import { reportError } from '@/features/analytics/telemetry' const Columns = ({ children }: { children?: ReactNode }) => { return ( @@ -160,7 +161,9 @@ const Home = () => { window.location.replace(data.external_home_url) } catch (error) { setRedirectFailed(true) - console.error('Site is not reachable:', error) + reportError('generic_failure', error, { + context: 'Site is not reachable:', + }) } } } diff --git a/src/frontend/src/features/notifications/utils.ts b/src/frontend/src/features/notifications/utils.ts index a43dbe34..d1dbda00 100644 --- a/src/frontend/src/features/notifications/utils.ts +++ b/src/frontend/src/features/notifications/utils.ts @@ -4,6 +4,7 @@ import { NotificationDuration } from './NotificationDuration' import type { Participant } from 'livekit-client' import type { NotificationPayload } from './NotificationPayload' import type { RecordingMode } from '@/features/recording' +import { reportError } from '@/features/analytics/telemetry' export const notifyAutoMutedOnJoin = () => { toastQueue.add( @@ -55,7 +56,9 @@ export const decodeNotificationDataReceived = ( return parsed as NotificationPayload } catch (error) { // 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 } } diff --git a/src/frontend/src/features/participants/api/lowerHandParticipants.ts b/src/frontend/src/features/participants/api/lowerHandParticipants.ts index 3e22ad13..877db08e 100644 --- a/src/frontend/src/features/participants/api/lowerHandParticipants.ts +++ b/src/frontend/src/features/participants/api/lowerHandParticipants.ts @@ -1,5 +1,6 @@ import type { Participant } from 'livekit-client' import { useLowerHandParticipant } from './lowerHandParticipant' +import { reportError } from '@/features/analytics/telemetry' export const useLowerHandParticipants = () => { const { lowerHandParticipant } = useLowerHandParticipant() @@ -11,7 +12,9 @@ export const useLowerHandParticipants = () => { ) return Promise.all(promises) } 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.', { cause: error, }) diff --git a/src/frontend/src/features/participants/api/updateParticipantRole.ts b/src/frontend/src/features/participants/api/updateParticipantRole.ts index 8fc4eb53..cb2c712e 100644 --- a/src/frontend/src/features/participants/api/updateParticipantRole.ts +++ b/src/frontend/src/features/participants/api/updateParticipantRole.ts @@ -1,6 +1,7 @@ import { fetchApi } from '@/api/fetchApi' import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' import { AssignableParticipantRole } from '@/features/rooms/api/ApiRoom' +import { reportError } from '@/features/analytics/telemetry' export const useParticipantRole = () => { const data = useRoomData() @@ -22,8 +23,11 @@ export const useParticipantRole = () => { }), }) } catch (error) { - console.error( - `Failed to update participant's role ${identity}: ${error instanceof Error ? error.message : 'Unknown error'}` + reportError( + 'generic_failure', + new Error( + `Failed to update participant's role ${identity}: ${error instanceof Error ? error.message : 'Unknown error'}` + ) ) } } diff --git a/src/frontend/src/features/participants/hooks/useWaitingParticipants.ts b/src/frontend/src/features/participants/hooks/useWaitingParticipants.ts index 825ff7e4..96501634 100644 --- a/src/frontend/src/features/participants/hooks/useWaitingParticipants.ts +++ b/src/frontend/src/features/participants/hooks/useWaitingParticipants.ts @@ -10,6 +10,7 @@ import { } from '../../participants/api/listWaitingParticipants' import { decodeNotificationDataReceived } from '@/features/notifications/utils' import { NotificationType } from '@/features/notifications/NotificationType' +import { reportError } from '@/features/analytics/telemetry' export const POLL_INTERVAL_MS = 1000 @@ -87,7 +88,7 @@ export const useWaitingParticipants = () => { await refetchWaiting() } catch (e) { - console.error(e) + reportError('generic_failure', e) setListEnabled(true) } } diff --git a/src/frontend/src/features/pip/hooks/usePictureInPicture.ts b/src/frontend/src/features/pip/hooks/usePictureInPicture.ts index 475a3f0b..bbd454e6 100644 --- a/src/frontend/src/features/pip/hooks/usePictureInPicture.ts +++ b/src/frontend/src/features/pip/hooks/usePictureInPicture.ts @@ -3,6 +3,7 @@ import { useCallback, useMemo } from 'react' import { flushSync } from 'react-dom' import { documentPictureInPictureStore } from '@/stores/documentPictureInPicture' import { useTranslation } from 'react-i18next' +import { reportError } from '@/features/analytics/telemetry' export const IS_PIP_SUPPORTED = typeof globalThis !== 'undefined' && 'documentPictureInPicture' in globalThis @@ -86,7 +87,9 @@ export const usePictureInPicture = () => { documentPictureInPictureStore.window = ref(pipWindow) } catch (error) { // 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 } }, diff --git a/src/frontend/src/features/recording/components/ScreenRecordingSidePanel.tsx b/src/frontend/src/features/recording/components/ScreenRecordingSidePanel.tsx index cd410210..f3bb161b 100644 --- a/src/frontend/src/features/recording/components/ScreenRecordingSidePanel.tsx +++ b/src/frontend/src/features/recording/components/ScreenRecordingSidePanel.tsx @@ -29,6 +29,7 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel' import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner' import { FeatureFlags } from '@/features/analytics/enums' import { LimitDescription } from './LimitDescription' +import { reportError } from '@/features/analytics/telemetry' export const ScreenRecordingSidePanel = () => { const { data } = useConfig() @@ -106,7 +107,9 @@ export const ScreenRecordingSidePanel = () => { }) } } catch (error) { - console.error('Failed to handle recording:', error) + reportError('generic_failure', error, { + context: 'Failed to handle recording:', + }) } } diff --git a/src/frontend/src/features/recording/components/TranscriptSidePanel.tsx b/src/frontend/src/features/recording/components/TranscriptSidePanel.tsx index c748f09d..09cd4196 100644 --- a/src/frontend/src/features/recording/components/TranscriptSidePanel.tsx +++ b/src/frontend/src/features/recording/components/TranscriptSidePanel.tsx @@ -35,6 +35,7 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel' import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner' import { LimitDescription } from './LimitDescription' import { openSettingsDialog } from '@/stores/settings' +import { reportError } from '@/features/analytics/telemetry' export const TranscriptSidePanel = () => { const { data } = useConfig() @@ -127,7 +128,9 @@ export const TranscriptSidePanel = () => { }) } } catch (error) { - console.error('Failed to handle transcript:', error) + reportError('generic_failure', error, { + context: 'Failed to handle transcript:', + }) } } diff --git a/src/frontend/src/features/recording/hooks/useRoomMetadata.ts b/src/frontend/src/features/recording/hooks/useRoomMetadata.ts index 1c00e74e..f92446b1 100644 --- a/src/frontend/src/features/recording/hooks/useRoomMetadata.ts +++ b/src/frontend/src/features/recording/hooks/useRoomMetadata.ts @@ -1,5 +1,6 @@ import { useRoomInfo } from '@livekit/components-react' import { useMemo } from 'react' +import { reportError } from '@/features/analytics/telemetry' export const useRoomMetadata = () => { const { metadata } = useRoomInfo() @@ -8,7 +9,9 @@ export const useRoomMetadata = () => { try { return JSON.parse(metadata) } catch (error) { - console.error('Failed to parse room metadata:', error) + reportError('generic_failure', error, { + context: 'Failed to parse room metadata:', + }) return undefined } } else { diff --git a/src/frontend/src/features/rooms/api/muteParticipant.ts b/src/frontend/src/features/rooms/api/muteParticipant.ts index 07c4ba52..02f88bb5 100644 --- a/src/frontend/src/features/rooms/api/muteParticipant.ts +++ b/src/frontend/src/features/rooms/api/muteParticipant.ts @@ -9,6 +9,7 @@ import { fetchApi } from '@/api/fetchApi' import { useIsAdminOrOwner } from '../livekit/hooks/useIsAdminOrOwner' import { useCallback } from 'react' +import { reportError } from '@/features/analytics/telemetry' export const useMuteParticipant = () => { const apiRoomData = useRoomData() @@ -31,7 +32,10 @@ export const useMuteParticipant = () => { // Guard against undefined token for non-admin users 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 } @@ -53,8 +57,11 @@ export const useMuteParticipant = () => { } ) } catch (error) { - console.error( - `Failed to mute participant ${participant.identity}: ${error instanceof Error ? error.message : 'Unknown error'}` + reportError( + 'participant_mute_api_failure', + new Error( + `Failed to mute participant ${participant.identity}: ${error instanceof Error ? error.message : 'Unknown error'}` + ) ) return } @@ -65,8 +72,11 @@ export const useMuteParticipant = () => { destinationIdentities: [participant.identity], }) } catch (e) { - console.error( - `Failed to notify muted participant ${participant.identity}: ${e}` + reportError( + 'participant_mute_api_failure', + new Error( + `Failed to notify muted participant ${participant.identity}: ${e}` + ) ) } diff --git a/src/frontend/src/features/rooms/api/muteParticipants.ts b/src/frontend/src/features/rooms/api/muteParticipants.ts index 2460d9ac..327e64a6 100644 --- a/src/frontend/src/features/rooms/api/muteParticipants.ts +++ b/src/frontend/src/features/rooms/api/muteParticipants.ts @@ -1,5 +1,6 @@ import type { Participant } from 'livekit-client' import { useMuteParticipant } from './muteParticipant' +import { reportError } from '@/features/analytics/telemetry' export const useMuteParticipants = () => { const { muteParticipant } = useMuteParticipant() @@ -11,7 +12,9 @@ export const useMuteParticipants = () => { ) return Promise.all(promises) } 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.', { cause: error, }) diff --git a/src/frontend/src/features/rooms/api/updateParticipantPermissions.ts b/src/frontend/src/features/rooms/api/updateParticipantPermissions.ts index ab90a318..358b3f74 100644 --- a/src/frontend/src/features/rooms/api/updateParticipantPermissions.ts +++ b/src/frontend/src/features/rooms/api/updateParticipantPermissions.ts @@ -1,6 +1,7 @@ import type { Participant, Track } from 'livekit-client' import { fetchApi } from '@/api/fetchApi' import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' +import { reportError } from '@/features/analytics/telemetry' type Source = Track.Source export const useParticipantPermissions = () => { @@ -32,8 +33,11 @@ export const useParticipantPermissions = () => { }), }) } catch (error) { - console.error( - `Failed to update participant's permissions ${participant.identity}: ${error instanceof Error ? error.message : 'Unknown error'}` + reportError( + 'permissions_api_failure', + new Error( + `Failed to update participant's permissions ${participant.identity}: ${error instanceof Error ? error.message : 'Unknown error'}` + ) ) } } diff --git a/src/frontend/src/features/rooms/api/updateParticipantsPermissions.ts b/src/frontend/src/features/rooms/api/updateParticipantsPermissions.ts index 4bdc208b..8a0c2545 100644 --- a/src/frontend/src/features/rooms/api/updateParticipantsPermissions.ts +++ b/src/frontend/src/features/rooms/api/updateParticipantsPermissions.ts @@ -1,5 +1,6 @@ import type { Participant, Track } from 'livekit-client' import { useParticipantPermissions } from './updateParticipantPermissions' +import { reportError } from '@/features/analytics/telemetry' type Source = Track.Source export const useUpdateParticipantsPermissions = () => { @@ -15,7 +16,9 @@ export const useUpdateParticipantsPermissions = () => { ) return Promise.all(promises) } 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.', { cause: error, }) diff --git a/src/frontend/src/features/rooms/components/Conference.tsx b/src/frontend/src/features/rooms/components/Conference.tsx index 18c576d9..9be2556f 100644 --- a/src/frontend/src/features/rooms/components/Conference.tsx +++ b/src/frontend/src/features/rooms/components/Conference.tsx @@ -26,6 +26,7 @@ import { css } from '@/styled-system/css' import { BackgroundProcessorFactory } from '../livekit/components/blur' import { LocalUserChoices } from '@/stores/userChoices' import { MediaDeviceErrorAlert } from './MediaDeviceErrorAlert' +import { reportError } from '@/features/analytics/telemetry' import { usePostHog } from 'posthog-js/react' import { useConfig } from '@/api/useConfig' import { isFireFox } from '@/utils/livekit' @@ -37,7 +38,6 @@ import { notifyAutoMutedOnJoin } from '@/features/notifications/utils' import { useSnapshot } from 'valtio' import { userPreferencesStore } from '@/stores/userPreferences' import { userStore } from '@/stores/user' -import { asError } from '../utils/error' export const Conference = ({ roomId, @@ -236,7 +236,9 @@ export const Conference = ({ backgroundColor: 'primaryDark.50 !important', })} onError={(e) => { - posthog.captureException(asError(e)) + reportError('livekit_room_error', e, { + failure: MediaDeviceFailure.getFailure(e) ?? 'not-a-device-error', + }) }} onConnected={async () => { if (!apiConfig) return diff --git a/src/frontend/src/features/rooms/components/Join.tsx b/src/frontend/src/features/rooms/components/Join.tsx index b3107b13..90600600 100644 --- a/src/frontend/src/features/rooms/components/Join.tsx +++ b/src/frontend/src/features/rooms/components/Join.tsx @@ -36,6 +36,7 @@ import { useLoginHint } from '@/hooks/useLoginHint' import { openPermissionsDialog } from '@/stores/permissions' import { useResolveInitiallyDefaultDeviceId } from '../livekit/hooks/useResolveInitiallyDefaultDeviceId' import { isSafari } from '@/utils/livekit' +import { reportError } from '@/features/analytics/telemetry' import { type LocalUserChoices, @@ -54,7 +55,8 @@ import { useSnapshot } from 'valtio' import { useUser } from '@/features/auth/api/useUser' 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 = ({ videoTrack, diff --git a/src/frontend/src/features/rooms/hooks/useWatchPermissions.ts b/src/frontend/src/features/rooms/hooks/useWatchPermissions.ts index 6868457c..fce355e9 100644 --- a/src/frontend/src/features/rooms/hooks/useWatchPermissions.ts +++ b/src/frontend/src/features/rooms/hooks/useWatchPermissions.ts @@ -1,6 +1,7 @@ import { useEffect } from 'react' import { permissionsStore } from '@/stores/permissions' import { isSafari } from '@/utils/livekit' +import { reportError } from '@/features/analytics/telemetry' const POLLING_TIME = 500 @@ -88,7 +89,9 @@ export const useWatchPermissions = () => { } } catch (error) { if (!isCancelled) { - console.error('Error polling permissions:', error) + reportError('permission_poll_failure', error, { + context: 'Error polling permissions:', + }) } } }, POLLING_TIME) @@ -142,7 +145,9 @@ export const useWatchPermissions = () => { } } catch (error) { if (!isCancelled) { - console.error('Error checking permissions:', error) + reportError('permission_poll_failure', error, { + context: 'Error checking permissions:', + }) } } finally { if (!isCancelled) { diff --git a/src/frontend/src/features/rooms/livekit/components/Admin.tsx b/src/frontend/src/features/rooms/livekit/components/Admin.tsx index 86142d90..7b4b5b2c 100644 --- a/src/frontend/src/features/rooms/livekit/components/Admin.tsx +++ b/src/frontend/src/features/rooms/livekit/components/Admin.tsx @@ -13,6 +13,7 @@ import { usePermissionsManager } from '../hooks/usePermissionsManager' import { useEffect } from 'react' import { closeSidePanel } from '@/stores/layout' import { useIsAdminOrOwner } from '../hooks/useIsAdminOrOwner' +import { reportError } from '@/features/analytics/telemetry' export const Admin = () => { const { t } = useTranslation('rooms', { keyPrefix: 'admin' }) @@ -205,7 +206,7 @@ export const Admin = () => { patchRoom({ roomId, room: { access_level: value as ApiAccessLevel }, - }).catch((e) => console.error(e)) + }).catch((e) => reportError('generic_failure', e)) } items={[ { diff --git a/src/frontend/src/features/rooms/livekit/components/controls/CameraSwitchButton.tsx b/src/frontend/src/features/rooms/livekit/components/controls/CameraSwitchButton.tsx index a4efedde..d3943049 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/CameraSwitchButton.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/CameraSwitchButton.tsx @@ -4,6 +4,7 @@ import { RiCameraSwitchLine } from '@remixicon/react' import { useEffect, useState } from 'react' import type { ButtonProps } from 'react-aria-components' import { useTranslation } from 'react-i18next' +import { reportError } from '@/features/analytics/telemetry' enum FacingMode { USER = 'user', @@ -103,7 +104,11 @@ export const CameraSwitchButton = (props: Partial) => { setActiveMediaDevice(device.deviceId) setFacingMode(target) } 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 ( diff --git a/src/frontend/src/features/rooms/livekit/components/controls/LeaveButton.tsx b/src/frontend/src/features/rooms/livekit/components/controls/LeaveButton.tsx index c12cde61..ce598889 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/LeaveButton.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/LeaveButton.tsx @@ -3,6 +3,7 @@ import { Button } from '@/primitives' import { RiPhoneFill } from '@remixicon/react' import { useTranslation } from 'react-i18next' import { ConnectionState } from 'livekit-client' +import { reportError } from '@/features/analytics/telemetry' export const LeaveButton = () => { const { t } = useTranslation('rooms', { keyPrefix: 'controls' }) @@ -15,11 +16,11 @@ export const LeaveButton = () => { tooltip={t('leave')} aria-label={t('leave')} onPress={() => { - room - .disconnect(true) - .catch((e) => - console.error('An error occurred while disconnecting:', e) - ) + room.disconnect(true).catch((e) => + reportError('disconnect_failure', e, { + context: 'An error occurred while disconnecting:', + }) + ) }} data-attr="controls-leave" > diff --git a/src/frontend/src/features/rooms/livekit/components/effects/EffectsConfiguration.tsx b/src/frontend/src/features/rooms/livekit/components/effects/EffectsConfiguration.tsx index 44c761d6..8c3fb335 100644 --- a/src/frontend/src/features/rooms/livekit/components/effects/EffectsConfiguration.tsx +++ b/src/frontend/src/features/rooms/livekit/components/effects/EffectsConfiguration.tsx @@ -33,6 +33,7 @@ import { useConfig } from '@/api/useConfig.ts' import { proxy, useSnapshot } from 'valtio' import { Spinner } from '@/primitives/Spinner.tsx' import { userChoicesStore, saveProcessorConfig } from '@/stores/userChoices' +import { reportError } from '@/features/analytics/telemetry' enum BlurRadius { NONE = 0, @@ -238,7 +239,9 @@ export const EffectsConfiguration = ({ updateEffectStatusMessage(config, wasSelectedBeforeToggle) } catch (error) { - console.error('Error applying effect:', error) + reportError('effects_processor_failure', error, { + context: 'Error applying effect:', + }) } finally { // Without setTimeout the DOM is not refreshing when updating the options. setTimeout(() => setProcessorPending(false)) diff --git a/src/frontend/src/features/rooms/livekit/components/effects/FunnyEffects.tsx b/src/frontend/src/features/rooms/livekit/components/effects/FunnyEffects.tsx index 1259a1ad..dec3ae85 100644 --- a/src/frontend/src/features/rooms/livekit/components/effects/FunnyEffects.tsx +++ b/src/frontend/src/features/rooms/livekit/components/effects/FunnyEffects.tsx @@ -5,6 +5,7 @@ import { RiGlassesLine, RiGoblet2Fill } from '@remixicon/react' import { useTranslation } from 'react-i18next' import { FaceLandmarksProcessor } from '../blur/FaceLandmarksProcessor' import type { LocalVideoTrack } from 'livekit-client' +import { reportError } from '@/features/analytics/telemetry' export type FunnyEffectsProps = { videoTrack: LocalVideoTrack @@ -55,7 +56,9 @@ export const FunnyEffects = ({ await videoTrack.setProcessor(newProcessor) } } catch (e) { - console.error('could not update processor', e) + reportError('effects_processor_failure', e, { + context: 'could not update processor', + }) } finally { onPending(false) } diff --git a/src/frontend/src/features/rooms/livekit/hooks/useCopyRoomToClipboard.ts b/src/frontend/src/features/rooms/livekit/hooks/useCopyRoomToClipboard.ts index 6f07d80f..c3bb0ce5 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/useCopyRoomToClipboard.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/useCopyRoomToClipboard.ts @@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from 'react' import { formatPinCode } from '@/features/rooms/utils/telephony' import type { ApiRoom } from '@/features/rooms/api/ApiRoom' import { getRouteUrl } from '@/navigation/getRouteUrl' +import { reportError } from '@/features/analytics/telemetry' const COPY_SUCCESS_TIMEOUT = 3000 @@ -58,7 +59,9 @@ export const useCopyRoomToClipboard = (room: ApiRoom | undefined) => { await navigator.clipboard.writeText(content) setIsCopied(true) } 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) setIsRoomUrlCopied(true) } catch (error) { - console.error(error) + reportError('clipboard_failure', error, { + context: 'copy_room_url', + }) } } diff --git a/src/frontend/src/features/rooms/livekit/hooks/useFullScreen.ts b/src/frontend/src/features/rooms/livekit/hooks/useFullScreen.ts index 1f004380..13337c9f 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/useFullScreen.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/useFullScreen.ts @@ -4,6 +4,7 @@ import { useMemo, useState } from 'react' import { type TrackReferenceOrPlaceholder } from '@livekit/components-core' +import { reportError } from '@/features/analytics/telemetry' export function useFullScreen({ trackRef, @@ -56,7 +57,9 @@ export function useFullScreen({ await docEl.msRequestFullscreen() } } 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() } } catch (error) { - console.error('Error exiting fullscreen:', error) + reportError('fullscreen_failure', error, { + context: 'Error exiting fullscreen:', + }) } } diff --git a/src/frontend/src/features/rooms/livekit/hooks/usePermissionsManager.ts b/src/frontend/src/features/rooms/livekit/hooks/usePermissionsManager.ts index 2cf292a9..4d27b932 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/usePermissionsManager.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/usePermissionsManager.ts @@ -1,6 +1,7 @@ import { usePatchRoom } from '@/features/rooms/api/patchRoom' import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' import { useCallback } from 'react' +import { reportError } from '@/features/analytics/telemetry' export const usePermissionsManager = () => { const { mutateAsync: patchRoom } = usePatchRoom() @@ -28,7 +29,9 @@ export const usePermissionsManager = () => { return { configuration: newConfiguration } } 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 } } }, diff --git a/src/frontend/src/features/rooms/livekit/hooks/usePublishSourcesManager.ts b/src/frontend/src/features/rooms/livekit/hooks/usePublishSourcesManager.ts index dae46921..3108749b 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/usePublishSourcesManager.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/usePublishSourcesManager.ts @@ -12,6 +12,7 @@ import { NotificationType, useNotifyParticipants, } from '@/features/notifications' +import { reportError } from '@/features/analytics/telemetry' export const updatePublishSources = ( currentSources: Source[], @@ -108,7 +109,9 @@ export const usePublishSourcesManager = () => { return { configuration: newConfiguration } } catch (error) { - console.error(`Failed to update ${sources}:`, error) + reportError('publish_sources_failure', error, { + context: `Failed to update ${sources}:`, + }) return { success: false, error } } }, diff --git a/src/frontend/src/features/rooms/livekit/hooks/useRaisedHand.ts b/src/frontend/src/features/rooms/livekit/hooks/useRaisedHand.ts index 01612abc..f42271db 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/useRaisedHand.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/useRaisedHand.ts @@ -8,6 +8,7 @@ import { import { isLocal } from '@/utils/livekit' import { useMemo } from 'react' import { useRaiseHand } from '@/features/rooms/api/updateRaiseHand' +import { reportError } from '@/features/analytics/telemetry' type useRaisedHandProps = { participant: Participant @@ -79,9 +80,9 @@ export function useRaisedHand({ participant }: useRaisedHandProps) { try { await raiseHand(!isHandRaised) } catch (e) { - console.error( - `Failed to toggle hand: ${e instanceof Error ? e.message : 'Unknown error'}` - ) + reportError('generic_failure', e, { + context: 'toggle_raised_hand', + }) } } diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index a92e0289..e6bfe693 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -11,6 +11,7 @@ import { SidePanel } from '../components/SidePanel' import { RecordingProvider } from '@/features/recording' import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal' import { ConnectionObserver } from '../components/ConnectionObserver' +import { reportError } from '@/features/analytics/telemetry' import { MediaStateObserver } from '../components/MediaStateObserver' import { RoomMetadataSynchronizer } from '../components/RoomMetadataSynchronizer' import { useRoomPageTitle } from '../hooks/useRoomPageTitle' @@ -91,7 +92,10 @@ export function VideoConference({ ...props }: VideoConferenceProps) { { - console.error(e) + reportError('device_switch_failure', e.error, { + at: 'ControlBar.onDeviceError', + source: e.source, + }) if ( e.source == Track.Source.ScreenShare && e.error.toString() == diff --git a/src/frontend/src/features/rooms/utils/error.ts b/src/frontend/src/features/rooms/utils/error.ts deleted file mode 100644 index c7556b39..00000000 --- a/src/frontend/src/features/rooms/utils/error.ts +++ /dev/null @@ -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)) -} diff --git a/src/frontend/src/features/sdk/routes/CreatePopup.tsx b/src/frontend/src/features/sdk/routes/CreatePopup.tsx index 36fe20dd..ece8c9f3 100644 --- a/src/frontend/src/features/sdk/routes/CreatePopup.tsx +++ b/src/frontend/src/features/sdk/routes/CreatePopup.tsx @@ -5,6 +5,7 @@ import { useUser } from '@/features/auth/api/useUser' import { Spinner } from '@/primitives/Spinner' import { CallbackIdHandler } from '../utils/CallbackIdHandler' import { PopupWindow } from '../utils/PopupWindow' +import { reportError } from '@/features/analytics/telemetry' const callbackIdHandler = new CallbackIdHandler() const popupWindow = new PopupWindow() @@ -52,7 +53,9 @@ const CreatePopup = () => { popupWindow.close() }) } catch (error) { - console.error('Failed to create meeting room:', error) + reportError('generic_failure', error, { + context: 'Failed to create meeting room:', + }) } } if (isLoggedIn && callbackId) { diff --git a/src/frontend/src/features/sdk/routes/SettingsPopup.tsx b/src/frontend/src/features/sdk/routes/SettingsPopup.tsx index 4246a003..52ca8139 100644 --- a/src/frontend/src/features/sdk/routes/SettingsPopup.tsx +++ b/src/frontend/src/features/sdk/routes/SettingsPopup.tsx @@ -15,6 +15,7 @@ import { usePatchRoom } from '@/features/rooms/api/patchRoom' import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom' import { updatePublishSources } from '@/features/rooms/livekit/hooks/usePublishSourcesManager' import { isSubsetOf } from '@/features/rooms/utils/isSubsetOf' +import { reportError } from '@/features/analytics/telemetry' type Source = Track.Source @@ -108,7 +109,7 @@ const SettingsPopup = () => { patchRoom({ roomId: roomSlug, room: { configuration: newConfiguration }, - }).catch((e) => console.error(e)) + }).catch((e) => reportError('generic_failure', e)) } const updateSource = (sources: Source[], enabled: boolean) => { @@ -329,7 +330,7 @@ const SettingsPopup = () => { patchRoom({ roomId: roomSlug, room: { access_level: value as ApiAccessLevel }, - }).catch((e) => console.error(e)) + }).catch((e) => reportError('generic_failure', e)) } items={[ { diff --git a/src/frontend/src/features/sdk/utils/PopupWindow.ts b/src/frontend/src/features/sdk/utils/PopupWindow.ts index 7d6c0590..f9cb3a87 100644 --- a/src/frontend/src/features/sdk/utils/PopupWindow.ts +++ b/src/frontend/src/features/sdk/utils/PopupWindow.ts @@ -1,5 +1,6 @@ import { authUrl } from '@/features/auth/utils/authUrl' import { PopupMessageType, CallbackCreationRoomData } from './types' +import { reportError } from '@/features/analytics/telemetry' export class PopupWindow { private sendMessageToManager( @@ -9,7 +10,7 @@ export class PopupWindow { callback?: () => void ) { if (!window.opener) { - console.error('No manager window found') + reportError('generic_failure', new Error('No manager window found')) window.close() return } diff --git a/src/frontend/src/features/settings/components/tabs/AccountTab.tsx b/src/frontend/src/features/settings/components/tabs/AccountTab.tsx index 0563b732..0741d4b5 100644 --- a/src/frontend/src/features/settings/components/tabs/AccountTab.tsx +++ b/src/frontend/src/features/settings/components/tabs/AccountTab.tsx @@ -11,6 +11,7 @@ import { useRenameParticipant } from '@/features/rooms/api/renameParticipant' import { saveUsername } from '@/stores/user' import { logout } from '@/features/auth/utils/logout' import { useConfig } from '@/api/useConfig' +import { reportError } from '@/features/analytics/telemetry' export type AccountTabProps = Pick & Pick @@ -35,9 +36,9 @@ export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => { saveUsername(name) onOpenChange?.(false) // only close on success } catch (error) { - console.error( - `Failed to rename participant: ${error instanceof Error ? error.message : 'Unknown error'}` - ) + reportError('generic_failure', error, { + context: 'rename_participant', + }) } } const handleOnCancel = () => { diff --git a/src/frontend/src/primitives/Checkbox.tsx b/src/frontend/src/primitives/Checkbox.tsx index 06445e94..3eece794 100644 --- a/src/frontend/src/primitives/Checkbox.tsx +++ b/src/frontend/src/primitives/Checkbox.tsx @@ -112,9 +112,6 @@ export const Checkbox = ({ const descriptionId = useId() if (isInvalid !== undefined) { - console.error( - 'Checkbox: passing isInvalid is not supported, use the validate prop instead' - ) return null } diff --git a/src/frontend/src/stores/accessibility.ts b/src/frontend/src/stores/accessibility.ts index 5d1bec48..7c3eb1a9 100644 --- a/src/frontend/src/stores/accessibility.ts +++ b/src/frontend/src/stores/accessibility.ts @@ -1,6 +1,7 @@ import { proxy, subscribe } from 'valtio' import { STORAGE_KEYS } from '@/utils/storageKeys' import { deserializeToProxyMap } from '@/utils/valtio' +import { reportError } from '@/features/analytics/telemetry' export type UiFont = | 'default' @@ -154,10 +155,9 @@ function getAccessibilityState(): AccessibilityState { return DEFAULT_STATE } catch (error: unknown) { - console.error( - '[AccessibilityStore] Failed to parse stored settings:', - error - ) + reportError('generic_failure', error, { + context: '[AccessibilityStore] Failed to parse stored settings:', + }) return DEFAULT_STATE } } diff --git a/src/frontend/src/stores/notifications.ts b/src/frontend/src/stores/notifications.ts index 4416d004..03bcfad3 100644 --- a/src/frontend/src/stores/notifications.ts +++ b/src/frontend/src/stores/notifications.ts @@ -3,6 +3,7 @@ import { proxyMap } from 'valtio/utils' import { deserializeToProxyMap, serializeProxyMap } from '@/utils/valtio' import { STORAGE_KEYS } from '@/utils/storageKeys' import { NotificationType } from '@/features/notifications/NotificationType' +import { reportError } from '@/features/analytics/telemetry' type State = { soundNotifications: Map @@ -41,10 +42,9 @@ function getNotificationsState(): State { ), } } catch (error: unknown) { - console.error( - '[NotificationsStore] Failed to parse stored settings:', - error - ) + reportError('generic_failure', error, { + context: '[NotificationsStore] Failed to parse stored settings:', + }) return DEFAULT_STATE } } diff --git a/src/frontend/src/stores/user.ts b/src/frontend/src/stores/user.ts index 7bd2b16e..ac9664b2 100644 --- a/src/frontend/src/stores/user.ts +++ b/src/frontend/src/stores/user.ts @@ -1,5 +1,6 @@ import { proxy, subscribe } from 'valtio' import { STORAGE_KEYS } from '@/utils/storageKeys' +import { reportError } from '@/features/analytics/telemetry' type State = { username: string @@ -18,10 +19,9 @@ function getUserState(): State { ...parsed, } } catch (error: unknown) { - console.error( - '[UserPreferencesStore] Failed to parse stored settings:', - error - ) + reportError('generic_failure', error, { + context: '[UserStore] Failed to parse stored settings:', + }) return DEFAULT_STATE } } diff --git a/src/frontend/src/stores/userPreferences.ts b/src/frontend/src/stores/userPreferences.ts index 0d19e1fd..33ab68c4 100644 --- a/src/frontend/src/stores/userPreferences.ts +++ b/src/frontend/src/stores/userPreferences.ts @@ -1,5 +1,6 @@ import { proxy, subscribe } from 'valtio' import { STORAGE_KEYS } from '@/utils/storageKeys' +import { reportError } from '@/features/analytics/telemetry' type State = { is_idle_disconnect_modal_enabled: boolean @@ -21,10 +22,9 @@ function getUserPreferencesState(): State { ...parsed, } } catch (error: unknown) { - console.error( - '[UserPreferencesStore] Failed to parse stored settings:', - error - ) + reportError('generic_failure', error, { + context: '[UserPreferencesStore] Failed to parse stored settings:', + }) return DEFAULT_STATE } } diff --git a/src/frontend/src/utils/mergeProps.ts b/src/frontend/src/utils/mergeProps.ts index 1e77555d..77404ca3 100644 --- a/src/frontend/src/utils/mergeProps.ts +++ b/src/frontend/src/utils/mergeProps.ts @@ -13,6 +13,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import clsx from 'clsx' +import { reportError } from '@/features/analytics/telemetry' /** * 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 { callback(...args) } catch (e) { - console.error(e) + reportError('generic_failure', e) } } }