feat(rtcstats): Integrate rtcstats (#6945)

* Integrate rtcstats

* expcetion handling / clean up

* order imports

* config fix

* remove mock amplitude handler

* additional comments

* lint fix

* address code review

* move rtcstats middleware

* link to jitsi rtcstats package

* address code review

* address code review / add ws onclose handler

* add display name / bump rtcstats version

* resolve import error
This commit is contained in:
Andrei Gavrilescu 2020-07-15 18:22:00 +03:00 committed by GitHub
parent 11fd5363ce
commit 29805edd02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 259 additions and 0 deletions

View File

@ -406,6 +406,15 @@ var config = {
// The Amplitude APP Key: // The Amplitude APP Key:
// amplitudeAPPKey: '<APP_KEY>' // amplitudeAPPKey: '<APP_KEY>'
// Configuration for the rtcstats server:
// In order to enable rtcstats one needs to provide a endpoint url.
// rtcstatsEndpoint: wss://rtcstats-server-pilot.jitsi.net/,
// The interval at which rtcstats will poll getStats, defaults to 1000ms.
// If the value is set to 0 getStats won't be polled and the rtcstats client
// will only send data related to RTCPeerConnection events.
// rtcstatsPolIInterval: 1000
// Array of script URLs to load as lib-jitsi-meet "analytics handlers". // Array of script URLs to load as lib-jitsi-meet "analytics handlers".
// scriptURLs: [ // scriptURLs: [
// "libs/analytics-ga.min.js", // google-analytics // "libs/analytics-ga.min.js", // google-analytics

7
package-lock.json generated
View File

@ -15003,6 +15003,13 @@
"sdp": "^2.6.0" "sdp": "^2.6.0"
} }
}, },
"rtcstats": {
"version": "github:jitsi/rtcstats#02a1a089d9a97d1414d216ff7d9c432253e50190",
"from": "github:jitsi/rtcstats#v6.1.3",
"requires": {
"@jitsi/js-utils": "1.0.0"
}
},
"run-async": { "run-async": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",

View File

@ -90,6 +90,7 @@
"redux": "4.0.4", "redux": "4.0.4",
"redux-thunk": "2.2.0", "redux-thunk": "2.2.0",
"rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af", "rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af",
"rtcstats": "github:jitsi/rtcstats#v6.1.3",
"styled-components": "3.4.9", "styled-components": "3.4.9",
"util": "0.12.1", "util": "0.12.1",
"uuid": "3.1.0", "uuid": "3.1.0",

View File

@ -538,6 +538,26 @@ export function createRemoteVideoMenuButtonEvent(buttonName, attributes) {
}; };
} }
/**
* The rtcstats websocket onclose event. We send this to amplitude in order
* to detect trace ws prematurely closing.
*
* @param {Object} closeEvent - The event with which the websocket closed.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createRTCStatsTraceCloseEvent(closeEvent) {
const event = {
action: 'trace.onclose',
source: 'rtcstats'
};
event.code = closeEvent.code;
event.reason = closeEvent.reason;
return event;
}
/** /**
* Creates an event indicating that an action related to video blur * Creates an event indicating that an action related to video blur
* occurred (e.g. It was started or stopped). * occurred (e.g. It was started or stopped).

View File

@ -30,6 +30,16 @@ export function sendAnalytics(event: Object) {
} }
} }
/**
* Return saved amplitude identity info such as session id, device id and user id. We assume these do not change for
* the duration of the conference.
*
* @returns {Object}
*/
export function getAmplitudeIdentity() {
return analytics.amplitudeIdentityProps;
}
/** /**
* Resets the analytics adapter to its initial state - removes handlers, cache, * Resets the analytics adapter to its initial state - removes handlers, cache,
* disabled state, etc. * disabled state, etc.
@ -92,6 +102,8 @@ export function createHandlers({ getState }: { getState: Function }) {
try { try {
const amplitude = new AmplitudeHandler(handlerConstructorOptions); const amplitude = new AmplitudeHandler(handlerConstructorOptions);
analytics.amplitudeIdentityProps = amplitude.getIdentityProps();
handlers.push(amplitude); handlers.push(amplitude);
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch (e) {} } catch (e) {}

View File

@ -65,4 +65,17 @@ export default class AmplitudeHandler extends AbstractHandler {
this._extractName(event), this._extractName(event),
event); event);
} }
/**
* Return amplitude identity information.
*
* @returns {Object}
*/
getIdentityProps() {
return {
sessionId: amplitude.getInstance(this._amplitudeOptions).getSessionId(),
deviceId: amplitude.getInstance(this._amplitudeOptions).options.deviceId,
userId: amplitude.getInstance(this._amplitudeOptions).options.userId
};
}
} }

View File

@ -37,6 +37,7 @@ import '../recent-list/middleware';
import '../recording/middleware'; import '../recording/middleware';
import '../rejoin/middleware'; import '../rejoin/middleware';
import '../room-lock/middleware'; import '../room-lock/middleware';
import '../rtcstats/middleware';
import '../subtitles/middleware'; import '../subtitles/middleware';
import '../toolbox/middleware'; import '../toolbox/middleware';
import '../transcribing/middleware'; import '../transcribing/middleware';

View File

@ -0,0 +1,111 @@
import rtcstatsInit from 'rtcstats/rtcstats';
import traceInit from 'rtcstats/trace-ws';
import {
createRTCStatsTraceCloseEvent,
sendAnalytics
} from '../analytics';
import logger from './logger';
/**
* Filter out RTCPeerConnection that are created by callstats.io.
*
* @param {*} config - Config object sent to the PC c'tor.
* @returns {boolean}
*/
function connectionFilter(config) {
if (config && config.iceServers[0] && config.iceServers[0].urls) {
for (const iceUrl of config.iceServers[0].urls) {
if (iceUrl.indexOf('taas.callstats.io') >= 0) {
return true;
}
}
}
}
/**
* Class that controls the rtcstats flow, because it overwrites and proxies global function it should only be
* initialized once.
*/
class RTCStats {
/**
* Initialize the rtcstats components. First off we initialize the trace, which is a wrapped websocket
* that does the actual communication with the server. Secondly, the rtcstats component is initialized,
* it overwrites GUM and PeerConnection global functions and adds a proxy over them used to capture stats.
* Note, lib-jitsi-meet takes references to these methods before initializing so the init method needs to be
* loaded before it does.
*
* @param {Object} options -.
* @param {string} options.rtcstatsEndpoint - The Amplitude app key required.
* @param {number} options.rtcstatsPollInterval - The getstats poll interval in ms.
* @returns {void}
*/
init(options) {
this.handleTraceWSClose = this.handleTraceWSClose.bind(this);
this.trace = traceInit(options.rtcstatsEndpoint, this.handleTraceWSClose);
rtcstatsInit(this.trace, options.rtcstatsPollInterval, [ '' ], connectionFilter);
this.initialized = true;
}
/**
* Check whether or not the RTCStats is initialized.
*
* @returns {boolean}
*/
isInitialized() {
return this.initialized;
}
/**
* Send identity data to rtcstats server, this will be reflected in the identity section of the stats dump.
* It can be generally used to send additional metadata that might be relevant such as amplitude user data
* or deployment specific information.
*
* @param {Object} identityData - Metadata object to send as identity.
* @returns {void}
*/
sendIdentityData(identityData) {
this.trace && this.trace('identity', null, identityData);
}
/**
* Connect to the rtcstats server instance. Stats (data obtained from getstats) won't be send until the
* connect successfully initializes, however calls to GUM are recorded in an internal buffer even if not
* connected and sent once it is established.
*
* @returns {void}
*/
connect() {
this.trace && this.trace.connect();
}
/**
* Self explanatory; closes the web socked connection.
* Note, at the point of writing this documentation there was no method to reset the function overwrites,
* thus even if the websocket is closed the global function proxies are still active but send no data,
* this shouldn't influence the normal flow of the application.
*
* @returns {void}
*/
close() {
this.trace && this.trace.close();
}
/**
* The way rtcstats is currently designed the ws wouldn't normally be closed by the application logic but rather
* by the page being closed/reloaded. Using this assumption any onclose event is most likely something abnormal
* that happened on the ws. We then track this in order to determine how many rtcstats connection were closed
* prematurely.
*
* @param {Object} closeEvent - Event sent by ws onclose.
* @returns {void}
*/
handleTraceWSClose(closeEvent) {
logger.info('RTCStats trace ws closed', closeEvent);
sendAnalytics(createRTCStatsTraceCloseEvent(closeEvent));
}
}
export default new RTCStats();

View File

@ -0,0 +1 @@
import './middleware';

View File

@ -0,0 +1,5 @@
// @flow
import { getLogger } from '../base/logging/functions';
export default getLogger('features/rtcstats');

View File

@ -0,0 +1,79 @@
// @flow
import { getAmplitudeIdentity } from '../analytics';
import {
CONFERENCE_JOINED
} from '../base/conference';
import { LIB_WILL_INIT } from '../base/lib-jitsi-meet';
import { getLocalParticipant } from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import RTCStats from './RTCStats';
import logger from './logger';
/**
* Middleware which intercepts lib-jitsi-meet initialization and conference join in order init the
* rtcstats-client.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
const config = state['features/base/config'];
const { analytics } = config;
switch (action.type) {
case LIB_WILL_INIT: {
if (analytics.rtcstatsEndpoint) {
// RTCStats "proxies" WebRTC functions such as GUM and RTCPeerConnection by rewriting the global
// window functions. Because lib-jitsi-meet uses references to those functions that are taken on
// init, we need to add these proxies before it initializes, otherwise lib-jitsi-meet will use the
// original non proxy versions of these functions.
try {
// Default poll interval is 1000ms if not provided in the config.
const pollInterval = analytics.rtcstatsPollInterval || 1000;
// Initialize but don't connect to the rtcstats server wss, as it will start sending data for all
// media calls made even before the conference started.
RTCStats.init({
rtcstatsEndpoint: analytics.rtcstatsEndpoint,
rtcstatsPollInterval: pollInterval
});
} catch (error) {
logger.error('Failed to initialize RTCStats: ', error);
}
}
break;
}
case CONFERENCE_JOINED: {
if (analytics.rtcstatsEndpoint && RTCStats.isInitialized()) {
// Once the conference started connect to the rtcstats server and send data.
try {
RTCStats.connect();
const localParticipant = getLocalParticipant(state);
// The current implementation of rtcstats-server is configured to send data to amplitude, thus
// we add identity specific information so we can corelate on the amplitude side. If amplitude is
// not configured an empty object will be sent.
// The current configuration of the conference is also sent as metadata to rtcstats server.
// This is done in order to facilitate queries based on different conference configurations.
// e.g. Find all RTCPeerConnections that connect to a specific shard or were created in a
// conference with a specific version.
RTCStats.sendIdentityData({
...getAmplitudeIdentity(),
...config,
displayName: localParticipant?.name
});
} catch (error) {
// If the connection failed do not impact jitsi-meet just silently fail.
logger.error('RTCStats connect failed with: ', error);
}
}
break;
}
}
return next(action);
});