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:
|
// 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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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