[MM-57698] Implement client ICE candidate pairs metric (#8051)
* Implement client ICE candidate pairs metric * Update calls package
This commit is contained in:
parent
7682c5ccce
commit
3656bff4bd
4 changed files with 195 additions and 5 deletions
130
app/products/calls/connection/connection.test.ts
Normal file
130
app/products/calls/connection/connection.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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
6
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue