diff --git a/CHANGELOG.md b/CHANGELOG.md index f73cdd09..bf88bc4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to ### Changed - ♻️(frontend) encapsulate error tracking behind a telemetry module +- ♻️(frontend) encapsulate PostHog capture calls in the telemetry module ## [1.25.2] - 2026-08-06 diff --git a/src/frontend/src/features/analytics/telemetry.ts b/src/frontend/src/features/analytics/telemetry.ts index 2f68a12b..5d87a1b3 100644 --- a/src/frontend/src/features/analytics/telemetry.ts +++ b/src/frontend/src/features/analytics/telemetry.ts @@ -1,5 +1,21 @@ import { getPosthog } from './utils' +export const captureEvent = ( + event: string, + props?: Record +) => { + 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 = // media | 'join_preview_failure' diff --git a/src/frontend/src/features/recording/components/ScreenRecordingSidePanel.tsx b/src/frontend/src/features/recording/components/ScreenRecordingSidePanel.tsx index f3bb161b..73c45ed7 100644 --- a/src/frontend/src/features/recording/components/ScreenRecordingSidePanel.tsx +++ b/src/frontend/src/features/recording/components/ScreenRecordingSidePanel.tsx @@ -16,7 +16,6 @@ import { notifyRecordingSaveInProgress, useNotifyParticipants, } from '@/features/notifications' -import posthog from 'posthog-js' import { useConfig } from '@/api/useConfig' import { NoAccessView } from './NoAccessView' 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 { FeatureFlags } from '@/features/analytics/enums' import { LimitDescription } from './LimitDescription' -import { reportError } from '@/features/analytics/telemetry' +import { captureEvent, reportError } from '@/features/analytics/telemetry' export const ScreenRecordingSidePanel = () => { const { data } = useConfig() @@ -64,7 +63,7 @@ export const ScreenRecordingSidePanel = () => { await notifyParticipants({ type: NotificationType.ScreenRecordingRequested, }) - posthog.capture('screen-recording-requested', {}) + captureEvent('screen-recording-requested', {}) } const handleScreenRecording = async () => { @@ -101,7 +100,7 @@ export const ScreenRecordingSidePanel = () => { await notifyParticipants({ type: NotificationType.ScreenRecordingStarted, }) - posthog.capture('screen-recording-started', { + captureEvent('screen-recording-started', { includeTranscript: includeTranscript, language: selectedLanguageKey, }) diff --git a/src/frontend/src/features/recording/components/TranscriptSidePanel.tsx b/src/frontend/src/features/recording/components/TranscriptSidePanel.tsx index 09cd4196..1abada47 100644 --- a/src/frontend/src/features/recording/components/TranscriptSidePanel.tsx +++ b/src/frontend/src/features/recording/components/TranscriptSidePanel.tsx @@ -17,7 +17,6 @@ import { useNotifyParticipants, notifyRecordingSaveInProgress, } from '@/features/notifications' -import posthog from 'posthog-js' import { useConfig } from '@/api/useConfig' import { VStack } from '@/styled-system/jsx' 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 { LimitDescription } from './LimitDescription' import { openSettingsDialog } from '@/stores/settings' -import { reportError } from '@/features/analytics/telemetry' +import { captureEvent, reportError } from '@/features/analytics/telemetry' export const TranscriptSidePanel = () => { const { data } = useConfig() @@ -77,7 +76,7 @@ export const TranscriptSidePanel = () => { await notifyParticipants({ type: NotificationType.TranscriptionRequested, }) - posthog.capture('transcript-requested', {}) + captureEvent('transcript-requested', {}) } const handleTranscript = async () => { @@ -122,7 +121,7 @@ export const TranscriptSidePanel = () => { await notifyParticipants({ type: NotificationType.TranscriptionStarted, }) - posthog.capture('transcript-started', { + captureEvent('transcript-started', { includeScreenRecording: includeScreenRecording, language: selectedLanguageKey, }) diff --git a/src/frontend/src/features/rooms/components/Conference.tsx b/src/frontend/src/features/rooms/components/Conference.tsx index 9be2556f..92e517ed 100644 --- a/src/frontend/src/features/rooms/components/Conference.tsx +++ b/src/frontend/src/features/rooms/components/Conference.tsx @@ -26,8 +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 { captureEvent, reportError } from '@/features/analytics/telemetry' import { useConfig } from '@/api/useConfig' import { isFireFox } from '@/utils/livekit' import { useIsMobile } from '@/utils/useIsMobile' @@ -48,7 +47,6 @@ export const Conference = ({ mode?: 'join' | 'create' initialRoomData?: ApiRoom }) => { - const posthog = usePostHog() const { data: apiConfig } = useConfig() const { userChoices: userConfig } = usePersistentUserChoices() as { @@ -58,8 +56,8 @@ export const Conference = ({ const { username } = useSnapshot(userStore) useEffect(() => { - posthog.capture('visit-room', { slug: roomId }) - }, [roomId, posthog]) + captureEvent('visit-room', { slug: roomId }) + }, [roomId]) const fetchKey = [keys.room, roomId] const [isConnectionWarmedUp, setIsConnectionWarmedUp] = useState(false) diff --git a/src/frontend/src/features/rooms/components/Rating.tsx b/src/frontend/src/features/rooms/components/Rating.tsx index b39870c0..ec2f20fe 100644 --- a/src/frontend/src/features/rooms/components/Rating.tsx +++ b/src/frontend/src/features/rooms/components/Rating.tsx @@ -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 { cva } from '@/styled-system/css' import { useTranslation } from 'react-i18next' 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 { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled' import type { CandidateInfo } from '@/stores/connectionObserver' +import { captureEvent } from '@/features/analytics/telemetry' const Card = styled('div', { base: { @@ -72,11 +71,9 @@ const labelRecipe = cva({ }) const OpenFeedback = ({ - posthog, onNext, metadata, }: { - posthog: PostHog onNext: () => void metadata?: Record }) => { @@ -90,7 +87,7 @@ const OpenFeedback = ({ const onSubmit = () => { try { - posthog.capture('open-feedback', { + captureEvent('open-feedback', { feedback, ...metadata, }) @@ -141,12 +138,10 @@ const OpenFeedback = ({ } const RateQuality = ({ - posthog, onNext, metadata, maxRating = 5, }: { - posthog: PostHog onNext: () => void metadata?: Record maxRating?: number @@ -160,7 +155,7 @@ const RateQuality = ({ const onSubmit = () => { try { - posthog.capture('quality-rating', { + captureEvent('quality-rating', { rating: selectedRating, ...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 ( - - {t('heading')} - setEmail(e.target.value)} - style={{ - marginBottom: '1rem', - }} - /> - - - - - - ) -} - type RatingMetadata = { room_id?: string pc_publisher?: CandidateInfo @@ -318,12 +252,6 @@ export const Rating = ({ metadata: RatingMetadata }) => { const isAnalyticsEnabled = useIsAnalyticsEnabled() - const posthog = usePostHog() - - const isUserAnonymous = useMemo(() => { - return posthog.get_property('$user_state') == 'anonymous' - }, [posthog]) - const [step, setStep] = useState(0) const sessionId = useMemo(() => crypto.randomUUID(), []) @@ -339,37 +267,14 @@ export const Rating = ({ if (!isAnalyticsEnabled) return if (step == 0) { - return ( - setStep(step + 1)} - metadata={metadata} - /> - ) + return setStep(step + 1)} metadata={metadata} /> } if (step == 1) { - return ( - setStep(step + 1)} - metadata={metadata} - /> - ) + return setStep(step + 1)} metadata={metadata} /> } if (step == 2) { - return isUserAnonymous ? ( - setStep(step + 1)} - /> - ) : ( - setStep(0)} /> - ) - } - - if (step == 3) { return setStep(0)} /> } } diff --git a/src/frontend/src/features/rooms/livekit/components/ConnectionObserver.tsx b/src/frontend/src/features/rooms/livekit/components/ConnectionObserver.tsx index 83f7f1e0..f4afc6eb 100644 --- a/src/frontend/src/features/rooms/livekit/components/ConnectionObserver.tsx +++ b/src/frontend/src/features/rooms/livekit/components/ConnectionObserver.tsx @@ -11,10 +11,10 @@ import { DisconnectReason, RoomEvent } from 'livekit-client' import { userPreferencesStore } from '@/stores/userPreferences' import { connectionObserverStore } from '@/stores/connectionObserver' -import posthog from 'posthog-js' import { useFeatureFlagEnabled } from 'posthog-js/react' import { isMobileBrowser } from '@livekit/components-core' import { FeatureFlags } from '@/features/analytics/enums' +import { captureEvent } from '@/features/analytics/telemetry' const CANDIDATE_POLL_INTERVAL_MS = 5000 @@ -182,23 +182,23 @@ export const ConnectionObserver = () => { // total session duration from first connect to final disconnect. if (connectionStartTimeRef.current != null) return connectionStartTimeRef.current = Date.now() - posthog.capture('connection-event') + captureEvent('connection-event') } const handleReconnect = () => { - posthog.capture('reconnect-event') + captureEvent('reconnect-event') } const handleReconnected = () => { - posthog.capture('reconnected-event') + captureEvent('reconnected-event') } const handleSignalingConnect = () => { - posthog.capture('signaling-connect-event') + captureEvent('signaling-connect-event') } const handleSignalingReconnect = () => { - posthog.capture('signaling-reconnect-event') + captureEvent('signaling-reconnect-event') } const handleDisconnect = ( @@ -206,7 +206,7 @@ export const ConnectionObserver = () => { ) => { const connectionEndTime = Date.now() - posthog.capture('disconnect-event', { + captureEvent('disconnect-event', { // Calculate total session duration from first connection to final disconnect // This duration is sensitive to refreshing the page. sessionDuration: connectionStartTimeRef.current diff --git a/src/frontend/src/features/rooms/livekit/components/blur/BackgroundCustomProcessor.ts b/src/frontend/src/features/rooms/livekit/components/blur/BackgroundCustomProcessor.ts index d2ce7c28..24cc8a77 100644 --- a/src/frontend/src/features/rooms/livekit/components/blur/BackgroundCustomProcessor.ts +++ b/src/frontend/src/features/rooms/livekit/components/blur/BackgroundCustomProcessor.ts @@ -1,5 +1,4 @@ import type { ProcessorOptions, Track } from 'livekit-client' -import posthog from 'posthog-js' import { FilesetResolver, ImageSegmenter, @@ -18,6 +17,7 @@ import { type ProcessorType, MEDIAPIPE_PATH_WASM, } from '.' +import { captureEvent } from '@/features/analytics/telemetry.ts' const PROCESSING_WIDTH = 256 const PROCESSING_HEIGHT = 144 @@ -100,7 +100,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface { await this.initSegmenter() this._initWorker() - posthog.capture('firefox-blurring-init') + captureEvent('firefox-blurring-init', {}) } _initVirtualBackgroundImage() { diff --git a/src/frontend/src/features/rooms/livekit/components/blur/FaceLandmarksProcessor.ts b/src/frontend/src/features/rooms/livekit/components/blur/FaceLandmarksProcessor.ts index 5b4873cf..130abb0d 100644 --- a/src/frontend/src/features/rooms/livekit/components/blur/FaceLandmarksProcessor.ts +++ b/src/frontend/src/features/rooms/livekit/components/blur/FaceLandmarksProcessor.ts @@ -1,5 +1,4 @@ import type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client' -import posthog from 'posthog-js' import { FilesetResolver, FaceLandmarker, @@ -16,6 +15,7 @@ import { ProcessorType, MEDIAPIPE_PATH_WASM, } from '.' +import { captureEvent } from '@/features/analytics/telemetry' const PROCESSING_WIDTH = 256 * 3 const PROCESSING_HEIGHT = 144 * 3 @@ -101,7 +101,7 @@ export class FaceLandmarksProcessor implements TrackProcessor { await this.initFaceLandmarker() this._initWorker() - posthog.capture('face-landmarks-init') + captureEvent('face-landmarks-init', {}) } _initWorker() { diff --git a/src/frontend/src/features/settings/components/tabs/AudioTab.tsx b/src/frontend/src/features/settings/components/tabs/AudioTab.tsx index 19d7f36d..1c6ef7ad 100644 --- a/src/frontend/src/features/settings/components/tabs/AudioTab.tsx +++ b/src/frontend/src/features/settings/components/tabs/AudioTab.tsx @@ -11,7 +11,6 @@ import { useTranslation } from 'react-i18next' import { SoundTester } from '@/components/SoundTester' import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker' import { useNoiseReductionAvailable } from '@/features/rooms/livekit/hooks/useNoiseReductionAvailable' -import posthog from 'posthog-js' import { RowWrapper } from './layout/RowWrapper' import { useSnapshot } from 'valtio' import { @@ -20,6 +19,7 @@ import { saveNoiseReductionEnabled, userChoicesStore, } from '@/stores/userChoices' +import { captureEvent } from '@/features/analytics/telemetry' export type AudioTabProps = Pick & Pick @@ -128,7 +128,7 @@ export const AudioTab = ({ id }: AudioTabProps) => { isSelected={noiseReductionEnabled} onChange={(v) => { saveNoiseReductionEnabled(v) - if (v) posthog.capture('noise-reduction-init') + if (v) captureEvent('noise-reduction-init') }} > {t('audio.noiseReduction.label')}