[MM-57698] Implement client ICE candidate pairs metric (#8051)

* Implement client ICE candidate pairs metric

* Update calls package
This commit is contained in:
Claudio Costa 2024-07-26 10:14:33 +02:00 committed by GitHub
parent 7682c5ccce
commit 3656bff4bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 195 additions and 5 deletions

View file

@ -0,0 +1,130 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {RTCMonitor, RTCPeer, parseRTCStats} from '@mattermost/calls/lib';
import {Platform} from 'react-native';
import InCallManager from 'react-native-incall-manager';
import NetworkManager from '@managers/network_manager';
import {newConnection} from './connection';
import {WebSocketClient} from './websocket_client';
jest.mock('./websocket_client');
jest.mock('@mattermost/calls/lib');
describe('newConnection', () => {
const mockClient = {
getWebSocketUrl: jest.fn(() => 'ws://localhost:8065'),
getCallsConfig: jest.fn(() => ({})),
};
const mockRTCStats = {
iceStats: {
succeeded: [
{
id: 'candidatePairA',
timestamp: 45,
priority: 45,
state: 'succeeded',
local: {
candidateType: 'host',
protocol: 'udp',
port: 45000,
},
remote: {
candidateType: 'host',
protocol: 'udp',
port: 8443,
},
},
],
failed: [],
},
};
beforeAll(() => {
// eslint-disable-next-line
// @ts-ignore
global.navigator = {};
// eslint-disable-next-line
// @ts-ignore
NetworkManager.getClient = jest.fn(() => mockClient);
// eslint-disable-next-line
// @ts-ignore
RTCPeer.mockImplementation(() => {
return {
getStats: jest.fn(),
on: jest.fn(),
};
});
// eslint-disable-next-line
// @ts-ignore
RTCMonitor.mockImplementation(() => {
return {
on: jest.fn(),
};
});
Platform.OS = 'web';
InCallManager.start = jest.fn();
InCallManager.stopProximitySensor = jest.fn();
});
afterAll(() => {
// eslint-disable-next-line
// @ts-ignore
delete global.navigator;
jest.resetAllMocks();
});
it('collectICEStats', (done) => {
const wsSend = jest.fn();
const joinHandler = jest.fn(async (event: string, cb: () => void) => {
if (event === 'join') {
await cb();
expect(parseRTCStats).toHaveBeenCalled();
expect(wsSend).toHaveBeenCalledWith('metric', {
metric_name: 'client_ice_candidate_pair',
data: JSON.stringify({
state: 'succeeded',
local: {
type: 'host',
protocol: 'udp',
},
remote: {
type: 'host',
protocol: 'udp',
},
}),
},
);
done();
}
});
// eslint-disable-next-line
// @ts-ignore
WebSocketClient.mockImplementation(() => {
return {
initialize: jest.fn(),
on: joinHandler,
send: wsSend,
};
});
// eslint-disable-next-line
// @ts-ignore
parseRTCStats.mockImplementation(() => mockRTCStats);
newConnection('http://localhost:8065', 'channelID', () => {}, () => {}, false).
then((connection) => {
expect(connection).toBeDefined();
expect(joinHandler).toHaveBeenCalled();
});
});
});

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {RTCMonitor, RTCPeer} from '@mattermost/calls/lib';
import {RTCMonitor, RTCPeer, parseRTCStats} from '@mattermost/calls/lib';
import {deflate} from 'pako';
import {DeviceEventEmitter, type EmitterSubscription, NativeEventEmitter, NativeModules, Platform} from 'react-native';
import InCallManager from 'react-native-incall-manager';
@ -226,6 +226,64 @@ export async function newConnection(
}
};
const collectICEStats = () => {
const start = Date.now();
const seenMap: {[key: string]: string} = {};
const gatherStats = async () => {
if (!peer) {
return;
}
try {
const stats = parseRTCStats(await peer.getStats()).iceStats;
for (const state of Object.keys(stats)) {
for (const pair of stats[state]) {
const seenState = seenMap[pair.id];
seenMap[pair.id] = pair.state;
if (seenState !== pair.state) {
logDebug('calls: ice candidate pair stats', JSON.stringify(pair));
}
if (seenState === 'succeeded' || state !== 'succeeded') {
continue;
}
if (!pair.local || !pair.remote) {
continue;
}
ws.send('metric', {
metric_name: 'client_ice_candidate_pair',
data: JSON.stringify({
state: pair.state,
local: {
type: pair.local.candidateType,
protocol: pair.local.protocol,
},
remote: {
type: pair.remote.candidateType,
protocol: pair.remote.protocol,
},
}),
});
}
}
} catch (err) {
logError('failed to parse ICE stats', err);
}
// Repeat the check for at most 30 seconds.
if (Date.now() < start + 30000) {
// We check every two seconds.
setTimeout(gatherStats, 2000);
}
};
gatherStats();
};
ws.on('error', (err: Error) => {
logDebug('calls: ws error', err);
if (err === wsReconnectionTimeoutErr) {
@ -329,6 +387,8 @@ export async function newConnection(
logger,
});
collectICEStats();
rtcMonitor = new RTCMonitor({
peer,
logger,

6
package-lock.json generated
View file

@ -17,7 +17,7 @@
"@formatjs/intl-numberformat": "8.10.3",
"@formatjs/intl-pluralrules": "5.2.14",
"@gorhom/bottom-sheet": "4.6.3",
"@mattermost/calls": "github:mattermost/calls-common#09f1d4e715ccaf4f161e5e5340e251997336ee3d",
"@mattermost/calls": "github:mattermost/calls-common#5cc5598fe9574125a61a3b54501deb6b65fcc70f",
"@mattermost/compass-icons": "0.1.45",
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
"@mattermost/keyboard-tracker": "file:./libraries/@mattermost/keyboard-tracker",
@ -5564,8 +5564,8 @@
"node_modules/@mattermost/calls": {
"name": "@mattermost/calls-common",
"version": "0.27.2",
"resolved": "git+ssh://git@github.com/mattermost/calls-common.git#09f1d4e715ccaf4f161e5e5340e251997336ee3d",
"integrity": "sha512-8nBf2upZbCa0GTkusCmmHlT8Tz4BmegAFoDNK82Y3o+wfZd3ZESIG6GEG6aKlrXt/fwU7iCaSQw59WxDXhun9w=="
"resolved": "git+ssh://git@github.com/mattermost/calls-common.git#5cc5598fe9574125a61a3b54501deb6b65fcc70f",
"integrity": "sha512-oJkw51naheYGDlxV4iRx0rnI1zAjWE/InRG6QXPvq2nbJ9U0QUPsk/H9/O6trP2ZuXR/v1u72z/OwqLud1blbQ=="
},
"node_modules/@mattermost/commonmark": {
"version": "0.30.1-2",

View file

@ -18,7 +18,7 @@
"@formatjs/intl-numberformat": "8.10.3",
"@formatjs/intl-pluralrules": "5.2.14",
"@gorhom/bottom-sheet": "4.6.3",
"@mattermost/calls": "github:mattermost/calls-common#09f1d4e715ccaf4f161e5e5340e251997336ee3d",
"@mattermost/calls": "github:mattermost/calls-common#5cc5598fe9574125a61a3b54501deb6b65fcc70f",
"@mattermost/compass-icons": "0.1.45",
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
"@mattermost/keyboard-tracker": "file:./libraries/@mattermost/keyboard-tracker",