mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-26 11:58:53 +00:00
📈(frontend) track WebRTC peer candidates in PostHog events
Capture selected ICE candidates for both subscriber and publisher peer connections. This enables correlation between survey feedback and connectivity setup, helping identify problematic network configurations.
This commit is contained in:
committed by
aleb_the_flash
parent
4d222e4ab4
commit
bd3a26a2af
@@ -4,4 +4,5 @@ export enum FeatureFlags {
|
||||
faceLandmarks = 'face-landmarks',
|
||||
noiseReduction = 'noise-reduction',
|
||||
subtitles = 'subtitles',
|
||||
candidatePolling = 'candidate-polling',
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ import { useConfig } from '@/api/useConfig'
|
||||
import { isFireFox } from '@/utils/livekit'
|
||||
import { useIsMobile } from '@/utils/useIsMobile'
|
||||
import { navigateTo } from '@/navigation/navigateTo'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { connectionObserverStore } from '@/stores/connectionObserver'
|
||||
|
||||
export const Conference = ({
|
||||
roomId,
|
||||
@@ -44,6 +46,8 @@ export const Conference = ({
|
||||
const posthog = usePostHog()
|
||||
const { data: apiConfig } = useConfig()
|
||||
|
||||
const connectionObserverSnap = useSnapshot(connectionObserverStore)
|
||||
|
||||
const { userChoices: userConfig } = usePersistentUserChoices() as {
|
||||
userChoices: LocalUserChoices
|
||||
}
|
||||
@@ -228,13 +232,32 @@ export const Conference = ({
|
||||
posthog.captureException(e)
|
||||
}}
|
||||
onDisconnected={(e) => {
|
||||
const metadata = {
|
||||
room_id: roomId,
|
||||
pc_publisher: connectionObserverSnap.publisher && {
|
||||
...connectionObserverSnap.publisher,
|
||||
},
|
||||
pc_subscriber: connectionObserverSnap.subscriber && {
|
||||
...connectionObserverSnap.subscriber,
|
||||
},
|
||||
pc_publisher_changes_count:
|
||||
connectionObserverSnap.publisherChangesCount,
|
||||
pc_subscriber_changes_count:
|
||||
connectionObserverSnap.subscriberChangesCount,
|
||||
}
|
||||
|
||||
connectionObserverStore.publisher = null
|
||||
connectionObserverStore.publisherChangesCount = 0
|
||||
connectionObserverStore.subscriber = null
|
||||
connectionObserverStore.subscriberChangesCount = 0
|
||||
|
||||
switch (e) {
|
||||
case DisconnectReason.CLIENT_INITIATED:
|
||||
navigateTo(
|
||||
'feedback',
|
||||
{},
|
||||
{
|
||||
state: { room_id: roomId },
|
||||
state: { ...metadata },
|
||||
}
|
||||
)
|
||||
return
|
||||
@@ -244,7 +267,10 @@ export const Conference = ({
|
||||
'feedback',
|
||||
{},
|
||||
{
|
||||
state: { reason: e, room_id: roomId },
|
||||
state: {
|
||||
reason: e,
|
||||
...metadata,
|
||||
},
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
@@ -7,6 +7,7 @@ import { usePostHog } from 'posthog-js/react'
|
||||
import type { PostHog } from 'posthog-js'
|
||||
import { Button as RACButton } from 'react-aria-components'
|
||||
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
|
||||
import { CandidateInfo } from '@/stores/connectionObserver'
|
||||
|
||||
const Card = styled('div', {
|
||||
base: {
|
||||
@@ -303,7 +304,19 @@ const AuthenticationMessage = ({
|
||||
)
|
||||
}
|
||||
|
||||
export const Rating = ({ roomId }: { roomId?: string }) => {
|
||||
type RatingMetadata = {
|
||||
room_id?: string
|
||||
pc_publisher?: CandidateInfo
|
||||
pc_subscriber?: CandidateInfo
|
||||
pc_publisher_changes_count?: number
|
||||
pc_subscriber_changes_count?: number
|
||||
}
|
||||
|
||||
export const Rating = ({
|
||||
metadata: metadataProp,
|
||||
}: {
|
||||
metadata: RatingMetadata
|
||||
}) => {
|
||||
const isAnalyticsEnabled = useIsAnalyticsEnabled()
|
||||
const posthog = usePostHog()
|
||||
|
||||
@@ -318,9 +331,9 @@ export const Rating = ({ roomId }: { roomId?: string }) => {
|
||||
const metadata = useMemo(
|
||||
() => ({
|
||||
session_id: sessionId,
|
||||
room_id: roomId,
|
||||
...metadataProp,
|
||||
}),
|
||||
[roomId, sessionId]
|
||||
[sessionId, metadataProp]
|
||||
)
|
||||
|
||||
if (!isAnalyticsEnabled) return
|
||||
|
||||
@@ -10,6 +10,9 @@ import { connectionObserverStore } from '@/stores/connectionObserver'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { userPreferencesStore } from '@/stores/userPreferences'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useIsAdvancedConnectionObserverEnabled } from './useIsAdvancedConnectionObserverEnabled'
|
||||
|
||||
const CANDIDATE_POLL_INTERVAL_MS = 5000
|
||||
|
||||
export const useConnectionObserver = () => {
|
||||
const room = useRoomContext()
|
||||
@@ -17,6 +20,8 @@ export const useConnectionObserver = () => {
|
||||
|
||||
const { data } = useConfig()
|
||||
const isAnalyticsEnabled = useIsAnalyticsEnabled()
|
||||
const isAdvancedConnectionObserverEnabled =
|
||||
useIsAdvancedConnectionObserverEnabled()
|
||||
|
||||
const userPreferencesSnap = useSnapshot(userPreferencesStore)
|
||||
|
||||
@@ -67,6 +72,100 @@ export const useConnectionObserver = () => {
|
||||
userPreferencesSnap.is_idle_disconnect_modal_enabled,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAdvancedConnectionObserverEnabled) return
|
||||
if (!room) return
|
||||
|
||||
let interval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const pollCandidate = async (
|
||||
label: 'publisher' | 'subscriber',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
pc?: any
|
||||
) => {
|
||||
if (!pc) return
|
||||
|
||||
let stats: RTCStatsReport
|
||||
try {
|
||||
stats = await pc.getStats()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
stats.forEach((report: any) => {
|
||||
if (
|
||||
report.type === 'candidate-pair' &&
|
||||
report.state === 'succeeded' &&
|
||||
report.nominated
|
||||
) {
|
||||
const remoteCandidate = stats.get(report.remoteCandidateId)
|
||||
if (!remoteCandidate) return
|
||||
|
||||
const next = {
|
||||
type: remoteCandidate.candidateType,
|
||||
address: remoteCandidate.address,
|
||||
protocol: remoteCandidate.protocol,
|
||||
}
|
||||
|
||||
const current = connectionObserverStore[label]
|
||||
|
||||
const hasChanged =
|
||||
current?.type !== next.type ||
|
||||
current?.address !== next.address ||
|
||||
current?.protocol !== next.protocol
|
||||
|
||||
if (hasChanged) {
|
||||
connectionObserverStore[label] = next
|
||||
const key = `${label}ChangesCount` as const
|
||||
connectionObserverStore[key] =
|
||||
(connectionObserverStore[key] || 0) + 1
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const poll = async () => {
|
||||
const publisher = room.engine?.pcManager?.publisher
|
||||
const subscriber = room.engine?.pcManager?.subscriber
|
||||
|
||||
await Promise.all([
|
||||
pollCandidate('publisher', publisher),
|
||||
pollCandidate('subscriber', subscriber),
|
||||
])
|
||||
}
|
||||
|
||||
const startPolling = async () => {
|
||||
if (interval) return // prevent duplicates
|
||||
|
||||
// Initial snapshot
|
||||
await poll()
|
||||
|
||||
interval = setInterval(poll, CANDIDATE_POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
const stopPolling = () => {
|
||||
if (!interval) return
|
||||
clearInterval(interval)
|
||||
interval = null
|
||||
}
|
||||
|
||||
room.on(RoomEvent.Connected, startPolling)
|
||||
room.on(RoomEvent.Reconnected, startPolling)
|
||||
|
||||
room.on(RoomEvent.Reconnecting, stopPolling)
|
||||
room.on(RoomEvent.Disconnected, stopPolling)
|
||||
|
||||
return () => {
|
||||
stopPolling()
|
||||
|
||||
room.off(RoomEvent.Connected, startPolling)
|
||||
room.off(RoomEvent.Reconnected, startPolling)
|
||||
room.off(RoomEvent.Reconnecting, stopPolling)
|
||||
room.off(RoomEvent.Disconnected, stopPolling)
|
||||
}
|
||||
}, [room, isAdvancedConnectionObserverEnabled])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAnalyticsEnabled) return
|
||||
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { useFeatureFlagEnabled } from 'posthog-js/react'
|
||||
import { FeatureFlags } from '@/features/analytics/enums.ts'
|
||||
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled.ts'
|
||||
import { isMobileBrowser } from '@livekit/components-core'
|
||||
|
||||
export const useIsAdvancedConnectionObserverEnabled = () => {
|
||||
const featureEnabled = useFeatureFlagEnabled(FeatureFlags.candidatePolling)
|
||||
const isAnalyticsEnabled = useIsAnalyticsEnabled()
|
||||
|
||||
const isMobile = isMobileBrowser()
|
||||
return !isMobile && isAnalyticsEnabled && featureEnabled
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { Rating } from '@/features/rooms/components/Rating.tsx'
|
||||
import { useLocation } from 'wouter'
|
||||
import { useMemo } from 'react'
|
||||
import { DisconnectReason } from 'livekit-client'
|
||||
import { CandidateInfo } from '@/stores/connectionObserver.ts'
|
||||
|
||||
// fixme - duplicated with home, refactor in a proper style
|
||||
const Heading = styled('h1', {
|
||||
@@ -43,9 +44,15 @@ export const FeedbackRoute = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const roomId = useMemo(() => {
|
||||
const metadata = useMemo(() => {
|
||||
const state = window.history.state
|
||||
return state?.room_id
|
||||
return {
|
||||
room_id: state?.room_id as string,
|
||||
pc_publisher: state?.pc_publisher as CandidateInfo,
|
||||
pc_publisher_changes_count: state?.pc_publisher_changes_count as number,
|
||||
pc_subscriber: state?.pc_subscriber as CandidateInfo,
|
||||
pc_subscriber_changes_count: state?.pc_subscriber_changes_count as number,
|
||||
}
|
||||
}, [])
|
||||
|
||||
const showBackButton = reasonKey !== DisconnectReasonKey.ParticipantRemoved
|
||||
@@ -65,7 +72,7 @@ export const FeedbackRoute = () => {
|
||||
{t('feedback.home')}
|
||||
</Button>
|
||||
</HStack>
|
||||
<Rating roomId={roomId} />
|
||||
<Rating metadata={metadata} />
|
||||
</VStack>
|
||||
</Center>
|
||||
</Screen>
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
import { proxy } from 'valtio'
|
||||
|
||||
export type CandidateInfo = {
|
||||
type: string
|
||||
address: string
|
||||
protocol: string
|
||||
}
|
||||
|
||||
type State = {
|
||||
isIdleDisconnectModalOpen: boolean
|
||||
publisher: CandidateInfo | null
|
||||
publisherChangesCount: number
|
||||
subscriber: CandidateInfo | null
|
||||
subscriberChangesCount: number
|
||||
}
|
||||
|
||||
export const connectionObserverStore = proxy<State>({
|
||||
isIdleDisconnectModalOpen: false,
|
||||
publisher: null,
|
||||
publisherChangesCount: 0,
|
||||
subscriber: null,
|
||||
subscriberChangesCount: 0,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user