mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-15 00:04:59 +08:00
Detect split-brains caused by parallel calls
This is another KPI for PostHog.
This commit is contained in:
parent
7221b7c3a2
commit
838137c83b
65
src/room/checkForParallelCalls.ts
Normal file
65
src/room/checkForParallelCalls.ts
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
|
||||
function isObject(x: unknown): x is Record<string, unknown> {
|
||||
return typeof x === "object" && x !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the state of a room for multiple calls happening in parallel, sending
|
||||
* the details to PostHog if that is indeed what's happening. (This is unwanted
|
||||
* as it indicates a split-brain scenario.)
|
||||
*/
|
||||
export function checkForParallelCalls(state: RoomState): void {
|
||||
const now = Date.now();
|
||||
const participantsPerCall = new Map<string, number>();
|
||||
|
||||
// For each participant in each call, increment the participant count
|
||||
for (const e of state.getStateEvents(EventType.GroupCallMemberPrefix)) {
|
||||
const content = e.getContent<Record<string, unknown>>();
|
||||
const calls: unknown[] = Array.isArray(content["m.calls"])
|
||||
? content["m.calls"]
|
||||
: [];
|
||||
|
||||
for (const call of calls) {
|
||||
if (isObject(call) && typeof call["m.call_id"] === "string") {
|
||||
const devices: unknown[] = Array.isArray(call["m.devices"])
|
||||
? call["m.devices"]
|
||||
: [];
|
||||
|
||||
for (const device of devices) {
|
||||
if (isObject(device) && (device["expires_ts"] as number) > now) {
|
||||
const participantCount =
|
||||
participantsPerCall.get(call["m.call_id"]) ?? 0;
|
||||
participantsPerCall.set(call["m.call_id"], participantCount + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (participantsPerCall.size > 1) {
|
||||
PosthogAnalytics.instance.trackEvent({
|
||||
eventName: "ParallelCalls",
|
||||
participantsPerCall: Object.fromEntries(participantsPerCall),
|
||||
});
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IWidgetApiRequest } from "matrix-widget-api";
|
||||
import { MatrixClient } from "matrix-js-sdk";
|
||||
import { MatrixClient, RoomStateEvent } from "matrix-js-sdk";
|
||||
import {
|
||||
ByteSentStatsReport,
|
||||
ConnectionStatsReport,
|
||||
@ -42,6 +42,7 @@ import { TranslatedError, translatedError } from "../TranslatedError";
|
||||
import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget";
|
||||
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
||||
import { ElementCallOpenTelemetry } from "../otel/otel";
|
||||
import { checkForParallelCalls } from "./checkForParallelCalls";
|
||||
|
||||
export enum ConnectionState {
|
||||
EstablishingCall = "establishing call", // call hasn't been established yet
|
||||
@ -377,18 +378,19 @@ export function useGroupCall(
|
||||
groupCall.on(GroupCallEvent.CallsChanged, onCallsChanged);
|
||||
groupCall.on(GroupCallEvent.ParticipantsChanged, onParticipantsChanged);
|
||||
groupCall.on(GroupCallEvent.Error, onError);
|
||||
|
||||
groupCall.on(
|
||||
GroupCallStatsReportEvent.ConnectionStats,
|
||||
onConnectionStatsReport
|
||||
);
|
||||
|
||||
groupCall.on(
|
||||
GroupCallStatsReportEvent.ByteSentStats,
|
||||
onByteSentStatsReport
|
||||
);
|
||||
|
||||
groupCall.on(GroupCallStatsReportEvent.SummaryStats, onSummaryStatsReport);
|
||||
groupCall.room.currentState.on(
|
||||
RoomStateEvent.Update,
|
||||
checkForParallelCalls
|
||||
);
|
||||
|
||||
updateState({
|
||||
error: null,
|
||||
@ -448,6 +450,10 @@ export function useGroupCall(
|
||||
GroupCallStatsReportEvent.SummaryStats,
|
||||
onSummaryStatsReport
|
||||
);
|
||||
groupCall.room.currentState.off(
|
||||
RoomStateEvent.Update,
|
||||
checkForParallelCalls
|
||||
);
|
||||
leaveCall();
|
||||
};
|
||||
}, [groupCall, updateState, leaveCall]);
|
||||
|
171
test/room/checkForParallelCalls-test.ts
Normal file
171
test/room/checkForParallelCalls-test.ts
Normal file
@ -0,0 +1,171 @@
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Mocked, mocked } from "jest-mock";
|
||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
import { PosthogAnalytics } from "../../src/analytics/PosthogAnalytics";
|
||||
import { checkForParallelCalls } from "../../src/room/checkForParallelCalls";
|
||||
|
||||
const withFakeTimers = (continuation: () => void) => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
continuation();
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
};
|
||||
|
||||
const withMockedPosthog = (
|
||||
continuation: (posthog: Mocked<PosthogAnalytics>) => void
|
||||
) => {
|
||||
const posthog = mocked({
|
||||
trackEvent: jest.fn(),
|
||||
} as unknown as PosthogAnalytics);
|
||||
const instanceSpy = jest
|
||||
.spyOn(PosthogAnalytics, "instance", "get")
|
||||
.mockReturnValue(posthog);
|
||||
try {
|
||||
continuation(posthog);
|
||||
} finally {
|
||||
instanceSpy.mockRestore();
|
||||
}
|
||||
};
|
||||
|
||||
const mockRoomState = (
|
||||
groupCallMemberContents: Record<string, unknown>[]
|
||||
): RoomState => {
|
||||
const stateEvents = groupCallMemberContents.map((content) => ({
|
||||
getContent: () => content,
|
||||
}));
|
||||
return { getStateEvents: () => stateEvents } as unknown as RoomState;
|
||||
};
|
||||
|
||||
test("checkForParallelCalls does nothing if all participants are in the same call", () => {
|
||||
withFakeTimers(() => {
|
||||
withMockedPosthog((posthog) => {
|
||||
const roomState = mockRoomState([
|
||||
{
|
||||
"m.calls": [
|
||||
{
|
||||
"m.call_id": "1",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Call",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.call_id": null, // invalid
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Android",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
null, // invalid
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.calls": [
|
||||
{
|
||||
"m.call_id": "1",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Desktop",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
checkForParallelCalls(roomState);
|
||||
expect(posthog.trackEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("checkForParallelCalls sends diagnostics to PostHog if there is a split-brain", () => {
|
||||
withFakeTimers(() => {
|
||||
withMockedPosthog((posthog) => {
|
||||
const roomState = mockRoomState([
|
||||
{
|
||||
"m.calls": [
|
||||
{
|
||||
"m.call_id": "1",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Call",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.call_id": "2",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Android",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.calls": [
|
||||
{
|
||||
"m.call_id": "1",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Desktop",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.call_id": "2",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Call",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() - 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
checkForParallelCalls(roomState);
|
||||
expect(posthog.trackEvent).toHaveBeenCalledWith({
|
||||
eventName: "ParallelCalls",
|
||||
participantsPerCall: {
|
||||
"1": 2,
|
||||
"2": 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user