From 00776e20fa4f1e3603c5856933739d5e32bcb455 Mon Sep 17 00:00:00 2001 From: Usama Date: Thu, 16 Jul 2026 13:48:08 +0000 Subject: [PATCH 1/2] feat(monitor): instrument Monitor + portal recording with PostHog Add host-facing Monitor analytics and end-to-end portal recording reliability events so we can see adoption, drop-off, and failures. Host dashboard: - monitor_opened/closed, monitor_drilldown_opened, monitor_conversation_opened (row + funnel), monitor_participant_name_edited, monitor_locked_row_clicked - monitor_stream_degraded/reconnected/load_failed Portal reliability (participant): - portal_recording_interrupted (at detection), backgrounded/foregrounded, network offline/online, chunk_upload_failed, uploads_incomplete_on_finish, conversation_deleted_during_recording, mic_permission_resolved, abandoned Backend (server_*, keyed on conversation_id): - server_chunk_upload_bad, server_chunk_not_found_in_s3, server_chunk_transcription_failed Notes: - Stream-health state lives on the shared SSE connection so it fires once across the 2-3 concurrent useConversationMonitor mounts, not per mount. - Page-session analytics isolated to a null-rendering leaf so live snapshots don't re-render the Monitor page chrome. - Edge-triggered effects use primitive deps / refs to avoid per-tick reruns. Tests: row + locked click-through and isProblemState (frontend); confirm-upload bad/missing-chunk and transcription-failure captures (backend). --- .../conversation/LiveFunnelSection.tsx | 36 +++- .../conversation/LiveMonitorSection.test.tsx | 179 +++++++++++++++++- .../conversation/LiveMonitorSection.tsx | 36 +++- .../ParticipantConversationAudio.tsx | 111 ++++++++++- .../ParticipantOnboardingCards.tsx | 20 +- .../src/components/participant/hooks/index.ts | 22 ++- .../src/hooks/useConversationMonitor.ts | 53 ++++++ .../routes/project/ProjectMonitorRoute.tsx | 39 +++- echo/server/dembrane/api/participant.py | 31 +-- echo/server/dembrane/transcribe.py | 31 ++- .../test_confirm_chunk_upload_analytics.py | 74 ++++++++ .../test_transcription_failure_analytics.py | 50 +++++ 12 files changed, 645 insertions(+), 37 deletions(-) create mode 100644 echo/server/tests/api/test_confirm_chunk_upload_analytics.py create mode 100644 echo/server/tests/test_transcription_failure_analytics.py diff --git a/echo/frontend/src/components/conversation/LiveFunnelSection.tsx b/echo/frontend/src/components/conversation/LiveFunnelSection.tsx index 65d762036..85c76abab 100644 --- a/echo/frontend/src/components/conversation/LiveFunnelSection.tsx +++ b/echo/frontend/src/components/conversation/LiveFunnelSection.tsx @@ -18,6 +18,7 @@ import { WifiSlashIcon, } from "@phosphor-icons/react"; import { formatDistanceToNow } from "date-fns"; +import posthog from "posthog-js"; import { useEffect, useMemo, useState } from "react"; import { useParams } from "react-router"; @@ -31,7 +32,7 @@ import { useConversationMonitor, } from "@/hooks/useConversationMonitor"; import { FunnelCanvas, type NodeDatum } from "./FunnelCanvas"; -import { StatePill } from "./LiveMonitorSection"; +import { isProblemState, StatePill } from "./LiveMonitorSection"; const weakNetwork = ( network: { online?: boolean; effective_type?: string } | null, @@ -144,9 +145,11 @@ const VisitorDrilldown = ({ visitor }: { visitor: FunnelVisitor }) => ( const ConversationDrilldown = ({ conversation, base, + projectId, }: { conversation: MonitorConversation; base: string | null; + projectId: string; }) => { const [name, setName] = useState(conversation.label ?? ""); const update = useUpdateConversationByIdMutation(); @@ -160,7 +163,13 @@ const ConversationDrilldown = ({ { id: conversation.id, payload: { participant_name: name.trim() } }, { onError: () => toast.error(t`Could not save`), - onSuccess: () => toast.success(t`Saved`), + onSuccess: () => { + posthog.capture("monitor_participant_name_edited", { + conversation_id: conversation.id, + project_id: projectId, + }); + toast.success(t`Saved`); + }, }, ); }; @@ -198,6 +207,16 @@ const ConversationDrilldown = ({ + posthog.capture("monitor_conversation_opened", { + conversation_id: conversation.id, + from_problem_state: isProblemState(conversation), + participant_state: conversation.state, + project_id: projectId, + recording_health: conversation.recording_health, + transcription_status: conversation.transcription_status, + }) + } >