From 40e4f17c6594aafdd364290548d967d9e407b970 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Fri, 14 Aug 2026 10:35:21 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(frontend)=20stop=20reporting=20scr?= =?UTF-8?q?een-share=20denials=20as=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a small helper that classifies a `getDisplayMedia` failure as a user, browser, or OS permission denial, or returns null when it is a genuine error. Chromium reports denials with explicit, non-localized messages: * "Permission denied by user" when the user cancels or dismisses the source picker. * "Permission denied by system" when the OS blocks capture (e.g. the macOS Screen Recording privacy setting). * Plain "Permission denied" for browser-level blocks (site settings, enterprise policy, permissions-policy). Firefox and Safari use generic `NotAllowedError` messages, which fall into the "browser" bucket. Firefox additionally does not map macOS Screen Recording (TCC) blocks to `NotAllowedError`: the OS silently returns no capturable sources, so `getDisplayMedia` rejects with `NotFoundError` ("The object can not be found here."). Same quirk as the mic/cam OS blocks handled in `useWatchMediaDeviceErrors` via `isLikelySystemNotFound`. Behavior on a denied screen-share permission: * Denials are expected outcomes (picker cancelled by the user, OS privacy settings, enterprise policy…) and no longer surface as exceptions in error tracking; capture an analytics event instead. * Only OS-level blocks get the modal, since it explains how to unblock them. --- CHANGELOG.md | 1 + .../src/features/analytics/telemetry.ts | 1 + .../rooms/livekit/prefabs/VideoConference.tsx | 62 ++++++++++++++----- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1078d410..31e8df91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to - πŸ›(frontend) implement hysteresis band for the control bar layout - πŸ›(frontend) fix toolbar ResizeObserver loop and alignment drift - πŸ›(analytics) filter benign ResizeObserver loop error in Sentry/PostHog +- πŸ›(frontend) stop reporting screen-share denials as errors ## [1.26.0] - 2026-08-12 diff --git a/src/frontend/src/features/analytics/telemetry.ts b/src/frontend/src/features/analytics/telemetry.ts index d8d64669..8587c001 100644 --- a/src/frontend/src/features/analytics/telemetry.ts +++ b/src/frontend/src/features/analytics/telemetry.ts @@ -137,6 +137,7 @@ export const captureMediaEvent = async ( | 'media-device-success' | 'device-not-found' | 'permissions-denied' + | 'screen-share-permission-denied' | 'silent-mic-detected' | 'silent-mic-analyser-unavailable' | 'silent-mic-recovered' diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index 53f42f67..f470aae7 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -11,7 +11,9 @@ 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 { captureMediaEvent, reportError } from '@/features/analytics/telemetry' +import { getOS } from '@/utils/os' +import { isFireFox } from '@/utils/livekit' import { MediaStateObserver } from '../components/MediaStateObserver' import { RoomMetadataSynchronizer } from '../components/RoomMetadataSynchronizer' import { useNoiseReduction } from '../hooks/useNoiseReduction' @@ -36,6 +38,20 @@ export interface VideoConferenceProps extends React.HTMLAttributes { + if (error.name === 'NotAllowedError') { + if (/by system/i.test(error.message)) return 'system' + if (/by user/i.test(error.message)) return 'user' + return 'browser' + } + if (error.name === 'NotFoundError' && isFireFox() && getOS() === 'macos') { + return 'system' + } + return null +} + /** * The `VideoConference` ready-made component is your drop-in solution for a classic video conferencing application. * It provides functionality such as focusing on one participant, grid view with pagination to handle large numbers @@ -61,6 +77,34 @@ export function VideoConference({ ...props }: VideoConferenceProps) { const [isShareErrorVisible, setIsShareErrorVisible] = useState(false) + const handleDeviceError = ({ + source, + error, + }: { + source: Track.Source + error: Error + }) => { + if (source === Track.Source.ScreenShare) { + const scope = getScreenSharePermissionDeniedScope(error) + if (scope) { + if (scope === 'system') setIsShareErrorVisible(true) + void captureMediaEvent('screen-share-permission-denied', { + at: 'ControlBar.onDeviceError', + source, + error_name: error.name, + error_message: error.message, + denied_scope: scope, + os: getOS(), + }) + return + } + } + reportError('device_switch_failure', error, { + at: 'ControlBar.onDeviceError', + source, + }) + } + return ( <> @@ -92,21 +136,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) { )} - { - reportError('device_switch_failure', e.error, { - at: 'ControlBar.onDeviceError', - source: e.source, - }) - if ( - e.source == Track.Source.ScreenShare && - e.error.toString() == - 'NotAllowedError: Permission denied by system' - ) { - setIsShareErrorVisible(true) - } - }} - /> + )}