mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
Track reconnect duration and align with existing analytics pattern
This commit is contained in:
@@ -27,7 +27,6 @@ import {
|
|||||||
CallDisconnectedEventTracker,
|
CallDisconnectedEventTracker,
|
||||||
CallConnectDurationTracker,
|
CallConnectDurationTracker,
|
||||||
CallReconnectingTracker,
|
CallReconnectingTracker,
|
||||||
type CallReconnectingReason,
|
|
||||||
} from "./PosthogEvents";
|
} from "./PosthogEvents";
|
||||||
import { Config } from "../config/Config";
|
import { Config } from "../config/Config";
|
||||||
import { getUrlParams } from "../UrlParams";
|
import { getUrlParams } from "../UrlParams";
|
||||||
@@ -423,13 +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();
|
||||||
private readonly _eventCallReconnecting = new CallReconnectingTracker();
|
public eventCallReconnecting = new CallReconnectingTracker();
|
||||||
|
|
||||||
public trackCallReconnecting(
|
|
||||||
callId: string,
|
|
||||||
reason: CallReconnectingReason,
|
|
||||||
): void {
|
|
||||||
this._eventCallReconnecting.track(callId, reason);
|
|
||||||
this.eventCallEnded.cacheReconnecting(reason);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,12 +211,13 @@ describe("CallReconnecting", () => {
|
|||||||
|
|
||||||
it("tracks event with correct shape", () => {
|
it("tracks event with correct shape", () => {
|
||||||
const tracker = new CallReconnectingTracker();
|
const tracker = new CallReconnectingTracker();
|
||||||
tracker.track("!room:example.org", "syncing");
|
tracker.track("!room:example.org", "syncing", 3.5);
|
||||||
|
|
||||||
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith({
|
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith({
|
||||||
eventName: "CallReconnecting",
|
eventName: "CallReconnecting",
|
||||||
callId: "!room:example.org",
|
callId: "!room:example.org",
|
||||||
reason: "syncing",
|
reason: "syncing",
|
||||||
|
reconnectDuration: 3.5,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -227,10 +228,10 @@ describe("CallReconnecting", () => {
|
|||||||
"livekit",
|
"livekit",
|
||||||
] as CallReconnectingReason[])("tracks reason %s correctly", (reason) => {
|
] as CallReconnectingReason[])("tracks reason %s correctly", (reason) => {
|
||||||
const tracker = new CallReconnectingTracker();
|
const tracker = new CallReconnectingTracker();
|
||||||
tracker.track("!room:example.org", reason);
|
tracker.track("!room:example.org", reason, 1.0);
|
||||||
|
|
||||||
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith(
|
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ reason }),
|
expect.objectContaining({ reason, reconnectDuration: 1.0 }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -291,14 +291,20 @@ interface CallReconnecting extends IPosthogEvent {
|
|||||||
eventName: "CallReconnecting";
|
eventName: "CallReconnecting";
|
||||||
callId: string;
|
callId: string;
|
||||||
reason: CallReconnectingReason;
|
reason: CallReconnectingReason;
|
||||||
|
reconnectDuration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CallReconnectingTracker {
|
export class CallReconnectingTracker {
|
||||||
public track(callId: string, reason: CallReconnectingReason): void {
|
public track(
|
||||||
|
callId: string,
|
||||||
|
reason: CallReconnectingReason,
|
||||||
|
reconnectDuration: number,
|
||||||
|
): void {
|
||||||
PosthogAnalytics.instance.trackEvent<CallReconnecting>({
|
PosthogAnalytics.instance.trackEvent<CallReconnecting>({
|
||||||
eventName: "CallReconnecting",
|
eventName: "CallReconnecting",
|
||||||
callId,
|
callId,
|
||||||
reason,
|
reason,
|
||||||
|
reconnectDuration,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -693,11 +693,11 @@ describe("LocalMembership", () => {
|
|||||||
PosthogAnalytics.resetInstance();
|
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 scope = new ObservableScope();
|
||||||
const trackSpy = vi.spyOn(
|
const trackSpy = vi.spyOn(
|
||||||
PosthogAnalytics.instance,
|
PosthogAnalytics.instance.eventCallReconnecting,
|
||||||
"trackCallReconnecting",
|
"track",
|
||||||
);
|
);
|
||||||
|
|
||||||
const hsConnected$ = new BehaviorSubject<boolean>(true);
|
const hsConnected$ = new BehaviorSubject<boolean>(true);
|
||||||
@@ -726,23 +726,26 @@ describe("LocalMembership", () => {
|
|||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
// Simulate homeserver disconnect with syncing as the reason
|
// Disconnect with syncing reason, then reconnect
|
||||||
disconnectReason$.next("syncing");
|
disconnectReason$.next("syncing");
|
||||||
hsConnected$.next(false);
|
hsConnected$.next(false);
|
||||||
|
disconnectReason$.next(null);
|
||||||
|
hsConnected$.next(true);
|
||||||
|
|
||||||
expect(trackSpy).toHaveBeenCalledWith(
|
expect(trackSpy).toHaveBeenCalledWith(
|
||||||
defaultCreateLocalMemberValues.callId,
|
defaultCreateLocalMemberValues.callId,
|
||||||
"syncing",
|
"syncing",
|
||||||
|
expect.any(Number),
|
||||||
);
|
);
|
||||||
|
|
||||||
scope.end();
|
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 scope = new ObservableScope();
|
||||||
const trackSpy = vi.spyOn(
|
const trackSpy = vi.spyOn(
|
||||||
PosthogAnalytics.instance,
|
PosthogAnalytics.instance.eventCallReconnecting,
|
||||||
"trackCallReconnecting",
|
"track",
|
||||||
);
|
);
|
||||||
|
|
||||||
const connectionState$ = new BehaviorSubject<ConnectionState>(
|
const connectionState$ = new BehaviorSubject<ConnectionState>(
|
||||||
@@ -776,22 +779,24 @@ describe("LocalMembership", () => {
|
|||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
// Livekit drops while homeserver stays connected
|
// Livekit drops then recovers
|
||||||
connectionState$.next(ConnectionState.LivekitDisconnected);
|
connectionState$.next(ConnectionState.LivekitDisconnected);
|
||||||
|
connectionState$.next(ConnectionState.LivekitConnected);
|
||||||
|
|
||||||
expect(trackSpy).toHaveBeenCalledWith(
|
expect(trackSpy).toHaveBeenCalledWith(
|
||||||
defaultCreateLocalMemberValues.callId,
|
defaultCreateLocalMemberValues.callId,
|
||||||
"livekit",
|
"livekit",
|
||||||
|
expect.any(Number),
|
||||||
);
|
);
|
||||||
|
|
||||||
scope.end();
|
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 scope = new ObservableScope();
|
||||||
const trackSpy = vi.spyOn(
|
const trackSpy = vi.spyOn(
|
||||||
PosthogAnalytics.instance,
|
PosthogAnalytics.instance.eventCallReconnecting,
|
||||||
"trackCallReconnecting",
|
"track",
|
||||||
);
|
);
|
||||||
|
|
||||||
const hsConnected$ = new BehaviorSubject<boolean>(true);
|
const hsConnected$ = new BehaviorSubject<boolean>(true);
|
||||||
@@ -820,26 +825,29 @@ describe("LocalMembership", () => {
|
|||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
// First reconnect
|
// First full reconnect cycle
|
||||||
disconnectReason$.next("membershipConnected");
|
disconnectReason$.next("membershipConnected");
|
||||||
hsConnected$.next(false);
|
hsConnected$.next(false);
|
||||||
// Reconnected
|
|
||||||
disconnectReason$.next(null);
|
disconnectReason$.next(null);
|
||||||
hsConnected$.next(true);
|
hsConnected$.next(true);
|
||||||
// Second reconnect
|
// Second full reconnect cycle
|
||||||
disconnectReason$.next("certainlyConnected");
|
disconnectReason$.next("certainlyConnected");
|
||||||
hsConnected$.next(false);
|
hsConnected$.next(false);
|
||||||
|
disconnectReason$.next(null);
|
||||||
|
hsConnected$.next(true);
|
||||||
|
|
||||||
expect(trackSpy).toHaveBeenCalledTimes(2);
|
expect(trackSpy).toHaveBeenCalledTimes(2);
|
||||||
expect(trackSpy).toHaveBeenNthCalledWith(
|
expect(trackSpy).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
defaultCreateLocalMemberValues.callId,
|
defaultCreateLocalMemberValues.callId,
|
||||||
"membershipConnected",
|
"membershipConnected",
|
||||||
|
expect.any(Number),
|
||||||
);
|
);
|
||||||
expect(trackSpy).toHaveBeenNthCalledWith(
|
expect(trackSpy).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
defaultCreateLocalMemberValues.callId,
|
defaultCreateLocalMemberValues.callId,
|
||||||
"certainlyConnected",
|
"certainlyConnected",
|
||||||
|
expect.any(Number),
|
||||||
);
|
);
|
||||||
|
|
||||||
scope.end();
|
scope.end();
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
catchError,
|
catchError,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
filter,
|
|
||||||
from,
|
from,
|
||||||
fromEvent,
|
fromEvent,
|
||||||
map,
|
map,
|
||||||
@@ -35,7 +34,6 @@ 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";
|
||||||
@@ -55,6 +53,7 @@ import {
|
|||||||
import { ElementWidgetActions, widget } from "../../../widget.ts";
|
import { ElementWidgetActions, widget } from "../../../widget.ts";
|
||||||
import { getUrlParams } from "../../../UrlParams.ts";
|
import { getUrlParams } from "../../../UrlParams.ts";
|
||||||
import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics.ts";
|
import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics.ts";
|
||||||
|
import { type CallReconnectingReason } from "../../../analytics/PosthogEvents.ts";
|
||||||
import { MatrixRTCMode } from "../../../settings/settings.ts";
|
import { MatrixRTCMode } from "../../../settings/settings.ts";
|
||||||
import { Config } from "../../../config/Config.ts";
|
import { Config } from "../../../config/Config.ts";
|
||||||
import {
|
import {
|
||||||
@@ -524,19 +523,29 @@ export const createLocalMembership$ = ({
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let reconnectStart: { time: number; reason: CallReconnectingReason } | null =
|
||||||
|
null;
|
||||||
reconnecting$
|
reconnecting$
|
||||||
.pipe(
|
.pipe(distinctUntilChanged(), scope.bind())
|
||||||
distinctUntilChanged(),
|
.subscribe((reconnecting) => {
|
||||||
filter(Boolean),
|
if (reconnecting) {
|
||||||
withLatestFrom(
|
const homeserverReason = homeserverConnected.disconnectReason$.value;
|
||||||
homeserverConnected.disconnectReason$,
|
reconnectStart = {
|
||||||
localConnectionState$,
|
time: Date.now(),
|
||||||
),
|
reason: homeserverReason !== null ? homeserverReason : "livekit",
|
||||||
scope.bind(),
|
};
|
||||||
)
|
} else if (reconnectStart !== null) {
|
||||||
.subscribe(([_, homeserverReason]) => {
|
const duration = (Date.now() - reconnectStart.time) / 1000;
|
||||||
const reason = homeserverReason !== null ? homeserverReason : "livekit";
|
PosthogAnalytics.instance.eventCallReconnecting.track(
|
||||||
PosthogAnalytics.instance.trackCallReconnecting(callId, reason);
|
callId,
|
||||||
|
reconnectStart.reason,
|
||||||
|
duration,
|
||||||
|
);
|
||||||
|
PosthogAnalytics.instance.eventCallEnded.cacheReconnecting(
|
||||||
|
reconnectStart.reason,
|
||||||
|
);
|
||||||
|
reconnectStart = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// inform the widget about the connect and disconnect intent from the user.
|
// inform the widget about the connect and disconnect intent from the user.
|
||||||
|
|||||||
Reference in New Issue
Block a user