Add Posthog events for Call reconnect including the reason

This commit is contained in:
fkwp
2026-05-07 10:36:36 +02:00
parent 147d0f96e0
commit 4a0759e9b8
7 changed files with 102 additions and 2 deletions

View File

@@ -26,6 +26,7 @@ import {
QualitySurveyEventTracker, QualitySurveyEventTracker,
CallDisconnectedEventTracker, CallDisconnectedEventTracker,
CallConnectDurationTracker, CallConnectDurationTracker,
CallReconnectingTracker,
} from "./PosthogEvents"; } from "./PosthogEvents";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
import { getUrlParams } from "../UrlParams"; import { getUrlParams } from "../UrlParams";
@@ -421,4 +422,5 @@ export class PosthogAnalytics {
public eventQualitySurvey = new QualitySurveyEventTracker(); public eventQualitySurvey = new QualitySurveyEventTracker();
public eventCallDisconnected = new CallDisconnectedEventTracker(); public eventCallDisconnected = new CallDisconnectedEventTracker();
public eventCallConnectDuration = new CallConnectDurationTracker(); public eventCallConnectDuration = new CallConnectDurationTracker();
public eventCallReconnecting = new CallReconnectingTracker();
} }

View File

@@ -89,6 +89,11 @@ describe("CallEnded", () => {
roomEventEncryptionKeysSent: 10, roomEventEncryptionKeysSent: 10,
roomEventEncryptionKeysReceived: 5, roomEventEncryptionKeysReceived: 5,
roomEventEncryptionKeysReceivedAverageAge: 100, roomEventEncryptionKeysReceivedAverageAge: 100,
callReconnectingCount: 0,
callReconnectingCountSyncing: 0,
callReconnectingCountMembershipConnected: 0,
callReconnectingCountCertainlyConnected: 0,
callReconnectingCountLivekit: 0,
}, },
{ send_instantly: true }, { send_instantly: true },
); );

View File

@@ -24,12 +24,29 @@ interface CallEnded extends IPosthogEvent {
roomEventEncryptionKeysSent: number; roomEventEncryptionKeysSent: number;
roomEventEncryptionKeysReceived: number; roomEventEncryptionKeysReceived: number;
roomEventEncryptionKeysReceivedAverageAge: number; roomEventEncryptionKeysReceivedAverageAge: number;
callReconnectingCount: number;
callReconnectingCountSyncing: number;
callReconnectingCountMembershipConnected: number;
callReconnectingCountCertainlyConnected: number;
callReconnectingCountLivekit: number;
} }
export class CallEndedTracker { export class CallEndedTracker {
private cache: { startTime?: Date; maxParticipantsCount: number } = { private cache: {
startTime?: Date;
maxParticipantsCount: number;
reconnectingCount: number;
reconnectingCountByReason: Record<CallReconnectingReason, number>;
} = {
startTime: undefined, startTime: undefined,
maxParticipantsCount: 0, maxParticipantsCount: 0,
reconnectingCount: 0,
reconnectingCountByReason: {
syncing: 0,
membershipConnected: 0,
certainlyConnected: 0,
livekit: 0,
},
}; };
public cacheStartCall(time: Date): void { 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( public track(
callId: string, callId: string,
callParticipantsNow: number, callParticipantsNow: number,
@@ -67,6 +89,15 @@ export class CallEndedTracker {
.roomEventEncryptionKeysReceivedTotalAge / .roomEventEncryptionKeysReceivedTotalAge /
rtcSession.statistics.counters.roomEventEncryptionKeysReceived rtcSession.statistics.counters.roomEventEncryptionKeysReceived
: 0, : 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 }, { 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<CallReconnecting>({
eventName: "CallReconnecting",
callId,
reason,
});
}
}

View File

@@ -560,6 +560,7 @@ export function createCallViewModel$(
connectionManager, connectionManager,
matrixRTCSession, matrixRTCSession,
localTransport$, localTransport$,
callId: matrixRoom.roomId,
logger: logger.getChild(`[${Date.now()}]`), logger: logger.getChild(`[${Date.now()}]`),
}); });

View File

@@ -22,6 +22,7 @@ import {
switchMap, switchMap,
of, of,
delay, delay,
combineLatest,
} from "rxjs"; } from "rxjs";
import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; 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]"); const logger = rootLogger.getChild("[HomeserverConnected]");
export type HomeserverDisconnectReason =
| "syncing"
| "membershipConnected"
| "certainlyConnected";
export interface HomeserverConnected { export interface HomeserverConnected {
combined$: Behavior<boolean>; combined$: Behavior<boolean>;
rtsSession$: Behavior<Status>; rtsSession$: Behavior<Status>;
disconnectReason$: Behavior<HomeserverDisconnectReason | null>;
} }
/** /**
@@ -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$ };
} }

View File

@@ -217,7 +217,9 @@ describe("LocalMembership", () => {
homeserverConnected: { homeserverConnected: {
combined$: constant(true), combined$: constant(true),
rtsSession$: constant(RTCMemberStatus.Connected), rtsSession$: constant(RTCMemberStatus.Connected),
disconnectReason$: constant(null),
}, },
callId: "!test-room-id:example.org",
}; };
it("throws error on missing RTC config error", () => { it("throws error on missing RTC config error", () => {

View File

@@ -25,6 +25,7 @@ import {
catchError, catchError,
combineLatest, combineLatest,
distinctUntilChanged, distinctUntilChanged,
filter,
from, from,
fromEvent, fromEvent,
map, map,
@@ -34,6 +35,7 @@ import {
startWith, startWith,
switchMap, switchMap,
tap, tap,
withLatestFrom,
} from "rxjs"; } from "rxjs";
import { type Logger } from "matrix-js-sdk/lib/logger"; import { type Logger } from "matrix-js-sdk/lib/logger";
import { deepCompare } from "matrix-js-sdk/lib/utils"; import { deepCompare } from "matrix-js-sdk/lib/utils";
@@ -129,6 +131,7 @@ interface Props {
createPublisherFactory: (connection: Connection) => Publisher; createPublisherFactory: (connection: Connection) => Publisher;
joinMatrixRTC: (transport: LivekitTransportConfig) => void; joinMatrixRTC: (transport: LivekitTransportConfig) => void;
homeserverConnected: HomeserverConnected; homeserverConnected: HomeserverConnected;
callId: string;
localTransport$: Behavior<LocalTransport>; localTransport$: Behavior<LocalTransport>;
matrixRTCSession: Pick< matrixRTCSession: Pick<
MatrixRTCSession, MatrixRTCSession,
@@ -152,6 +155,7 @@ interface Props {
* @param props.logger The logger to use. * @param props.logger The logger to use.
* @param props.muteStates The mute states for video and audio. * @param props.muteStates The mute states for video and audio.
* @param props.matrixRTCSession The matrix RTC session to join. * @param props.matrixRTCSession The matrix RTC session to join.
* @param props.callId The room ID used as the call identifier in analytics events.
* @returns * @returns
* - publisher: The handle to create tracks and publish them to the room. * - 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) * - 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, logger: parentLogger,
muteStates, muteStates,
matrixRTCSession, matrixRTCSession,
callId,
}: Props): { }: Props): {
/** /**
* This request to start audio and video tracks. * This request to start audio and video tracks.
@@ -519,6 +524,19 @@ export const createLocalMembership$ = ({
false, 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. // inform the widget about the connect and disconnect intent from the user.
scope scope
.behavior(joinAndPublishRequested$.pipe(pairwise(), scope.bind()), [ .behavior(joinAndPublishRequested$.pipe(pairwise(), scope.bind()), [