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:
parent
11fd5363ce
commit
29805edd02
|
@ -406,6 +406,15 @@ var config = {
|
|||
// The Amplitude 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".
|
||||
// scriptURLs: [
|
||||
// "libs/analytics-ga.min.js", // google-analytics
|
||||
|
|
|
@ -15003,6 +15003,13 @@
|
|||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
|
||||
|
|
|
@ -90,6 +90,7 @@
|
|||
"redux": "4.0.4",
|
||||
"redux-thunk": "2.2.0",
|
||||
"rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af",
|
||||
"rtcstats": "github:jitsi/rtcstats#v6.1.3",
|
||||
"styled-components": "3.4.9",
|
||||
"util": "0.12.1",
|
||||
"uuid": "3.1.0",
|
||||
|
|
|
@ -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
|
||||
* occurred (e.g. It was started or stopped).
|
||||
|
|
|
@ -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,
|
||||
* disabled state, etc.
|
||||
|
@ -92,6 +102,8 @@ export function createHandlers({ getState }: { getState: Function }) {
|
|||
try {
|
||||
const amplitude = new AmplitudeHandler(handlerConstructorOptions);
|
||||
|
||||
analytics.amplitudeIdentityProps = amplitude.getIdentityProps();
|
||||
|
||||
handlers.push(amplitude);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
|
|
|
@ -65,4 +65,17 @@ export default class AmplitudeHandler extends AbstractHandler {
|
|||
this._extractName(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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import '../recent-list/middleware';
|
|||
import '../recording/middleware';
|
||||
import '../rejoin/middleware';
|
||||
import '../room-lock/middleware';
|
||||
import '../rtcstats/middleware';
|
||||
import '../subtitles/middleware';
|
||||
import '../toolbox/middleware';
|
||||
import '../transcribing/middleware';
|
||||
|
|
|
@ -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();
|
|
@ -0,0 +1 @@
|
|||
import './middleware';
|
|
@ -0,0 +1,5 @@
|
|||
// @flow
|
||||
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/rtcstats');
|
|
@ -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);
|
||||
});
|
Loading…
Reference in New Issue