Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine 1d3281da7f 🔥(frontend) drop unused vendored ConnectionObserver
The vendored ConnectionObserver collected connection data that never
turned out to be useful for debugging.

Remove it to reduce dead code, and re-add a targeted observer later
if a concrete debugging need shows up.
2026-08-13 10:26:55 +02:00
16 changed files with 54 additions and 187 deletions
-7
View File
@@ -11,19 +11,12 @@ and this project adheres to
### Changed
- 🔥(frontend) drop unused vendored ConnectionObserver
- 🐛(frontend) vendor formatChatMessageLinks and trim surrounding newlines
### Fixed
- 📈(frontend) downgrade unreachable external home URL from error to event
- 🐛(frontend) handle 401 responses when syncing user preferences
- 🐛(frontend) harden speaker test against missing sinks and play errors
- 🐛(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
- 🐛(frontend) generalize screen-share error modal beyond macOS
- 📈(frontend) stop double-reporting media device failures
## [1.26.0] - 2026-08-12
@@ -1,24 +0,0 @@
import type { CaptureResult } from 'posthog-js'
const IGNORED_EXCEPTION_PATTERNS = [
/ResizeObserver loop (completed with undelivered notifications|limit exceeded)/,
]
const shouldIgnoreException = (value: unknown): boolean =>
typeof value === 'string' &&
IGNORED_EXCEPTION_PATTERNS.some((pattern) => pattern.test(value))
export const filterExceptions = (
event: CaptureResult | null
): CaptureResult | null => {
if (event?.event !== '$exception') return event
const exceptionList = event.properties?.['$exception_list']
const values: unknown[] = Array.isArray(exceptionList)
? exceptionList.map((exception) => exception?.value)
: []
values.push(event.properties?.['$exception_message'])
return values.some(shouldIgnoreException) ? null : event
}
@@ -2,7 +2,6 @@ import { useEffect } from 'react'
import { type ApiUser } from '@/features/auth/api/ApiUser'
import { useUser } from '@/features/auth/api/useUser'
import { getPosthog } from '../utils'
import { filterExceptions } from '../exceptionFilters'
export const startAnalyticsSession = (data: ApiUser) => {
getPosthog().then((ph) => {
@@ -48,7 +47,6 @@ export const useAnalytics = ({
capture_unhandled_rejections: true,
capture_console_errors: true,
},
before_send: filterExceptions,
})
})
}, [id, host, flags_api_host, isDisabled])
@@ -23,7 +23,6 @@ export type LogCode =
| 'livekit_room_error'
| 'device_switch_failure'
| 'permission_poll_failure'
| 'media_devices_error_event'
// non-media families
| 'participant_mute_api_failure'
| 'permissions_api_failure'
@@ -138,7 +137,6 @@ 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'
@@ -1,6 +1,6 @@
import { ChatRow } from '@/stores/chat'
import React, { useMemo } from 'react'
import { formatChatMessageLinks } from '../utils'
import { formatChatMessageLinks } from '@livekit/components-react'
import { css } from '@/styled-system/css'
import { Text } from '@/primitives'
-32
View File
@@ -1,32 +0,0 @@
import { tokenize, createDefaultGrammar } from '@livekit/components-core'
import { ReactNode } from 'react'
const defaultGrammar = Object.freeze(createDefaultGrammar())
export function formatChatMessageLinks(message: string): ReactNode {
const trimmedMessage = message.replace(/^[\r\n]+|[\r\n]+$/g, '')
return tokenize(trimmedMessage, defaultGrammar).map((tok, i) => {
if (typeof tok === `string`) {
return tok
} else {
const content = tok.content.toString()
const href =
tok.type === `url`
? /^http(s?):\/\//.test(content)
? content
: `https://${content}`
: `mailto:${content}`
return (
<a
className="lk-chat-link"
key={i}
href={href}
target="_blank"
rel="noreferrer"
>
{content}
</a>
)
}
})
}
@@ -26,8 +26,8 @@ const StyledContainer = styled('div', {
backgroundColor: 'primaryDark.100',
maxWidth: '100%',
opacity: 0,
translate: '0 3.25rem',
transition: 'opacity, translate',
transform: 'translateY(3.25rem)',
transition: 'opacity, transform',
transitionDuration: '0.5s',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
pointerEvents: 'none',
@@ -36,7 +36,7 @@ const StyledContainer = styled('div', {
isVisible: {
true: {
opacity: 1,
translate: '0 0',
transform: 'translateY(0)',
pointerEvents: 'auto',
},
},
@@ -84,7 +84,7 @@ export const ReactionButtonsContainer = ({
shouldBeCenteredWithToggleButton,
setShouldBeCenteredWithToggleButton,
] = useState(false)
const [offsetX, setOffsetX] = useState(0)
const [rightOffset, setRightOffset] = useState(0)
const updateArrows = useCallback(() => {
const el = scrollRef.current
@@ -115,7 +115,7 @@ export const ReactionButtonsContainer = ({
useLayoutEffect(() => {
if (!shouldBeCenteredWithToggleButton || isMobile) {
setOffsetX(0)
setRightOffset(0)
return
}
@@ -133,7 +133,7 @@ export const ReactionButtonsContainer = ({
const containerCenterX = containerRect.left + containerRect.width / 2
const shift = toggleCenterX - containerCenterX
if (Math.abs(shift) < 0.5) return
setOffsetX((prev) => prev + shift)
setRightOffset((prev) => prev - shift * 2)
}
const schedule = () => {
@@ -182,7 +182,7 @@ export const ReactionButtonsContainer = ({
isVisible={isVisible}
style={
shouldBeCenteredWithToggleButton && !isMobile && adjustedCentering
? { transform: `translateX(${offsetX}px)` }
? { marginRight: `${rightOffset}px` }
: { margin: '0 15px' }
}
>
@@ -225,10 +225,9 @@ export const Conference = ({
backgroundColor: 'primaryDark.50 !important',
})}
onError={(e) => {
const failure = MediaDeviceFailure.getFailure(e)
if (failure && failure !== MediaDeviceFailure.Other) return
reportError('livekit_room_error', e, {
path: 'connect_publish',
failure: MediaDeviceFailure.getFailure(e) ?? 'not-a-device-error',
})
}}
onConnected={async () => {
@@ -1,13 +1,6 @@
import { A, Button, Dialog, P } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { css } from '@/styled-system/css'
import { getOS, type OS } from '@/utils/os'
const SCREEN_CAPTURE_SETTINGS_LINKS: Partial<Record<OS, string>> = {
macos:
'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture',
windows: 'ms-settings:privacy-graphicscaptureprogrammatic',
}
// todo - refactor it into a generic system
export const ScreenShareErrorModal = ({
@@ -18,8 +11,7 @@ export const ScreenShareErrorModal = ({
onClose: () => void
}) => {
const { t } = useTranslation('rooms', { keyPrefix: 'error.screenShare' })
const os = getOS()
const settingsHref = SCREEN_CAPTURE_SETTINGS_LINKS[os]
const isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1
return (
<Dialog
@@ -34,16 +26,15 @@ export const ScreenShareErrorModal = ({
<>
<P>
{t('message')}{' '}
{settingsHref && (
{isMac && (
<>
{t('settingsInstructions')}{' '}
{t('macInstructions')}{' '}
<A
href={settingsHref}
target="_blank"
href="x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"
color="primary"
aria-label={t(`settingsLabel.${os}`) + '-' + t('newTab')}
aria-label={t('macSystemPreferences') + '-' + t('newTab')}
>
{t(`settingsLabel.${os}`)}
{t('macSystemPreferences')}
</A>
.{' '}
</>
@@ -50,16 +50,15 @@ export const useWatchMediaDeviceErrors = (): MediaDeviceAlert & {
useEffect(() => {
const onDeviceError = (error: Error, kind?: MediaDeviceKind) => {
const failure = MediaDeviceFailure.getFailure(error)
if (!failure) return
if (failure != MediaDeviceFailure.Other) {
void captureMediaEvent('media-device-error', {
log_code: 'media_devices_error_event',
path: 'connect_publish',
failure,
kind: kind ?? 'unknown',
})
}
if (!kind) return
if (!failure || !kind) return
void captureMediaEvent('media-device-error', {
log_code: 'media_devices_error_event',
path: 'connect_publish',
failure,
kind,
})
const permissionKind = PERMISSION_BY_DEVICE_KIND[kind]
switch (failure) {
case MediaDeviceFailure.DeviceInUse:
@@ -12,8 +12,7 @@ import type { ToggleButtonProps } from '@/primitives/ToggleButton'
import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
const CONTROL_BAR_BREAKPOINT_WIDE = 1100
const CONTROL_BAR_BREAKPOINT_NARROW = 1050
const CONTROL_BAR_BREAKPOINT = 1100
const NavigationControls = ({
onPress,
@@ -66,9 +65,10 @@ export const LateralMenu = () => {
</DialogTrigger>
)
}
interface BreakpointObserverProps {
containerRef: RefObject<HTMLDivElement>
onWideChange: (isWide: boolean | null) => void
onWideChange: (isWide: boolean) => void
}
const BreakpointObserver = ({
@@ -76,20 +76,7 @@ const BreakpointObserver = ({
onWideChange,
}: BreakpointObserverProps) => {
const { width } = useSize(containerRef)
const [isWide, setIsWide] = useState<boolean | null>(null)
useEffect(() => {
if (!width) {
return
}
if (width > CONTROL_BAR_BREAKPOINT_WIDE) {
setIsWide(true)
} else if (width <= CONTROL_BAR_BREAKPOINT_NARROW) {
setIsWide(false)
} else {
setIsWide((prev) => (prev === null ? false : prev))
}
}, [width])
const isWide = width > CONTROL_BAR_BREAKPOINT
useEffect(() => {
onWideChange(isWide)
@@ -103,7 +90,7 @@ export const MoreOptions = ({
}: {
parentElement: RefObject<HTMLDivElement>
}) => {
const [isWide, setIsWide] = useState<boolean | null>(null)
const [isWide, setIsWide] = useState(false)
return (
<nav
@@ -120,7 +107,7 @@ export const MoreOptions = ({
containerRef={parentElement}
onWideChange={setIsWide}
/>
{isWide !== null && (isWide ? <NavigationControls /> : <LateralMenu />)}
{isWide ? <NavigationControls /> : <LateralMenu />}
</nav>
)
}
@@ -11,9 +11,7 @@ import { SidePanel } from '../components/SidePanel'
import { RecordingProvider } from '@/features/recording'
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
import { ConnectionObserver } from '../components/ConnectionObserver'
import { captureMediaEvent, reportError } from '@/features/analytics/telemetry'
import { getOS } from '@/utils/os'
import { isFireFox } from '@/utils/livekit'
import { reportError } from '@/features/analytics/telemetry'
import { MediaStateObserver } from '../components/MediaStateObserver'
import { RoomMetadataSynchronizer } from '../components/RoomMetadataSynchronizer'
import { useNoiseReduction } from '../hooks/useNoiseReduction'
@@ -38,20 +36,6 @@ export interface VideoConferenceProps extends React.HTMLAttributes<HTMLDivElemen
SettingsComponent?: React.ComponentType
}
const getScreenSharePermissionDeniedScope = (
error: Error
): 'system' | 'user' | 'browser' | null => {
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
@@ -77,34 +61,6 @@ 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 (
<>
<RoomMetadataSynchronizer />
@@ -136,7 +92,21 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
<StageLayout />
)}
</RoomContentArea>
<ControlBar onDeviceError={handleDeviceError} />
<ControlBar
onDeviceError={(e) => {
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)
}
}}
/>
<SidePanel />
</>
)}
+2 -5
View File
@@ -214,11 +214,8 @@
"title": "Bildschirmfreigabe nicht möglich",
"ariaLabel": "Bildschirmfreigabe nicht möglich",
"message": "Deinem Browser fehlt möglicherweise die Berechtigung, den Bildschirm deines Geräts abzugreifen.",
"settingsInstructions": "Gehe zu den",
"settingsLabel": {
"macos": "Systemeinstellungen",
"windows": "Windows-Datenschutzeinstellungen"
},
"macInstructions": "Gehe zu den",
"macSystemPreferences": "Systemeinstellungen",
"helpLinkText": "Weitere Informationen findest du unter",
"helpLinkLabel": "Bildschirmfreigabeproblem",
"closeButton": "Schließen",
+2 -5
View File
@@ -214,11 +214,8 @@
"title": "Unable to share your screen",
"ariaLabel": "Unable to share your screen",
"message": "Your browser may not be allowed to record the screen on your computer.",
"settingsInstructions": "Go to your",
"settingsLabel": {
"macos": "System Preferences",
"windows": "Windows privacy settings"
},
"macInstructions": "Go to your",
"macSystemPreferences": "System Preferences",
"helpLinkText": "To learn more, see",
"helpLinkLabel": "Presentation issue",
"closeButton": "Dismiss",
+2 -5
View File
@@ -214,11 +214,8 @@
"title": "Impossible de partager votre écran",
"ariaLabel": "Impossible de partager votre écran",
"message": "Il se peut que votre navigateur ne soit pas autorisé à enregistrer l'écran sur votre ordinateur.",
"settingsInstructions": "Accédez à vos",
"settingsLabel": {
"macos": "Préférences système",
"windows": "paramètres de confidentialité Windows"
},
"macInstructions": "Accèdez à vos",
"macSystemPreferences": "Préférences système",
"helpLinkText": "Pour en savoir plus, consulter",
"helpLinkLabel": "Problème de présentation",
"closeButton": "Ignorer",
+2 -5
View File
@@ -214,11 +214,8 @@
"title": "Kan uw scherm niet delen",
"ariaLabel": "Kan uw scherm niet delen",
"message": "Het is mogelijk dat uw browser geen toestemming heeft om het scherm op uw computer op te nemen.",
"settingsInstructions": "Ga naar uw",
"settingsLabel": {
"macos": "Systeemvoorkeuren",
"windows": "Windows-privacyinstellingen"
},
"macInstructions": "Ga naar uw",
"macSystemPreferences": "Systeemvoorkeuren",
"helpLinkText": "Meer informatie, zie",
"helpLinkLabel": "Presentatieprobleem",
"closeButton": "Negeren",