From 4a0759e9b8e3f19ad41e31f4f7fc86c66a2e67e0 Mon Sep 17 00:00:00 2001 From: fkwp Date: Thu, 7 May 2026 10:36:36 +0200 Subject: [PATCH] Add Posthog events for Call reconnect including the reason --- src/analytics/PosthogAnalytics.ts | 2 + src/analytics/PosthogEvents.test.ts | 5 ++ src/analytics/PosthogEvents.ts | 55 ++++++++++++++++++- src/state/CallViewModel/CallViewModel.ts | 1 + .../localMember/HomeserverConnected.ts | 21 ++++++- .../localMember/LocalMember.test.ts | 2 + .../CallViewModel/localMember/LocalMember.ts | 18 ++++++ 7 files changed, 102 insertions(+), 2 deletions(-) diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 46223afe..6ec8f8c7 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -26,6 +26,7 @@ import { QualitySurveyEventTracker, CallDisconnectedEventTracker, CallConnectDurationTracker, + CallReconnectingTracker, } from "./PosthogEvents"; import { Config } from "../config/Config"; import { getUrlParams } from "../UrlParams"; @@ -421,4 +422,5 @@ export class PosthogAnalytics { public eventQualitySurvey = new QualitySurveyEventTracker(); public eventCallDisconnected = new CallDisconnectedEventTracker(); public eventCallConnectDuration = new CallConnectDurationTracker(); + public eventCallReconnecting = new CallReconnectingTracker(); } diff --git a/src/analytics/PosthogEvents.test.ts b/src/analytics/PosthogEvents.test.ts index 35b86f5d..8d17084b 100644 --- a/src/analytics/PosthogEvents.test.ts +++ b/src/analytics/PosthogEvents.test.ts @@ -89,6 +89,11 @@ describe("CallEnded", () => { roomEventEncryptionKeysSent: 10, roomEventEncryptionKeysReceived: 5, roomEventEncryptionKeysReceivedAverageAge: 100, + callReconnectingCount: 0, + callReconnectingCountSyncing: 0, + callReconnectingCountMembershipConnected: 0, + callReconnectingCountCertainlyConnected: 0, + callReconnectingCountLivekit: 0, }, { send_instantly: true }, ); diff --git a/src/analytics/PosthogEvents.ts b/src/analytics/PosthogEvents.ts index 5553829a..35fa0c5a 100644 --- a/src/analytics/PosthogEvents.ts +++ b/src/analytics/PosthogEvents.ts @@ -24,12 +24,29 @@ interface CallEnded extends IPosthogEvent { roomEventEncryptionKeysSent: number; roomEventEncryptionKeysReceived: number; roomEventEncryptionKeysReceivedAverageAge: number; + callReconnectingCount: number; + callReconnectingCountSyncing: number; + callReconnectingCountMembershipConnected: number; + callReconnectingCountCertainlyConnected: number; + callReconnectingCountLivekit: number; } export class CallEndedTracker { - private cache: { startTime?: Date; maxParticipantsCount: number } = { + private cache: { + startTime?: Date; + maxParticipantsCount: number; + reconnectingCount: number; + reconnectingCountByReason: Record; + } = { startTime: undefined, maxParticipantsCount: 0, + reconnectingCount: 0, + reconnectingCountByReason: { + syncing: 0, + membershipConnected: 0, + certainlyConnected: 0, + livekit: 0, + }, }; public cacheStartCall(time: Date): void { @@ -43,6 +60,11 @@ export class CallEndedTracker { ); } + public cacheReconnecting(reason: CallReconnectingReason): void { + this.cache.reconnectingCount++; + this.cache.reconnectingCountByReason[reason]++; + } + public track( callId: string, callParticipantsNow: number, @@ -67,6 +89,15 @@ export class CallEndedTracker { .roomEventEncryptionKeysReceivedTotalAge / rtcSession.statistics.counters.roomEventEncryptionKeysReceived : 0, + callReconnectingCount: this.cache.reconnectingCount, + callReconnectingCountSyncing: + this.cache.reconnectingCountByReason.syncing, + callReconnectingCountMembershipConnected: + this.cache.reconnectingCountByReason.membershipConnected, + callReconnectingCountCertainlyConnected: + this.cache.reconnectingCountByReason.certainlyConnected, + callReconnectingCountLivekit: + this.cache.reconnectingCountByReason.livekit, }, { send_instantly: sendInstantly }, ); @@ -249,3 +280,25 @@ export class CallConnectDurationTracker { ); } } + +export type CallReconnectingReason = + | "syncing" + | "membershipConnected" + | "certainlyConnected" + | "livekit"; + +interface CallReconnecting extends IPosthogEvent { + eventName: "CallReconnecting"; + callId: string; + reason: CallReconnectingReason; +} + +export class CallReconnectingTracker { + public track(callId: string, reason: CallReconnectingReason): void { + PosthogAnalytics.instance.trackEvent({ + eventName: "CallReconnecting", + callId, + reason, + }); + } +} diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index e298bcfd..016b930f 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -560,6 +560,7 @@ export function createCallViewModel$( connectionManager, matrixRTCSession, localTransport$, + callId: matrixRoom.roomId, logger: logger.getChild(`[${Date.now()}]`), }); diff --git a/src/state/CallViewModel/localMember/HomeserverConnected.ts b/src/state/CallViewModel/localMember/HomeserverConnected.ts index 65cc24c6..479a261d 100644 --- a/src/state/CallViewModel/localMember/HomeserverConnected.ts +++ b/src/state/CallViewModel/localMember/HomeserverConnected.ts @@ -22,6 +22,7 @@ import { switchMap, of, delay, + combineLatest, } from "rxjs"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; @@ -36,9 +37,15 @@ import { type NodeStyleEventEmitter } from "../../../utils/test"; */ const logger = rootLogger.getChild("[HomeserverConnected]"); +export type HomeserverDisconnectReason = + | "syncing" + | "membershipConnected" + | "certainlyConnected"; + export interface HomeserverConnected { combined$: Behavior; rtsSession$: Behavior; + disconnectReason$: Behavior; } /** @@ -116,5 +123,17 @@ export function createHomeserverConnected$( ), ); - return { combined$, rtsSession$ }; + const disconnectReason$ = scope.behavior( + combineLatest([syncing$, membershipConnected$, certainlyConnected$]).pipe( + map(([syncing, membership, certainly]) => { + if (!syncing) return "syncing" as const; + if (!membership) return "membershipConnected" as const; + if (!certainly) return "certainlyConnected" as const; + return null; + }), + ), + null, + ); + + return { combined$, rtsSession$, disconnectReason$ }; } diff --git a/src/state/CallViewModel/localMember/LocalMember.test.ts b/src/state/CallViewModel/localMember/LocalMember.test.ts index 6eaaa0b0..0e4894ad 100644 --- a/src/state/CallViewModel/localMember/LocalMember.test.ts +++ b/src/state/CallViewModel/localMember/LocalMember.test.ts @@ -217,7 +217,9 @@ describe("LocalMembership", () => { homeserverConnected: { combined$: constant(true), rtsSession$: constant(RTCMemberStatus.Connected), + disconnectReason$: constant(null), }, + callId: "!test-room-id:example.org", }; it("throws error on missing RTC config error", () => { diff --git a/src/state/CallViewModel/localMember/LocalMember.ts b/src/state/CallViewModel/localMember/LocalMember.ts index a935e0aa..167ab1df 100644 --- a/src/state/CallViewModel/localMember/LocalMember.ts +++ b/src/state/CallViewModel/localMember/LocalMember.ts @@ -25,6 +25,7 @@ import { catchError, combineLatest, distinctUntilChanged, + filter, from, fromEvent, map, @@ -34,6 +35,7 @@ import { startWith, switchMap, tap, + withLatestFrom, } from "rxjs"; import { type Logger } from "matrix-js-sdk/lib/logger"; import { deepCompare } from "matrix-js-sdk/lib/utils"; @@ -129,6 +131,7 @@ interface Props { createPublisherFactory: (connection: Connection) => Publisher; joinMatrixRTC: (transport: LivekitTransportConfig) => void; homeserverConnected: HomeserverConnected; + callId: string; localTransport$: Behavior; matrixRTCSession: Pick< MatrixRTCSession, @@ -152,6 +155,7 @@ interface Props { * @param props.logger The logger to use. * @param props.muteStates The mute states for video and audio. * @param props.matrixRTCSession The matrix RTC session to join. + * @param props.callId The room ID used as the call identifier in analytics events. * @returns * - publisher: The handle to create tracks and publish them to the room. * - connected$: the current connection state. Including matrix server and livekit server connection. (only considering the livekit server we are using for our own media publication) @@ -169,6 +173,7 @@ export const createLocalMembership$ = ({ logger: parentLogger, muteStates, matrixRTCSession, + callId, }: Props): { /** * This request to start audio and video tracks. @@ -519,6 +524,19 @@ export const createLocalMembership$ = ({ false, ); + reconnecting$ + .pipe( + distinctUntilChanged(), + filter(Boolean), + withLatestFrom(homeserverConnected.disconnectReason$, localConnectionState$), + scope.bind(), + ) + .subscribe(([_, homeserverReason]) => { + const reason = homeserverReason !== null ? homeserverReason : "livekit"; + PosthogAnalytics.instance.eventCallReconnecting.track(callId, reason); + PosthogAnalytics.instance.eventCallEnded.cacheReconnecting(reason); + }); + // inform the widget about the connect and disconnect intent from the user. scope .behavior(joinAndPublishRequested$.pipe(pairwise(), scope.bind()), [