Track reconnect duration and align with existing analytics pattern

This commit is contained in:
fkwp
2026-05-08 12:06:45 +02:00
parent e3231317c5
commit 4ffe3a6d2a
5 changed files with 57 additions and 42 deletions

View File

@@ -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();
}

View File

@@ -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 }),
);
});
});

View File

@@ -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<CallReconnecting>({
eventName: "CallReconnecting",
callId,
reason,
reconnectDuration,
});
}
}

View File

@@ -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<boolean>(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<ConnectionState>(
@@ -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<boolean>(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();

View File

@@ -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.