Browse Source

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
master^2
Andrei Gavrilescu 4 years ago
parent
commit
29805edd02
No account linked to committer's email address

+ 9
- 0
config.js View File

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

+ 7
- 0
package-lock.json View File

@@ -15003,6 +15003,13 @@
15003 15003
         "sdp": "^2.6.0"
15004 15004
       }
15005 15005
     },
15006
+    "rtcstats": {
15007
+      "version": "github:jitsi/rtcstats#02a1a089d9a97d1414d216ff7d9c432253e50190",
15008
+      "from": "github:jitsi/rtcstats#v6.1.3",
15009
+      "requires": {
15010
+        "@jitsi/js-utils": "1.0.0"
15011
+      }
15012
+    },
15006 15013
     "run-async": {
15007 15014
       "version": "2.3.0",
15008 15015
       "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",

+ 1
- 0
package.json View File

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

+ 20
- 0
react/features/analytics/AnalyticsEvents.js View File

@@ -538,6 +538,26 @@ export function createRemoteVideoMenuButtonEvent(buttonName, attributes) {
538 538
     };
539 539
 }
540 540
 
541
+/**
542
+ * The rtcstats websocket onclose event. We send this to amplitude in order
543
+ * to detect trace ws prematurely closing.
544
+ *
545
+ * @param {Object} closeEvent - The event with which the websocket closed.
546
+ * @returns {Object} The event in a format suitable for sending via
547
+ * sendAnalytics.
548
+ */
549
+export function createRTCStatsTraceCloseEvent(closeEvent) {
550
+    const event = {
551
+        action: 'trace.onclose',
552
+        source: 'rtcstats'
553
+    };
554
+
555
+    event.code = closeEvent.code;
556
+    event.reason = closeEvent.reason;
557
+
558
+    return event;
559
+}
560
+
541 561
 /**
542 562
  * Creates an event indicating that an action related to video blur
543 563
  * occurred (e.g. It was started or stopped).

+ 12
- 0
react/features/analytics/functions.js View File

@@ -30,6 +30,16 @@ export function sendAnalytics(event: Object) {
30 30
     }
31 31
 }
32 32
 
33
+/**
34
+ * Return saved amplitude identity info such as session id, device id and user id. We assume these do not change for
35
+ * the duration of the conference.
36
+ *
37
+ * @returns {Object}
38
+ */
39
+export function getAmplitudeIdentity() {
40
+    return analytics.amplitudeIdentityProps;
41
+}
42
+
33 43
 /**
34 44
  * Resets the analytics adapter to its initial state - removes handlers, cache,
35 45
  * disabled state, etc.
@@ -92,6 +102,8 @@ export function createHandlers({ getState }: { getState: Function }) {
92 102
     try {
93 103
         const amplitude = new AmplitudeHandler(handlerConstructorOptions);
94 104
 
105
+        analytics.amplitudeIdentityProps = amplitude.getIdentityProps();
106
+
95 107
         handlers.push(amplitude);
96 108
     // eslint-disable-next-line no-empty
97 109
     } catch (e) {}

+ 13
- 0
react/features/analytics/handlers/AmplitudeHandler.js View File

@@ -65,4 +65,17 @@ export default class AmplitudeHandler extends AbstractHandler {
65 65
             this._extractName(event),
66 66
             event);
67 67
     }
68
+
69
+    /**
70
+     * Return amplitude identity information.
71
+     *
72
+     * @returns {Object}
73
+     */
74
+    getIdentityProps() {
75
+        return {
76
+            sessionId: amplitude.getInstance(this._amplitudeOptions).getSessionId(),
77
+            deviceId: amplitude.getInstance(this._amplitudeOptions).options.deviceId,
78
+            userId: amplitude.getInstance(this._amplitudeOptions).options.userId
79
+        };
80
+    }
68 81
 }

+ 1
- 0
react/features/app/middlewares.any.js View File

@@ -37,6 +37,7 @@ import '../recent-list/middleware';
37 37
 import '../recording/middleware';
38 38
 import '../rejoin/middleware';
39 39
 import '../room-lock/middleware';
40
+import '../rtcstats/middleware';
40 41
 import '../subtitles/middleware';
41 42
 import '../toolbox/middleware';
42 43
 import '../transcribing/middleware';

+ 111
- 0
react/features/rtcstats/RTCStats.js View File

@@ -0,0 +1,111 @@
1
+import rtcstatsInit from 'rtcstats/rtcstats';
2
+import traceInit from 'rtcstats/trace-ws';
3
+
4
+import {
5
+    createRTCStatsTraceCloseEvent,
6
+    sendAnalytics
7
+} from '../analytics';
8
+
9
+import logger from './logger';
10
+
11
+/**
12
+ * Filter out RTCPeerConnection that are created by callstats.io.
13
+ *
14
+ * @param {*} config - Config object sent to the PC c'tor.
15
+ * @returns {boolean}
16
+ */
17
+function connectionFilter(config) {
18
+    if (config && config.iceServers[0] && config.iceServers[0].urls) {
19
+        for (const iceUrl of config.iceServers[0].urls) {
20
+            if (iceUrl.indexOf('taas.callstats.io') >= 0) {
21
+                return true;
22
+            }
23
+        }
24
+    }
25
+}
26
+
27
+/**
28
+ * Class that controls the rtcstats flow, because it overwrites and proxies global function it should only be
29
+ * initialized once.
30
+ */
31
+class RTCStats {
32
+    /**
33
+     * Initialize the rtcstats components. First off we initialize the trace, which is a wrapped websocket
34
+     * that does the actual communication with the server. Secondly, the rtcstats component is initialized,
35
+     * it overwrites GUM and PeerConnection global functions and adds a proxy over them used to capture stats.
36
+     * Note, lib-jitsi-meet takes references to these methods before initializing so the init method needs to be
37
+     * loaded before it does.
38
+     *
39
+     * @param {Object} options -.
40
+     * @param {string} options.rtcstatsEndpoint - The Amplitude app key required.
41
+     * @param {number} options.rtcstatsPollInterval - The getstats poll interval in ms.
42
+     * @returns {void}
43
+     */
44
+    init(options) {
45
+        this.handleTraceWSClose = this.handleTraceWSClose.bind(this);
46
+        this.trace = traceInit(options.rtcstatsEndpoint, this.handleTraceWSClose);
47
+        rtcstatsInit(this.trace, options.rtcstatsPollInterval, [ '' ], connectionFilter);
48
+        this.initialized = true;
49
+    }
50
+
51
+    /**
52
+     * Check whether or not the RTCStats is initialized.
53
+     *
54
+     * @returns {boolean}
55
+     */
56
+    isInitialized() {
57
+        return this.initialized;
58
+    }
59
+
60
+    /**
61
+     * Send identity data to rtcstats server, this will be reflected in the identity section of the stats dump.
62
+     * It can be generally used to send additional metadata that might be relevant such as amplitude user data
63
+     * or deployment specific information.
64
+     *
65
+     * @param {Object} identityData - Metadata object to send as identity.
66
+     * @returns {void}
67
+     */
68
+    sendIdentityData(identityData) {
69
+        this.trace && this.trace('identity', null, identityData);
70
+    }
71
+
72
+    /**
73
+     * Connect to the rtcstats server instance. Stats (data obtained from getstats) won't be send until the
74
+     * connect successfully initializes, however calls to GUM are recorded in an internal buffer even if not
75
+     * connected and sent once it is established.
76
+     *
77
+     * @returns {void}
78
+     */
79
+    connect() {
80
+        this.trace && this.trace.connect();
81
+    }
82
+
83
+    /**
84
+     * Self explanatory; closes the web socked connection.
85
+     * Note, at the point of writing this documentation there was no method to reset the function overwrites,
86
+     * thus even if the websocket is closed the global function proxies are still active but send no data,
87
+     * this shouldn't influence the normal flow of the application.
88
+     *
89
+     * @returns {void}
90
+     */
91
+    close() {
92
+        this.trace && this.trace.close();
93
+    }
94
+
95
+    /**
96
+     * The way rtcstats is currently designed the ws wouldn't normally be closed by the application logic but rather
97
+     * by the page being closed/reloaded. Using this assumption any onclose event is most likely something abnormal
98
+     * that happened on the ws. We then track this in order to determine how many rtcstats connection were closed
99
+     * prematurely.
100
+     *
101
+     * @param {Object} closeEvent - Event sent by ws onclose.
102
+     * @returns {void}
103
+     */
104
+    handleTraceWSClose(closeEvent) {
105
+        logger.info('RTCStats trace ws closed', closeEvent);
106
+
107
+        sendAnalytics(createRTCStatsTraceCloseEvent(closeEvent));
108
+    }
109
+}
110
+
111
+export default new RTCStats();

+ 1
- 0
react/features/rtcstats/index.js View File

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

+ 5
- 0
react/features/rtcstats/logger.js View File

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

+ 79
- 0
react/features/rtcstats/middleware.js View File

@@ -0,0 +1,79 @@
1
+// @flow
2
+
3
+import { getAmplitudeIdentity } from '../analytics';
4
+import {
5
+    CONFERENCE_JOINED
6
+} from '../base/conference';
7
+import { LIB_WILL_INIT } from '../base/lib-jitsi-meet';
8
+import { getLocalParticipant } from '../base/participants';
9
+import { MiddlewareRegistry } from '../base/redux';
10
+
11
+import RTCStats from './RTCStats';
12
+import logger from './logger';
13
+
14
+/**
15
+ * Middleware which intercepts lib-jitsi-meet initialization and conference join in order init the
16
+ * rtcstats-client.
17
+ *
18
+ * @param {Store} store - The redux store.
19
+ * @returns {Function}
20
+ */
21
+MiddlewareRegistry.register(store => next => action => {
22
+    const state = store.getState();
23
+    const config = state['features/base/config'];
24
+    const { analytics } = config;
25
+
26
+    switch (action.type) {
27
+    case LIB_WILL_INIT: {
28
+        if (analytics.rtcstatsEndpoint) {
29
+            // RTCStats "proxies" WebRTC functions such as GUM and RTCPeerConnection by rewriting the global
30
+            // window functions. Because lib-jitsi-meet uses references to those functions that are taken on
31
+            // init, we need to add these proxies before it initializes, otherwise lib-jitsi-meet will use the
32
+            // original non proxy versions of these functions.
33
+            try {
34
+                // Default poll interval is 1000ms if not provided in the config.
35
+                const pollInterval = analytics.rtcstatsPollInterval || 1000;
36
+
37
+                // Initialize but don't connect to the rtcstats server wss, as it will start sending data for all
38
+                // media calls made even before the conference started.
39
+                RTCStats.init({
40
+                    rtcstatsEndpoint: analytics.rtcstatsEndpoint,
41
+                    rtcstatsPollInterval: pollInterval
42
+                });
43
+            } catch (error) {
44
+                logger.error('Failed to initialize RTCStats: ', error);
45
+            }
46
+        }
47
+        break;
48
+    }
49
+    case CONFERENCE_JOINED: {
50
+        if (analytics.rtcstatsEndpoint && RTCStats.isInitialized()) {
51
+            // Once the conference started connect to the rtcstats server and send data.
52
+            try {
53
+                RTCStats.connect();
54
+
55
+                const localParticipant = getLocalParticipant(state);
56
+
57
+                // The current implementation of rtcstats-server is configured to send data to amplitude, thus
58
+                // we add identity specific information so we can corelate on the amplitude side. If amplitude is
59
+                // not configured an empty object will be sent.
60
+                // The current configuration of the conference is also sent as metadata to rtcstats server.
61
+                // This is done in order to facilitate queries based on different conference configurations.
62
+                // e.g. Find all RTCPeerConnections that connect to a specific shard or were created in a
63
+                // conference with a specific version.
64
+                RTCStats.sendIdentityData({
65
+                    ...getAmplitudeIdentity(),
66
+                    ...config,
67
+                    displayName: localParticipant?.name
68
+                });
69
+            } catch (error) {
70
+                // If the connection failed do not impact jitsi-meet just silently fail.
71
+                logger.error('RTCStats connect failed with: ', error);
72
+            }
73
+        }
74
+        break;
75
+    }
76
+    }
77
+
78
+    return next(action);
79
+});

Loading…
Cancel
Save