Skip to content

Commit

Permalink
feat(rtcstats): Integrate rtcstats (#6945)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
andrei-gavrilescu authored Jul 15, 2020
1 parent 11fd536 commit 29805ed
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 0 deletions.
9 changes: 9 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions react/features/analytics/AnalyticsEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
12 changes: 12 additions & 0 deletions react/features/analytics/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {}
Expand Down
13 changes: 13 additions & 0 deletions react/features/analytics/handlers/AmplitudeHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}
}
1 change: 1 addition & 0 deletions react/features/app/middlewares.any.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
111 changes: 111 additions & 0 deletions react/features/rtcstats/RTCStats.js
Original file line number Diff line number Diff line change
@@ -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();
1 change: 1 addition & 0 deletions react/features/rtcstats/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './middleware';
5 changes: 5 additions & 0 deletions react/features/rtcstats/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @flow

import { getLogger } from '../base/logging/functions';

export default getLogger('features/rtcstats');
79 changes: 79 additions & 0 deletions react/features/rtcstats/middleware.js
Original file line number Diff line number Diff line change
@@ -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);
});

0 comments on commit 29805ed

Please sign in to comment.