From 4ffe3a6d2ac38f177826d184947e8c47cfd81aee Mon Sep 17 00:00:00 2001 From: fkwp Date: Fri, 8 May 2026 12:06:45 +0200 Subject: [PATCH] Track reconnect duration and align with existing analytics pattern --- src/analytics/PosthogAnalytics.ts | 11 +----- src/analytics/PosthogEvents.test.ts | 7 ++-- src/analytics/PosthogEvents.ts | 8 +++- .../localMember/LocalMember.test.ts | 36 +++++++++++------- .../CallViewModel/localMember/LocalMember.ts | 37 ++++++++++++------- 5 files changed, 57 insertions(+), 42 deletions(-) diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 0daf186a..6ec8f8c7 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -27,7 +27,6 @@ import { CallDisconnectedEventTracker, CallConnectDurationTracker, CallReconnectingTracker, - type CallReconnectingReason, } from "./PosthogEvents"; import { Config } from "../config/Config"; import { getUrlParams } from "../UrlParams"; @@ -423,13 +422,5 @@ export class PosthogAnalytics { public eventQualitySurvey = new QualitySurveyEventTracker(); public eventCallDisconnected = new CallDisconnectedEventTracker(); public eventCallConnectDuration = new CallConnectDurationTracker(); - private readonly _eventCallReconnecting = new CallReconnectingTracker(); - - public trackCallReconnecting( - callId: string, - reason: CallReconnectingReason, - ): void { - this._eventCallReconnecting.track(callId, reason); - this.eventCallEnded.cacheReconnecting(reason); - } + public eventCallReconnecting = new CallReconnectingTracker(); } diff --git a/src/analytics/PosthogEvents.test.ts b/src/analytics/PosthogEvents.test.ts index 6e44f6ab..4299f279 100644 --- a/src/analytics/PosthogEvents.test.ts +++ b/src/analytics/PosthogEvents.test.ts @@ -211,12 +211,13 @@ describe("CallReconnecting", () => { it("tracks event with correct shape", () => { const tracker = new CallReconnectingTracker(); - tracker.track("!room:example.org", "syncing"); + tracker.track("!room:example.org", "syncing", 3.5); expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith({ eventName: "CallReconnecting", callId: "!room:example.org", reason: "syncing", + reconnectDuration: 3.5, }); }); @@ -227,10 +228,10 @@ describe("CallReconnecting", () => { "livekit", ] as CallReconnectingReason[])("tracks reason %s correctly", (reason) => { const tracker = new CallReconnectingTracker(); - tracker.track("!room:example.org", reason); + tracker.track("!room:example.org", reason, 1.0); expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith( - expect.objectContaining({ reason }), + expect.objectContaining({ reason, reconnectDuration: 1.0 }), ); }); }); diff --git a/src/analytics/PosthogEvents.ts b/src/analytics/PosthogEvents.ts index 35fa0c5a..9afe76a2 100644 --- a/src/analytics/PosthogEvents.ts +++ b/src/analytics/PosthogEvents.ts @@ -291,14 +291,20 @@ interface CallReconnecting extends IPosthogEvent { eventName: "CallReconnecting"; callId: string; reason: CallReconnectingReason; + reconnectDuration: number; } export class CallReconnectingTracker { - public track(callId: string, reason: CallReconnectingReason): void { + public track( + callId: string, + reason: CallReconnectingReason, + reconnectDuration: number, + ): void { PosthogAnalytics.instance.trackEvent({ eventName: "CallReconnecting", callId, reason, + reconnectDuration, }); } } diff --git a/src/state/CallViewModel/localMember/LocalMember.test.ts b/src/state/CallViewModel/localMember/LocalMember.test.ts index c446b546..c4b780ac 100644 --- a/src/state/CallViewModel/localMember/LocalMember.test.ts +++ b/src/state/CallViewModel/localMember/LocalMember.test.ts @@ -693,11 +693,11 @@ describe("LocalMembership", () => { PosthogAnalytics.resetInstance(); }); - it("fires CallReconnecting with the homeserver reason when homeserver disconnects", async () => { + it("fires CallReconnecting with homeserver reason and duration when reconnected", async () => { const scope = new ObservableScope(); const trackSpy = vi.spyOn( - PosthogAnalytics.instance, - "trackCallReconnecting", + PosthogAnalytics.instance.eventCallReconnecting, + "track", ); const hsConnected$ = new BehaviorSubject(true); @@ -726,23 +726,26 @@ describe("LocalMembership", () => { await flushPromises(); - // Simulate homeserver disconnect with syncing as the reason + // Disconnect with syncing reason, then reconnect disconnectReason$.next("syncing"); hsConnected$.next(false); + disconnectReason$.next(null); + hsConnected$.next(true); expect(trackSpy).toHaveBeenCalledWith( defaultCreateLocalMemberValues.callId, "syncing", + expect.any(Number), ); scope.end(); }); - it("reports livekit reason when livekit disconnects but homeserver is fine", async () => { + it("reports livekit reason when livekit disconnects then reconnects", async () => { const scope = new ObservableScope(); const trackSpy = vi.spyOn( - PosthogAnalytics.instance, - "trackCallReconnecting", + PosthogAnalytics.instance.eventCallReconnecting, + "track", ); const connectionState$ = new BehaviorSubject( @@ -776,22 +779,24 @@ describe("LocalMembership", () => { await flushPromises(); - // Livekit drops while homeserver stays connected + // Livekit drops then recovers connectionState$.next(ConnectionState.LivekitDisconnected); + connectionState$.next(ConnectionState.LivekitConnected); expect(trackSpy).toHaveBeenCalledWith( defaultCreateLocalMemberValues.callId, "livekit", + expect.any(Number), ); scope.end(); }); - it("fires one event per reconnection, not once per condition change", async () => { + it("fires one event per completed reconnection cycle", async () => { const scope = new ObservableScope(); const trackSpy = vi.spyOn( - PosthogAnalytics.instance, - "trackCallReconnecting", + PosthogAnalytics.instance.eventCallReconnecting, + "track", ); const hsConnected$ = new BehaviorSubject(true); @@ -820,26 +825,29 @@ describe("LocalMembership", () => { await flushPromises(); - // First reconnect + // First full reconnect cycle disconnectReason$.next("membershipConnected"); hsConnected$.next(false); - // Reconnected disconnectReason$.next(null); hsConnected$.next(true); - // Second reconnect + // Second full reconnect cycle disconnectReason$.next("certainlyConnected"); hsConnected$.next(false); + disconnectReason$.next(null); + hsConnected$.next(true); expect(trackSpy).toHaveBeenCalledTimes(2); expect(trackSpy).toHaveBeenNthCalledWith( 1, defaultCreateLocalMemberValues.callId, "membershipConnected", + expect.any(Number), ); expect(trackSpy).toHaveBeenNthCalledWith( 2, defaultCreateLocalMemberValues.callId, "certainlyConnected", + expect.any(Number), ); scope.end(); diff --git a/src/state/CallViewModel/localMember/LocalMember.ts b/src/state/CallViewModel/localMember/LocalMember.ts index e22ffe97..1767fa16 100644 --- a/src/state/CallViewModel/localMember/LocalMember.ts +++ b/src/state/CallViewModel/localMember/LocalMember.ts @@ -25,7 +25,6 @@ import { catchError, combineLatest, distinctUntilChanged, - filter, from, fromEvent, map, @@ -35,7 +34,6 @@ import { startWith, switchMap, tap, - withLatestFrom, } from "rxjs"; import { type Logger } from "matrix-js-sdk/lib/logger"; import { deepCompare } from "matrix-js-sdk/lib/utils"; @@ -55,6 +53,7 @@ import { import { ElementWidgetActions, widget } from "../../../widget.ts"; import { getUrlParams } from "../../../UrlParams.ts"; import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics.ts"; +import { type CallReconnectingReason } from "../../../analytics/PosthogEvents.ts"; import { MatrixRTCMode } from "../../../settings/settings.ts"; import { Config } from "../../../config/Config.ts"; import { @@ -524,19 +523,29 @@ export const createLocalMembership$ = ({ false, ); + let reconnectStart: { time: number; reason: CallReconnectingReason } | null = + null; reconnecting$ - .pipe( - distinctUntilChanged(), - filter(Boolean), - withLatestFrom( - homeserverConnected.disconnectReason$, - localConnectionState$, - ), - scope.bind(), - ) - .subscribe(([_, homeserverReason]) => { - const reason = homeserverReason !== null ? homeserverReason : "livekit"; - PosthogAnalytics.instance.trackCallReconnecting(callId, reason); + .pipe(distinctUntilChanged(), scope.bind()) + .subscribe((reconnecting) => { + if (reconnecting) { + const homeserverReason = homeserverConnected.disconnectReason$.value; + reconnectStart = { + time: Date.now(), + reason: homeserverReason !== null ? homeserverReason : "livekit", + }; + } else if (reconnectStart !== null) { + const duration = (Date.now() - reconnectStart.time) / 1000; + PosthogAnalytics.instance.eventCallReconnecting.track( + callId, + reconnectStart.reason, + duration, + ); + PosthogAnalytics.instance.eventCallEnded.cacheReconnecting( + reconnectStart.reason, + ); + reconnectStart = null; + } }); // inform the widget about the connect and disconnect intent from the user.