♻️(frontend) encapsulate PostHog capture calls in the telemetry module

Move the remaining direct `posthog.capture` calls behind the
telemetry module, so PostHog is only referenced from a single place.

Call sites now use the telemetry API instead of touching PostHog
directly, making it easier to swap the backend later without
changing every call site.
This commit is contained in:
lebaudantoine
2026-08-07 16:10:05 +02:00
committed by aleb_the_flash
parent 48c0cb320e
commit fb3ee56702
10 changed files with 45 additions and 127 deletions
+1
View File
@@ -11,6 +11,7 @@ and this project adheres to
### Changed ### Changed
- ♻️(frontend) encapsulate error tracking behind a telemetry module - ♻️(frontend) encapsulate error tracking behind a telemetry module
- ♻️(frontend) encapsulate PostHog capture calls in the telemetry module
## [1.25.2] - 2026-08-06 ## [1.25.2] - 2026-08-06
@@ -1,5 +1,21 @@
import { getPosthog } from './utils' import { getPosthog } from './utils'
export const captureEvent = (
event: string,
props?: Record<string, unknown>
) => {
void getPosthog()
.then((ph) => {
ph.capture(event, props)
})
.catch(() => {
/* telemetry must never break the app */
})
if (import.meta.env.DEV) {
console.warn(`[telemetry] ${event}`, props)
}
}
export type LogCode = export type LogCode =
// media // media
| 'join_preview_failure' | 'join_preview_failure'
@@ -16,7 +16,6 @@ import {
notifyRecordingSaveInProgress, notifyRecordingSaveInProgress,
useNotifyParticipants, useNotifyParticipants,
} from '@/features/notifications' } from '@/features/notifications'
import posthog from 'posthog-js'
import { useConfig } from '@/api/useConfig' import { useConfig } from '@/api/useConfig'
import { NoAccessView } from './NoAccessView' import { NoAccessView } from './NoAccessView'
import { ControlsButton } from './ControlsButton' import { ControlsButton } from './ControlsButton'
@@ -29,7 +28,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' import { captureEvent, reportError } from '@/features/analytics/telemetry'
export const ScreenRecordingSidePanel = () => { export const ScreenRecordingSidePanel = () => {
const { data } = useConfig() const { data } = useConfig()
@@ -64,7 +63,7 @@ export const ScreenRecordingSidePanel = () => {
await notifyParticipants({ await notifyParticipants({
type: NotificationType.ScreenRecordingRequested, type: NotificationType.ScreenRecordingRequested,
}) })
posthog.capture('screen-recording-requested', {}) captureEvent('screen-recording-requested', {})
} }
const handleScreenRecording = async () => { const handleScreenRecording = async () => {
@@ -101,7 +100,7 @@ export const ScreenRecordingSidePanel = () => {
await notifyParticipants({ await notifyParticipants({
type: NotificationType.ScreenRecordingStarted, type: NotificationType.ScreenRecordingStarted,
}) })
posthog.capture('screen-recording-started', { captureEvent('screen-recording-started', {
includeTranscript: includeTranscript, includeTranscript: includeTranscript,
language: selectedLanguageKey, language: selectedLanguageKey,
}) })
@@ -17,7 +17,6 @@ import {
useNotifyParticipants, useNotifyParticipants,
notifyRecordingSaveInProgress, notifyRecordingSaveInProgress,
} from '@/features/notifications' } from '@/features/notifications'
import posthog from 'posthog-js'
import { useConfig } from '@/api/useConfig' import { useConfig } from '@/api/useConfig'
import { VStack } from '@/styled-system/jsx' import { VStack } from '@/styled-system/jsx'
import { Checkbox } from '@/primitives/Checkbox.tsx' import { Checkbox } from '@/primitives/Checkbox.tsx'
@@ -35,7 +34,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' import { captureEvent, reportError } from '@/features/analytics/telemetry'
export const TranscriptSidePanel = () => { export const TranscriptSidePanel = () => {
const { data } = useConfig() const { data } = useConfig()
@@ -77,7 +76,7 @@ export const TranscriptSidePanel = () => {
await notifyParticipants({ await notifyParticipants({
type: NotificationType.TranscriptionRequested, type: NotificationType.TranscriptionRequested,
}) })
posthog.capture('transcript-requested', {}) captureEvent('transcript-requested', {})
} }
const handleTranscript = async () => { const handleTranscript = async () => {
@@ -122,7 +121,7 @@ export const TranscriptSidePanel = () => {
await notifyParticipants({ await notifyParticipants({
type: NotificationType.TranscriptionStarted, type: NotificationType.TranscriptionStarted,
}) })
posthog.capture('transcript-started', { captureEvent('transcript-started', {
includeScreenRecording: includeScreenRecording, includeScreenRecording: includeScreenRecording,
language: selectedLanguageKey, language: selectedLanguageKey,
}) })
@@ -26,8 +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 { captureEvent, reportError } from '@/features/analytics/telemetry'
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'
import { useIsMobile } from '@/utils/useIsMobile' import { useIsMobile } from '@/utils/useIsMobile'
@@ -48,7 +47,6 @@ export const Conference = ({
mode?: 'join' | 'create' mode?: 'join' | 'create'
initialRoomData?: ApiRoom initialRoomData?: ApiRoom
}) => { }) => {
const posthog = usePostHog()
const { data: apiConfig } = useConfig() const { data: apiConfig } = useConfig()
const { userChoices: userConfig } = usePersistentUserChoices() as { const { userChoices: userConfig } = usePersistentUserChoices() as {
@@ -58,8 +56,8 @@ export const Conference = ({
const { username } = useSnapshot(userStore) const { username } = useSnapshot(userStore)
useEffect(() => { useEffect(() => {
posthog.capture('visit-room', { slug: roomId }) captureEvent('visit-room', { slug: roomId })
}, [roomId, posthog]) }, [roomId])
const fetchKey = [keys.room, roomId] const fetchKey = [keys.room, roomId]
const [isConnectionWarmedUp, setIsConnectionWarmedUp] = useState(false) const [isConnectionWarmedUp, setIsConnectionWarmedUp] = useState(false)
@@ -1,13 +1,12 @@
import { Button, H, Input, Text, TextArea } from '@/primitives' import { Button, H, Text, TextArea } from '@/primitives'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { cva } from '@/styled-system/css' import { cva } from '@/styled-system/css'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { styled, VStack } from '@/styled-system/jsx' import { styled, VStack } from '@/styled-system/jsx'
import { usePostHog } from 'posthog-js/react'
import type { PostHog } from 'posthog-js'
import { Button as RACButton } from 'react-aria-components' import { Button as RACButton } from 'react-aria-components'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled' import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import type { CandidateInfo } from '@/stores/connectionObserver' import type { CandidateInfo } from '@/stores/connectionObserver'
import { captureEvent } from '@/features/analytics/telemetry'
const Card = styled('div', { const Card = styled('div', {
base: { base: {
@@ -72,11 +71,9 @@ const labelRecipe = cva({
}) })
const OpenFeedback = ({ const OpenFeedback = ({
posthog,
onNext, onNext,
metadata, metadata,
}: { }: {
posthog: PostHog
onNext: () => void onNext: () => void
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
}) => { }) => {
@@ -90,7 +87,7 @@ const OpenFeedback = ({
const onSubmit = () => { const onSubmit = () => {
try { try {
posthog.capture('open-feedback', { captureEvent('open-feedback', {
feedback, feedback,
...metadata, ...metadata,
}) })
@@ -141,12 +138,10 @@ const OpenFeedback = ({
} }
const RateQuality = ({ const RateQuality = ({
posthog,
onNext, onNext,
metadata, metadata,
maxRating = 5, maxRating = 5,
}: { }: {
posthog: PostHog
onNext: () => void onNext: () => void
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
maxRating?: number maxRating?: number
@@ -160,7 +155,7 @@ const RateQuality = ({
const onSubmit = () => { const onSubmit = () => {
try { try {
posthog.capture('quality-rating', { captureEvent('quality-rating', {
rating: selectedRating, rating: selectedRating,
...metadata, ...metadata,
}) })
@@ -243,67 +238,6 @@ const ConfirmationMessage = ({ onNext }: { onNext: () => void }) => {
) )
} }
const AuthenticationMessage = ({
onNext,
posthog,
}: {
onNext: () => void
posthog: PostHog
}) => {
const { t } = useTranslation('rooms', { keyPrefix: 'authenticationMessage' })
const [email, setEmail] = useState('')
const onSubmit = () => {
posthog.people.set({ unsafe_email: email })
onNext()
}
return (
<Card
style={{
maxWidth: '380px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}
>
<H lvl={3}>{t('heading')}</H>
<Input
id="emailInput"
name="email"
placeholder={t('placeholder')}
required
value={email}
onChange={(e) => setEmail(e.target.value)}
style={{
marginBottom: '1rem',
}}
/>
<VStack gap="0.5">
<Button
variant="primary"
size="sm"
fullWidth
isDisabled={!email}
onPress={onSubmit}
>
{t('submit')}
</Button>
<Button
invisible
variant="secondary"
size="sm"
fullWidth
onPress={onNext}
>
{t('ignore')}
</Button>
</VStack>
</Card>
)
}
type RatingMetadata = { type RatingMetadata = {
room_id?: string room_id?: string
pc_publisher?: CandidateInfo pc_publisher?: CandidateInfo
@@ -318,12 +252,6 @@ export const Rating = ({
metadata: RatingMetadata metadata: RatingMetadata
}) => { }) => {
const isAnalyticsEnabled = useIsAnalyticsEnabled() const isAnalyticsEnabled = useIsAnalyticsEnabled()
const posthog = usePostHog()
const isUserAnonymous = useMemo(() => {
return posthog.get_property('$user_state') == 'anonymous'
}, [posthog])
const [step, setStep] = useState(0) const [step, setStep] = useState(0)
const sessionId = useMemo(() => crypto.randomUUID(), []) const sessionId = useMemo(() => crypto.randomUUID(), [])
@@ -339,37 +267,14 @@ export const Rating = ({
if (!isAnalyticsEnabled) return if (!isAnalyticsEnabled) return
if (step == 0) { if (step == 0) {
return ( return <RateQuality onNext={() => setStep(step + 1)} metadata={metadata} />
<RateQuality
posthog={posthog}
onNext={() => setStep(step + 1)}
metadata={metadata}
/>
)
} }
if (step == 1) { if (step == 1) {
return ( return <OpenFeedback onNext={() => setStep(step + 1)} metadata={metadata} />
<OpenFeedback
posthog={posthog}
onNext={() => setStep(step + 1)}
metadata={metadata}
/>
)
} }
if (step == 2) { if (step == 2) {
return isUserAnonymous ? (
<AuthenticationMessage
posthog={posthog}
onNext={() => setStep(step + 1)}
/>
) : (
<ConfirmationMessage onNext={() => setStep(0)} />
)
}
if (step == 3) {
return <ConfirmationMessage onNext={() => setStep(0)} /> return <ConfirmationMessage onNext={() => setStep(0)} />
} }
} }
@@ -11,10 +11,10 @@ import { DisconnectReason, RoomEvent } from 'livekit-client'
import { userPreferencesStore } from '@/stores/userPreferences' import { userPreferencesStore } from '@/stores/userPreferences'
import { connectionObserverStore } from '@/stores/connectionObserver' import { connectionObserverStore } from '@/stores/connectionObserver'
import posthog from 'posthog-js'
import { useFeatureFlagEnabled } from 'posthog-js/react' import { useFeatureFlagEnabled } from 'posthog-js/react'
import { isMobileBrowser } from '@livekit/components-core' import { isMobileBrowser } from '@livekit/components-core'
import { FeatureFlags } from '@/features/analytics/enums' import { FeatureFlags } from '@/features/analytics/enums'
import { captureEvent } from '@/features/analytics/telemetry'
const CANDIDATE_POLL_INTERVAL_MS = 5000 const CANDIDATE_POLL_INTERVAL_MS = 5000
@@ -182,23 +182,23 @@ export const ConnectionObserver = () => {
// total session duration from first connect to final disconnect. // total session duration from first connect to final disconnect.
if (connectionStartTimeRef.current != null) return if (connectionStartTimeRef.current != null) return
connectionStartTimeRef.current = Date.now() connectionStartTimeRef.current = Date.now()
posthog.capture('connection-event') captureEvent('connection-event')
} }
const handleReconnect = () => { const handleReconnect = () => {
posthog.capture('reconnect-event') captureEvent('reconnect-event')
} }
const handleReconnected = () => { const handleReconnected = () => {
posthog.capture('reconnected-event') captureEvent('reconnected-event')
} }
const handleSignalingConnect = () => { const handleSignalingConnect = () => {
posthog.capture('signaling-connect-event') captureEvent('signaling-connect-event')
} }
const handleSignalingReconnect = () => { const handleSignalingReconnect = () => {
posthog.capture('signaling-reconnect-event') captureEvent('signaling-reconnect-event')
} }
const handleDisconnect = ( const handleDisconnect = (
@@ -206,7 +206,7 @@ export const ConnectionObserver = () => {
) => { ) => {
const connectionEndTime = Date.now() const connectionEndTime = Date.now()
posthog.capture('disconnect-event', { captureEvent('disconnect-event', {
// Calculate total session duration from first connection to final disconnect // Calculate total session duration from first connection to final disconnect
// This duration is sensitive to refreshing the page. // This duration is sensitive to refreshing the page.
sessionDuration: connectionStartTimeRef.current sessionDuration: connectionStartTimeRef.current
@@ -1,5 +1,4 @@
import type { ProcessorOptions, Track } from 'livekit-client' import type { ProcessorOptions, Track } from 'livekit-client'
import posthog from 'posthog-js'
import { import {
FilesetResolver, FilesetResolver,
ImageSegmenter, ImageSegmenter,
@@ -18,6 +17,7 @@ import {
type ProcessorType, type ProcessorType,
MEDIAPIPE_PATH_WASM, MEDIAPIPE_PATH_WASM,
} from '.' } from '.'
import { captureEvent } from '@/features/analytics/telemetry.ts'
const PROCESSING_WIDTH = 256 const PROCESSING_WIDTH = 256
const PROCESSING_HEIGHT = 144 const PROCESSING_HEIGHT = 144
@@ -100,7 +100,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
await this.initSegmenter() await this.initSegmenter()
this._initWorker() this._initWorker()
posthog.capture('firefox-blurring-init') captureEvent('firefox-blurring-init', {})
} }
_initVirtualBackgroundImage() { _initVirtualBackgroundImage() {
@@ -1,5 +1,4 @@
import type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client' import type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client'
import posthog from 'posthog-js'
import { import {
FilesetResolver, FilesetResolver,
FaceLandmarker, FaceLandmarker,
@@ -16,6 +15,7 @@ import {
ProcessorType, ProcessorType,
MEDIAPIPE_PATH_WASM, MEDIAPIPE_PATH_WASM,
} from '.' } from '.'
import { captureEvent } from '@/features/analytics/telemetry'
const PROCESSING_WIDTH = 256 * 3 const PROCESSING_WIDTH = 256 * 3
const PROCESSING_HEIGHT = 144 * 3 const PROCESSING_HEIGHT = 144 * 3
@@ -101,7 +101,7 @@ export class FaceLandmarksProcessor implements TrackProcessor<Track.Kind> {
await this.initFaceLandmarker() await this.initFaceLandmarker()
this._initWorker() this._initWorker()
posthog.capture('face-landmarks-init') captureEvent('face-landmarks-init', {})
} }
_initWorker() { _initWorker() {
@@ -11,7 +11,6 @@ import { useTranslation } from 'react-i18next'
import { SoundTester } from '@/components/SoundTester' import { SoundTester } from '@/components/SoundTester'
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker' import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
import { useNoiseReductionAvailable } from '@/features/rooms/livekit/hooks/useNoiseReductionAvailable' import { useNoiseReductionAvailable } from '@/features/rooms/livekit/hooks/useNoiseReductionAvailable'
import posthog from 'posthog-js'
import { RowWrapper } from './layout/RowWrapper' import { RowWrapper } from './layout/RowWrapper'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { import {
@@ -20,6 +19,7 @@ import {
saveNoiseReductionEnabled, saveNoiseReductionEnabled,
userChoicesStore, userChoicesStore,
} from '@/stores/userChoices' } from '@/stores/userChoices'
import { captureEvent } from '@/features/analytics/telemetry'
export type AudioTabProps = Pick<DialogProps, 'onOpenChange'> & export type AudioTabProps = Pick<DialogProps, 'onOpenChange'> &
Pick<TabPanelProps, 'id'> Pick<TabPanelProps, 'id'>
@@ -128,7 +128,7 @@ export const AudioTab = ({ id }: AudioTabProps) => {
isSelected={noiseReductionEnabled} isSelected={noiseReductionEnabled}
onChange={(v) => { onChange={(v) => {
saveNoiseReductionEnabled(v) saveNoiseReductionEnabled(v)
if (v) posthog.capture('noise-reduction-init') if (v) captureEvent('noise-reduction-init')
}} }}
> >
{t('audio.noiseReduction.label')} {t('audio.noiseReduction.label')}