瀏覽代碼

Restructures the analytics events (#2333)

* ref: Restructures the pinned/unpinned events.

* ref: Refactors the "audio only disabled" event.

* ref: Refactors the "stream switch delay" event.

* ref: Refactors the "select participant failed" event.

* ref: Refactors the "initially muted" events.

* ref: Refactors the screen sharing started/stopped events.

* ref: Restructures the "device list changed" events.

* ref: Restructures the "shared video" events.

* ref: Restructures the "start muted" events.

* ref: Restructures the "start audio only" event.

* ref: Restructures the "sync track state" event.

* ref: Restructures the "callkit" events.

* ref: Restructures the "replace track".

* ref: Restructures keyboard shortcuts events.

* ref: Restructures most of the toolbar events.

* ref: Refactors the API events.

* ref: Restructures the video quality, profile button and invite dialog events.

* ref: Refactors the "device changed" events.

* ref: Refactors the page reload event.

* ref: Removes an unused function.

* ref: Removes a method which is needlessly exposed under a different name.

* ref: Refactors the events from the remote video menu.

* ref: Refactors the events from the profile pane.

* ref: Restructures the recording-related events.

Removes events fired when recording with something other than jibri
(which isn't currently supported anyway).

* ref: Cleans up AnalyticsEvents.js.

* ref: Removes an unused function and adds documentation.

* feat: Adds events for all API calls.

* fix: Addresses feedback.

* fix: Brings back mistakenly removed code.

* fix: Simplifies code and fixes a bug in toggleFilmstrip

when the 'visible' parameter is defined.

* feat: Removes the resolution change application log.

* ref: Uses consistent naming for events' attributes.

Uses "_" as a separator instead of camel case or ".".

* ref: Don't add the user agent and conference name

as permanent properties. The library does this on its own now.

* ref: Adapts the GA handler to changes in lib-jitsi-meet.

* ref: Removes unused fields from the analytics handler initializaiton.

* ref: Renames the google analytics file and add docs.

* fix: Fixes the push-to-talk events and logs.

* npm: Updates lib-jitsi-meet to 515374c8d3.

* fix: Fixes a recently introduced bug in the google analytics handler.

* ref: Uses "value" instead of "delay" since this is friendlier to GA.
master
bgrozev 7 年之前
父節點
當前提交
090f2f9ccb
共有 35 個檔案被更改,包括 1046 行新增1024 行删除
  1. 1
    1
      Makefile
  2. 146
    0
      analytics-ga.js
  3. 0
    47
      analytics.js
  4. 35
    78
      conference.js
  5. 6
    3
      config.js
  6. 36
    14
      modules/API/API.js
  7. 26
    0
      modules/UI/UI.js
  8. 35
    17
      modules/UI/recording/Recording.js
  9. 17
    19
      modules/UI/shared_video/SharedVideo.js
  10. 4
    5
      modules/UI/side_pannels/profile/Profile.js
  11. 37
    26
      modules/UI/videolayout/Filmstrip.js
  12. 13
    12
      modules/keyboardshortcut/keyboardshortcut.js
  13. 6
    6
      modules/util/JitsiMeetLogStorage.js
  14. 1
    1
      package-lock.json
  15. 1
    1
      package.json
  16. 396
    598
      react/features/analytics/AnalyticsEvents.js
  17. 12
    13
      react/features/analytics/functions.js
  18. 4
    9
      react/features/base/conference/actions.js
  19. 18
    19
      react/features/base/conference/middleware.js
  20. 8
    16
      react/features/base/media/middleware.js
  21. 6
    5
      react/features/base/tracks/actions.js
  22. 3
    3
      react/features/feedback/components/FeedbackDialog.web.js
  23. 1
    1
      react/features/filmstrip/middleware.js
  24. 3
    3
      react/features/invite/components/InviteDialog.web.js
  25. 5
    3
      react/features/mobile/background/actions.js
  26. 3
    4
      react/features/mobile/callkit/middleware.js
  27. 14
    5
      react/features/overlay/components/AbstractPageReloadOverlay.js
  28. 7
    8
      react/features/remote-video-menu/components/KickButton.js
  29. 6
    8
      react/features/remote-video-menu/components/MuteButton.js
  30. 4
    10
      react/features/remote-video-menu/components/MuteRemoteParticipantDialog.web.js
  31. 12
    17
      react/features/remote-video-menu/components/RemoteControlButton.js
  32. 7
    2
      react/features/toolbox/components/ProfileButton.web.js
  33. 14
    6
      react/features/toolbox/components/Toolbox.native.js
  34. 134
    53
      react/features/toolbox/defaultToolbarButtons.web.js
  35. 25
    11
      react/features/video-quality/components/VideoQualityDialog.web.js

+ 1
- 1
Makefile 查看文件

@@ -35,7 +35,7 @@ deploy-appbundle:
35 35
 		$(BUILD_DIR)/device_selection_popup_bundle.min.map \
36 36
 		$(BUILD_DIR)/alwaysontop.min.js \
37 37
 		$(BUILD_DIR)/alwaysontop.min.map \
38
-		$(OUTPUT_DIR)/analytics.js \
38
+		$(OUTPUT_DIR)/analytics-ga.js \
39 39
 		$(DEPLOY_DIR)
40 40
 
41 41
 deploy-lib-jitsi-meet:

+ 146
- 0
analytics-ga.js 查看文件

@@ -0,0 +1,146 @@
1
+/* global ga */
2
+
3
+(function(ctx) {
4
+    /**
5
+     *
6
+     */
7
+    function Analytics() {
8
+        /* eslint-disable */
9
+
10
+        /**
11
+         * Google Analytics
12
+         */
13
+        (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
14
+            (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
15
+        })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
16
+        ga('create', 'UA-319188-14', 'jit.si');
17
+        ga('send', 'pageview');
18
+
19
+        /* eslint-enable */
20
+    }
21
+
22
+    /**
23
+     * Extracts the integer to use for a Google Analytics event's value field
24
+     * from a lib-jitsi-meet analytics event.
25
+     * @param {Object} event - The lib-jitsi-meet analytics event.
26
+     * @returns {Object} - The integer to use for the 'value' of a Google
27
+     * Analytics event.
28
+     * @private
29
+     */
30
+    Analytics.prototype._extractAction = function(event) {
31
+        // Page events have a single 'name' field.
32
+        if (event.type === 'page') {
33
+            return event.name;
34
+        }
35
+
36
+        // All other events have action, actionSubject, and source fields. All
37
+        // three fields are required, and the often jitsi-meet and
38
+        // lib-jitsi-meet use the same value when separate values are not
39
+        // necessary (i.e. event.action == event.actionSubject).
40
+        // Here we concatenate these three fields, but avoid adding the same
41
+        // value twice, because it would only make the GA event's action harder
42
+        // to read.
43
+        let action = event.action;
44
+
45
+        if (event.actionSubject && event.actionSubject !== event.action) {
46
+            // Intentionally use string concatenation as analytics needs to
47
+            // work on IE but this file does not go through babel. For some
48
+            // reason disabling this globally for the file does not have an
49
+            // effect.
50
+            // eslint-disable-next-line prefer-template
51
+            action = event.actionSubject + '.' + action;
52
+        }
53
+        if (event.source && event.source !== event.action
54
+                && event.source !== event.action) {
55
+            // eslint-disable-next-line prefer-template
56
+            action = event.source + '.' + action;
57
+        }
58
+
59
+        return action;
60
+    };
61
+
62
+    /**
63
+     * Extracts the integer to use for a Google Analytics event's value field
64
+     * from a lib-jitsi-meet analytics event.
65
+     * @param {Object} event - The lib-jitsi-meet analytics event.
66
+     * @returns {Object} - The integer to use for the 'value' of a Google
67
+     * Analytics event, or NaN if the lib-jitsi-meet event doesn't contain a
68
+     * suitable value.
69
+     * @private
70
+     */
71
+    Analytics.prototype._extractValue = function(event) {
72
+        let value = event && event.attributes && event.attributes.value;
73
+
74
+        // Try to extract an integer from the "value" attribute.
75
+        value = Math.round(parseFloat(value));
76
+
77
+        return value;
78
+    };
79
+
80
+    /**
81
+     * Extracts the string to use for a Google Analytics event's label field
82
+     * from a lib-jitsi-meet analytics event.
83
+     * @param {Object} event - The lib-jitsi-meet analytics event.
84
+     * @returns {string} - The string to use for the 'label' of a Google
85
+     * Analytics event.
86
+     * @private
87
+     */
88
+    Analytics.prototype._extractLabel = function(event) {
89
+        let label = '';
90
+
91
+        // The label field is limited to 500B. We will concatenate all
92
+        // attributes of the event, except the user agent because it may be
93
+        // lengthy and is probably included from elsewhere.
94
+        for (const property in event.attributes) {
95
+            if (property !== 'permanent_user_agent'
96
+                && event.attributes.hasOwnProperty(property)) {
97
+                // eslint-disable-next-line prefer-template
98
+                label += property + '=' + event.attributes[property] + '&';
99
+            }
100
+        }
101
+
102
+        if (label.length > 0) {
103
+            label = label.slice(0, -1);
104
+        }
105
+
106
+        return label;
107
+    };
108
+
109
+    /**
110
+     * This is the entry point of the API. The function sends an event to
111
+     * google analytics. The format of the event is described in
112
+     * AnalyticsAdapter in lib-jitsi-meet.
113
+     * @param {Object} event - the event in the format specified by
114
+     * lib-jitsi-meet.
115
+     */
116
+    Analytics.prototype.sendEvent = function(event) {
117
+        if (!event) {
118
+            return;
119
+        }
120
+
121
+        const gaEvent = {
122
+            'eventCategory': 'jitsi-meet',
123
+            'eventAction': this._extractAction(event),
124
+            'eventLabel': this._extractLabel(event)
125
+        };
126
+        const value = this._extractValue(event);
127
+
128
+        if (!isNaN(value)) {
129
+            gaEvent.eventValue = value;
130
+        }
131
+
132
+        ga('send', 'event', gaEvent);
133
+    };
134
+
135
+    if (typeof ctx.JitsiMeetJS === 'undefined') {
136
+        ctx.JitsiMeetJS = {};
137
+    }
138
+    if (typeof ctx.JitsiMeetJS.app === 'undefined') {
139
+        ctx.JitsiMeetJS.app = {};
140
+    }
141
+    if (typeof ctx.JitsiMeetJS.app.analyticsHandlers === 'undefined') {
142
+        ctx.JitsiMeetJS.app.analyticsHandlers = [];
143
+    }
144
+    ctx.JitsiMeetJS.app.analyticsHandlers.push(Analytics);
145
+})(window);
146
+/* eslint-enable prefer-template */

+ 0
- 47
analytics.js 查看文件

@@ -1,47 +0,0 @@
1
-/* global ga */
2
-
3
-(function(ctx) {
4
-    /**
5
-     *
6
-     */
7
-    function Analytics() {
8
-        /* eslint-disable */
9
-
10
-        /**
11
-         * Google Analytics
12
-         */
13
-        (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
14
-            (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
15
-        })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
16
-        ga('create', 'UA-319188-14', 'jit.si');
17
-        ga('send', 'pageview');
18
-
19
-        /* eslint-enable */
20
-    }
21
-
22
-    Analytics.prototype.sendEvent = function(action, data) {
23
-        // empty label if missing value for it and add the value,
24
-        // the value should be integer or null
25
-        let value = data.value;
26
-
27
-        value = value ? Math.round(parseFloat(value)) : null;
28
-        const label = data.label || '';
29
-
30
-        // Intentionally use string concatenation as analytics needs to work on
31
-        // IE but this file does not go through babel.
32
-        // eslint-disable-next-line prefer-template
33
-        ga('send', 'event', 'jit.si', action + '.' + data.browserName,
34
-            label, value);
35
-    };
36
-
37
-    if (typeof ctx.JitsiMeetJS === 'undefined') {
38
-        ctx.JitsiMeetJS = {};
39
-    }
40
-    if (typeof ctx.JitsiMeetJS.app === 'undefined') {
41
-        ctx.JitsiMeetJS.app = {};
42
-    }
43
-    if (typeof ctx.JitsiMeetJS.app.analyticsHandlers === 'undefined') {
44
-        ctx.JitsiMeetJS.app.analyticsHandlers = [];
45
-    }
46
-    ctx.JitsiMeetJS.app.analyticsHandlers.push(Analytics);
47
-})(window);

+ 35
- 78
conference.js 查看文件

@@ -16,19 +16,13 @@ import UIUtil from './modules/UI/util/UIUtil';
16 16
 import * as JitsiMeetConferenceEvents from './ConferenceEvents';
17 17
 
18 18
 import {
19
-    CONFERENCE_AUDIO_INITIALLY_MUTED,
20
-    CONFERENCE_SHARING_DESKTOP_START,
21
-    CONFERENCE_SHARING_DESKTOP_STOP,
22
-    CONFERENCE_VIDEO_INITIALLY_MUTED,
23
-    DEVICE_LIST_CHANGED_AUDIO_MUTED,
24
-    DEVICE_LIST_CHANGED_VIDEO_MUTED,
25
-    SELECT_PARTICIPANT_FAILED,
26
-    SETTINGS_CHANGE_DEVICE_AUDIO_OUT,
27
-    SETTINGS_CHANGE_DEVICE_AUDIO_IN,
28
-    SETTINGS_CHANGE_DEVICE_VIDEO,
29
-    STREAM_SWITCH_DELAY,
19
+    createDeviceChangedEvent,
20
+    createScreenSharingEvent,
21
+    createSelectParticipantFailedEvent,
22
+    createStreamSwitchDelayEvent,
23
+    createTrackMutedEvent,
30 24
     initAnalytics,
31
-    sendAnalyticsEvent
25
+    sendAnalytics
32 26
 } from './react/features/analytics';
33 27
 
34 28
 import EventEmitter from 'events';
@@ -741,14 +735,13 @@ export default {
741 735
             })
742 736
             .then(([ tracks, con ]) => {
743 737
                 tracks.forEach(track => {
744
-                    if (track.isAudioTrack() && this.isLocalAudioMuted()) {
745
-                        sendAnalyticsEvent(CONFERENCE_AUDIO_INITIALLY_MUTED);
746
-                        logger.log('Audio mute: initially muted');
747
-                        track.mute();
748
-                    } else if (track.isVideoTrack()
749
-                                    && this.isLocalVideoMuted()) {
750
-                        sendAnalyticsEvent(CONFERENCE_VIDEO_INITIALLY_MUTED);
751
-                        logger.log('Video mute: initially muted');
738
+                    if ((track.isAudioTrack() && this.isLocalAudioMuted())
739
+                        || (track.isVideoTrack() && this.isLocalVideoMuted())) {
740
+                        const mediaType = track.getType();
741
+
742
+                        sendAnalytics(
743
+                            createTrackMutedEvent(mediaType, 'initial mute'));
744
+                        logger.log(`${mediaType} mute: initially muted.`);
752 745
                         track.mute();
753 746
                     }
754 747
                 });
@@ -1453,8 +1446,9 @@ export default {
1453 1446
             promise = createLocalTracksF({ devices: [ 'video' ] })
1454 1447
                 .then(([ stream ]) => this.useVideoStream(stream))
1455 1448
                 .then(() => {
1456
-                    sendAnalyticsEvent(CONFERENCE_SHARING_DESKTOP_STOP);
1457
-                    logger.log('switched back to local video');
1449
+                    sendAnalytics(createScreenSharingEvent('stopped'));
1450
+                    logger.log('Screen sharing stopped, switching to video.');
1451
+
1458 1452
                     if (!this.localVideo && wasVideoMuted) {
1459 1453
                         return Promise.reject('No local video to be muted!');
1460 1454
                     } else if (wasVideoMuted && this.localVideo) {
@@ -1609,7 +1603,7 @@ export default {
1609 1603
     },
1610 1604
 
1611 1605
     /**
1612
-     * Tries to switch to the screenshairng mode by disposing camera stream and
1606
+     * Tries to switch to the screensharing mode by disposing camera stream and
1613 1607
      * replacing it with a desktop one.
1614 1608
      *
1615 1609
      * @param {Object} [options] - Screen sharing options that will be passed to
@@ -1632,8 +1626,8 @@ export default {
1632 1626
             .then(stream => this.useVideoStream(stream))
1633 1627
             .then(() => {
1634 1628
                 this.videoSwitchInProgress = false;
1635
-                sendAnalyticsEvent(CONFERENCE_SHARING_DESKTOP_START);
1636
-                logger.log('sharing local desktop');
1629
+                sendAnalytics(createScreenSharingEvent('started'));
1630
+                logger.log('Screen sharing started');
1637 1631
             })
1638 1632
             .catch(error => {
1639 1633
                 this.videoSwitchInProgress = false;
@@ -1928,7 +1922,7 @@ export default {
1928 1922
 
1929 1923
                     room.selectParticipant(id);
1930 1924
                 } catch (e) {
1931
-                    sendAnalyticsEvent(SELECT_PARTICIPANT_FAILED);
1925
+                    sendAnalytics(createSelectParticipantFailedEvent(e));
1932 1926
                     reportError(e);
1933 1927
                 }
1934 1928
             });
@@ -2152,22 +2146,12 @@ export default {
2152 2146
         APP.UI.addListener(
2153 2147
             UIEvents.RESOLUTION_CHANGED,
2154 2148
             (id, oldResolution, newResolution, delay) => {
2155
-                const logObject = {
2156
-                    id: 'resolution_change',
2157
-                    participant: id,
2158
-                    oldValue: oldResolution,
2159
-                    newValue: newResolution,
2160
-                    delay
2161
-                };
2162
-
2163
-                room.sendApplicationLog(JSON.stringify(logObject));
2164
-
2165
-                // We only care about the delay between simulcast streams.
2166
-                // Longer delays will be caused by something else and will just
2167
-                // poison the data.
2168
-                if (delay < 2000) {
2169
-                    sendAnalyticsEvent(STREAM_SWITCH_DELAY, { value: delay });
2170
-                }
2149
+                sendAnalytics(createStreamSwitchDelayEvent(
2150
+                    {
2151
+                        'old_resolution': oldResolution,
2152
+                        'new_resolution': newResolution,
2153
+                        value: delay
2154
+                    }));
2171 2155
             });
2172 2156
 
2173 2157
         /* eslint-enable max-params */
@@ -2193,7 +2177,7 @@ export default {
2193 2177
             cameraDeviceId => {
2194 2178
                 const videoWasMuted = this.isLocalVideoMuted();
2195 2179
 
2196
-                sendAnalyticsEvent(SETTINGS_CHANGE_DEVICE_VIDEO);
2180
+                sendAnalytics(createDeviceChangedEvent('video', 'input'));
2197 2181
                 createLocalTracksF({
2198 2182
                     devices: [ 'video' ],
2199 2183
                     cameraDeviceId,
@@ -2232,7 +2216,7 @@ export default {
2232 2216
             micDeviceId => {
2233 2217
                 const audioWasMuted = this.isLocalAudioMuted();
2234 2218
 
2235
-                sendAnalyticsEvent(SETTINGS_CHANGE_DEVICE_AUDIO_IN);
2219
+                sendAnalytics(createDeviceChangedEvent('audio', 'input'));
2236 2220
                 createLocalTracksF({
2237 2221
                     devices: [ 'audio' ],
2238 2222
                     cameraDeviceId: null,
@@ -2262,7 +2246,7 @@ export default {
2262 2246
         APP.UI.addListener(
2263 2247
             UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED,
2264 2248
             audioOutputDeviceId => {
2265
-                sendAnalyticsEvent(SETTINGS_CHANGE_DEVICE_AUDIO_OUT);
2249
+                sendAnalytics(createDeviceChangedEvent('audio', 'output'));
2266 2250
                 APP.settings.setAudioOutputDeviceId(audioOutputDeviceId)
2267 2251
                     .then(() => logger.log('changed audio output device'))
2268 2252
                     .catch(err => {
@@ -2528,7 +2512,9 @@ export default {
2528 2512
                     // If audio was muted before, or we unplugged current device
2529 2513
                     // and selected new one, then mute new audio track.
2530 2514
                     if (audioWasMuted) {
2531
-                        sendAnalyticsEvent(DEVICE_LIST_CHANGED_AUDIO_MUTED);
2515
+                        sendAnalytics(createTrackMutedEvent(
2516
+                            'audio',
2517
+                            'device list changed'));
2532 2518
                         logger.log('Audio mute: device list changed');
2533 2519
                         muteLocalAudio(true);
2534 2520
                     }
@@ -2536,7 +2522,9 @@ export default {
2536 2522
                     // If video was muted before, or we unplugged current device
2537 2523
                     // and selected new one, then mute new video track.
2538 2524
                     if (!this.isSharingScreen && videoWasMuted) {
2539
-                        sendAnalyticsEvent(DEVICE_LIST_CHANGED_VIDEO_MUTED);
2525
+                        sendAnalytics(createTrackMutedEvent(
2526
+                            'video',
2527
+                            'device list changed'));
2540 2528
                         logger.log('Video mute: device list changed');
2541 2529
                         muteLocalVideo(true);
2542 2530
                     }
@@ -2622,37 +2610,6 @@ export default {
2622 2610
         }
2623 2611
     },
2624 2612
 
2625
-    /**
2626
-     * Log event to callstats and analytics.
2627
-     * @param {string} name the event name
2628
-     * @param {int} value the value (it's int because google analytics supports
2629
-     * only int).
2630
-     * @param {string} label short text which provides more info about the event
2631
-     * which allows to distinguish between few event cases of the same name
2632
-     * NOTE: Should be used after conference.init
2633
-     */
2634
-    logEvent(name, value, label) {
2635
-        sendAnalyticsEvent(name, {
2636
-            value,
2637
-            label
2638
-        });
2639
-        if (room) {
2640
-            room.sendApplicationLog(JSON.stringify({ name,
2641
-                value,
2642
-                label }));
2643
-        }
2644
-    },
2645
-
2646
-    /**
2647
-     * Methods logs an application event given in the JSON format.
2648
-     * @param {string} logJSON an event to be logged in JSON format
2649
-     */
2650
-    logJSON(logJSON) {
2651
-        if (room) {
2652
-            room.sendApplicationLog(logJSON);
2653
-        }
2654
-    },
2655
-
2656 2613
     /**
2657 2614
      * Disconnect from the conference and optionally request user feedback.
2658 2615
      * @param {boolean} [requestFeedback=false] if user feedback should be

+ 6
- 3
config.js 查看文件

@@ -307,21 +307,24 @@ var config = {
307 307
         // backToP2PDelay: 5
308 308
     },
309 309
 
310
+    // A list of scripts to load as lib-jitsi-meet "analytics handlers".
311
+    // analyticsScriptUrls: [
312
+    //      "libs/analytics-ga.js", // google-analytics
313
+    //      "https://example.com/my-custom-analytics.js"
314
+    // ],
310 315
 
311 316
     // Information about the jitsi-meet instance we are connecting to, including
312 317
     // the user region as seen by the server.
313
-    //
314
-
315 318
     deploymentInfo: {
316 319
         // shard: "shard1",
317 320
         // region: "europe",
318 321
         // userRegion: "asia"
319 322
     }
320 323
 
324
+
321 325
     // List of undocumented settings used in jitsi-meet
322 326
     /**
323 327
      alwaysVisibleToolbar
324
-     analyticsScriptUrls
325 328
      autoEnableDesktopSharing
326 329
      autoRecord
327 330
      autoRecordToken

+ 36
- 14
modules/API/API.js 查看文件

@@ -3,9 +3,8 @@
3 3
 import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
4 4
 import { parseJWTFromURLParams } from '../../react/features/base/jwt';
5 5
 import {
6
-    API_TOGGLE_AUDIO,
7
-    API_TOGGLE_VIDEO,
8
-    sendAnalyticsEvent
6
+    createApiEvent,
7
+    sendAnalytics
9 8
 } from '../../react/features/analytics';
10 9
 import { getJitsiMeetTransport } from '../transport';
11 10
 
@@ -56,25 +55,48 @@ let videoAvailable = true;
56 55
  */
57 56
 function initCommands() {
58 57
     commands = {
59
-        'display-name':
60
-            APP.conference.changeLocalDisplayName.bind(APP.conference),
58
+        'display-name': displayName => {
59
+            sendAnalytics(createApiEvent('display.name.changed'));
60
+            APP.conference.changeLocalDisplayName(displayName);
61
+        },
61 62
         'toggle-audio': () => {
62
-            sendAnalyticsEvent(API_TOGGLE_AUDIO);
63
+            sendAnalytics(createApiEvent('toggle-audio'));
63 64
             logger.log('Audio toggle: API command received');
64 65
             APP.conference.toggleAudioMuted(false /* no UI */);
65 66
         },
66 67
         'toggle-video': () => {
67
-            sendAnalyticsEvent(API_TOGGLE_VIDEO);
68
+            sendAnalytics(createApiEvent('toggle-video'));
68 69
             logger.log('Video toggle: API command received');
69 70
             APP.conference.toggleVideoMuted(false /* no UI */);
70 71
         },
71
-        'toggle-film-strip': APP.UI.toggleFilmstrip,
72
-        'toggle-chat': APP.UI.toggleChat,
73
-        'toggle-contact-list': APP.UI.toggleContactList,
74
-        'toggle-share-screen': toggleScreenSharing,
75
-        'video-hangup': () => APP.conference.hangup(true),
76
-        'email': APP.conference.changeLocalEmail,
77
-        'avatar-url': APP.conference.changeLocalAvatarUrl
72
+        'toggle-film-strip': () => {
73
+            sendAnalytics(createApiEvent('film.strip.toggled'));
74
+            APP.UI.toggleFilmstrip();
75
+        },
76
+        'toggle-chat': () => {
77
+            sendAnalytics(createApiEvent('chat.toggled'));
78
+            APP.UI.toggleChat();
79
+        },
80
+        'toggle-contact-list': () => {
81
+            sendAnalytics(createApiEvent('contact.list.toggled'));
82
+            APP.UI.toggleContactList();
83
+        },
84
+        'toggle-share-screen': () => {
85
+            sendAnalytics(createApiEvent('screen.sharing.toggled'));
86
+            toggleScreenSharing();
87
+        },
88
+        'video-hangup': () => {
89
+            sendAnalytics(createApiEvent('video.hangup'));
90
+            APP.conference.hangup(true);
91
+        },
92
+        'email': email => {
93
+            sendAnalytics(createApiEvent('email.changed'));
94
+            APP.conference.changeLocalEmail(email);
95
+        },
96
+        'avatar-url': avatarUrl => {
97
+            sendAnalytics(createApiEvent('avatar.url.changed'));
98
+            APP.conference.changeLocalAvatarUrl(avatarUrl);
99
+        }
78 100
     };
79 101
     transport.on('event', ({ data, name }) => {
80 102
         if (name && commands[name]) {

+ 26
- 0
modules/UI/UI.js 查看文件

@@ -142,6 +142,32 @@ UI.toggleFullScreen = function() {
142 142
     UIUtil.isFullScreen() ? UIUtil.exitFullScreen() : UIUtil.enterFullScreen();
143 143
 };
144 144
 
145
+/**
146
+ * Indicates if we're currently in full screen mode.
147
+ *
148
+ * @return {boolean} {true} to indicate that we're currently in full screen
149
+ * mode, {false} otherwise
150
+ */
151
+UI.isFullScreen = function() {
152
+    return UIUtil.isFullScreen();
153
+};
154
+
155
+/**
156
+ * Returns true if the etherpad window is currently visible.
157
+ * @returns {Boolean} - true if the etherpad window is currently visible.
158
+ */
159
+UI.isEtherpadVisible = function() {
160
+    return Boolean(etherpadManager && etherpadManager.isVisible());
161
+};
162
+
163
+/**
164
+ * Returns true if there is a shared video which is being shown (?).
165
+ * @returns {boolean} - true if there is a shared video which is being shown.
166
+ */
167
+UI.isSharedVideoShown = function() {
168
+    return Boolean(sharedVideoManager && sharedVideoManager.isSharedVideoShown);
169
+};
170
+
145 171
 /**
146 172
  * Notify user that server has shut down.
147 173
  */

+ 35
- 17
modules/UI/recording/Recording.js 查看文件

@@ -24,11 +24,9 @@ import {
24 24
     JitsiRecordingStatus
25 25
 } from '../../../react/features/base/lib-jitsi-meet';
26 26
 import {
27
-    RECORDING_CANCELED,
28
-    RECORDING_CLICKED,
29
-    RECORDING_STARTED,
30
-    RECORDING_STOPPED,
31
-    sendAnalyticsEvent
27
+    createToolbarEvent,
28
+    createRecordingDialogEvent,
29
+    sendAnalytics
32 30
 } from '../../../react/features/analytics';
33 31
 import { setToolboxEnabled } from '../../../react/features/toolbox';
34 32
 import { setNotificationsEnabled } from '../../../react/features/notifications';
@@ -452,12 +450,13 @@ const Recording = {
452 450
     },
453 451
 
454 452
     // checks whether recording is enabled and whether we have params
455
-    // to start automatically recording
453
+    // to start automatically recording (XXX: No, it doesn't do that).
456 454
     checkAutoRecord() {
457 455
         if (_isRecordingButtonEnabled && config.autoRecord) {
458 456
             this.predefinedToken = UIUtil.escapeHtml(config.autoRecordToken);
459
-            this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED,
460
-                                    this.predefinedToken);
457
+            this.eventEmitter.emit(
458
+                UIEvents.RECORDING_TOGGLED,
459
+                { token: this.predefinedToken });
461 460
         }
462 461
     },
463 462
 
@@ -467,11 +466,16 @@ const Recording = {
467 466
      * @returns {void}
468 467
      */
469 468
     _onToolbarButtonClick() {
469
+        sendAnalytics(createToolbarEvent(
470
+            'recording.button',
471
+            {
472
+                'dialog_present': Boolean(dialog)
473
+            }));
474
+
470 475
         if (dialog) {
471 476
             return;
472 477
         }
473 478
 
474
-        sendAnalyticsEvent(RECORDING_CLICKED);
475 479
         switch (this.currentState) {
476 480
         case JitsiRecordingStatus.ON:
477 481
         case JitsiRecordingStatus.RETRYING:
@@ -479,7 +483,13 @@ const Recording = {
479 483
             _showStopRecordingPrompt(this.recordingType).then(
480 484
                 () => {
481 485
                     this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED);
482
-                    sendAnalyticsEvent(RECORDING_STOPPED);
486
+
487
+                    // The confirm button on the stop recording dialog was
488
+                    // clicked
489
+                    sendAnalytics(
490
+                        createRecordingDialogEvent(
491
+                            'stop',
492
+                            'confirm.button'));
483 493
                 },
484 494
                 () => {}); // eslint-disable-line no-empty-function
485 495
             break;
@@ -492,21 +502,32 @@ const Recording = {
492 502
                     this.eventEmitter.emit(
493 503
                         UIEvents.RECORDING_TOGGLED,
494 504
                         { streamId });
495
-                    sendAnalyticsEvent(RECORDING_STARTED);
505
+
506
+                    // The confirm button on the start recording dialog was
507
+                    // clicked
508
+                    sendAnalytics(
509
+                        createRecordingDialogEvent(
510
+                            'start',
511
+                            'confirm.button'));
496 512
                 })
497 513
                 .catch(reason => {
498 514
                     if (reason === APP.UI.messageHandler.CANCEL) {
499
-                        sendAnalyticsEvent(RECORDING_CANCELED);
515
+                        // The cancel button on the start recording dialog was
516
+                        // clicked
517
+                        sendAnalytics(
518
+                            createRecordingDialogEvent(
519
+                                'start',
520
+                                'cancel.button'));
500 521
                     } else {
501 522
                         logger.error(reason);
502 523
                     }
503 524
                 });
504 525
             } else {
526
+                // Note that we only fire analytics events for Jibri.
505 527
                 if (this.predefinedToken) {
506 528
                     this.eventEmitter.emit(
507 529
                         UIEvents.RECORDING_TOGGLED,
508 530
                         { token: this.predefinedToken });
509
-                    sendAnalyticsEvent(RECORDING_STARTED);
510 531
 
511 532
                     return;
512 533
                 }
@@ -515,12 +536,9 @@ const Recording = {
515 536
                     this.eventEmitter.emit(
516 537
                         UIEvents.RECORDING_TOGGLED,
517 538
                         { token });
518
-                    sendAnalyticsEvent(RECORDING_STARTED);
519 539
                 })
520 540
                 .catch(reason => {
521
-                    if (reason === APP.UI.messageHandler.CANCEL) {
522
-                        sendAnalyticsEvent(RECORDING_CANCELED);
523
-                    } else {
541
+                    if (reason !== APP.UI.messageHandler.CANCEL) {
524 542
                         logger.error(reason);
525 543
                     }
526 544
                 });

+ 17
- 19
modules/UI/shared_video/SharedVideo.js 查看文件

@@ -11,15 +11,8 @@ import LargeContainer from '../videolayout/LargeContainer';
11 11
 import Filmstrip from '../videolayout/Filmstrip';
12 12
 
13 13
 import {
14
-    SHARED_VIDEO_ALREADY_SHARED,
15
-    SHARED_VIDEO_AUDIO_MUTED,
16
-    SHARED_VIDEO_AUDIO_UNMUTED,
17
-    SHARED_VIDEO_CANCELED,
18
-    SHARED_VIDEO_PAUSED,
19
-    SHARED_VIDEO_STARTED,
20
-    SHARED_VIDEO_STOPPED,
21
-    SHARED_VIDEO_VOLUME_CHANGED,
22
-    sendAnalyticsEvent
14
+    createSharedVideoEvent as createEvent,
15
+    sendAnalytics
23 16
 } from '../../../react/features/analytics';
24 17
 import {
25 18
     participantJoined,
@@ -95,11 +88,11 @@ export default class SharedVideoManager {
95 88
                     url => {
96 89
                         this.emitter.emit(
97 90
                             UIEvents.UPDATE_SHARED_VIDEO, url, 'start');
98
-                        sendAnalyticsEvent(SHARED_VIDEO_STARTED);
91
+                        sendAnalytics(createEvent('started'));
99 92
                     },
100 93
                     err => {
101 94
                         logger.log('SHARED VIDEO CANCELED', err);
102
-                        sendAnalyticsEvent(SHARED_VIDEO_CANCELED);
95
+                        sendAnalytics(createEvent('canceled'));
103 96
                     }
104 97
             );
105 98
 
@@ -119,7 +112,7 @@ export default class SharedVideoManager {
119 112
                     }
120 113
                     this.emitter.emit(
121 114
                         UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop');
122
-                    sendAnalyticsEvent(SHARED_VIDEO_STOPPED);
115
+                    sendAnalytics(createEvent('stopped'));
123 116
                 },
124 117
                 () => {}); // eslint-disable-line no-empty-function
125 118
         } else {
@@ -127,7 +120,7 @@ export default class SharedVideoManager {
127 120
                 descriptionKey: 'dialog.alreadySharedVideoMsg',
128 121
                 titleKey: 'dialog.alreadySharedVideoTitle'
129 122
             });
130
-            sendAnalyticsEvent(SHARED_VIDEO_ALREADY_SHARED);
123
+            sendAnalytics(createEvent('already.shared'));
131 124
         }
132 125
     }
133 126
 
@@ -236,7 +229,7 @@ export default class SharedVideoManager {
236 229
                 // eslint-disable-next-line eqeqeq
237 230
             } else if (event.data == YT.PlayerState.PAUSED) {
238 231
                 self.smartAudioUnmute();
239
-                sendAnalyticsEvent(SHARED_VIDEO_PAUSED);
232
+                sendAnalytics(createEvent('paused'));
240 233
             }
241 234
             // eslint-disable-next-line eqeqeq
242 235
             self.fireSharedVideoEvent(event.data == YT.PlayerState.PAUSED);
@@ -268,7 +261,12 @@ export default class SharedVideoManager {
268 261
             } else if (event.data.volume <= 0 || event.data.muted) {
269 262
                 self.smartAudioUnmute();
270 263
             }
271
-            sendAnalyticsEvent(SHARED_VIDEO_VOLUME_CHANGED);
264
+            sendAnalytics(createEvent(
265
+                'volume.changed',
266
+                {
267
+                    volume: event.data.volume,
268
+                    muted: event.data.muted
269
+                }));
272 270
         };
273 271
 
274 272
         window.onPlayerReady = function(event) {
@@ -434,8 +432,8 @@ export default class SharedVideoManager {
434 432
     }
435 433
 
436 434
     /**
437
-     * Updates video, if its not playing and needs starting or
438
-     * if its playing and needs to be paysed
435
+     * Updates video, if it's not playing and needs starting or if it's playing
436
+     * and needs to be paused.
439 437
      * @param id the id of the sender of the command
440 438
      * @param url the video url
441 439
      * @param attributes
@@ -574,7 +572,7 @@ export default class SharedVideoManager {
574 572
         if (APP.conference.isLocalAudioMuted()
575 573
             && !this.mutedWithUserInteraction
576 574
             && !this.isSharedVideoVolumeOn()) {
577
-            sendAnalyticsEvent(SHARED_VIDEO_AUDIO_UNMUTED);
575
+            sendAnalytics(createEvent('audio.unmuted'));
578 576
             logger.log('Shared video: audio unmuted');
579 577
             this.emitter.emit(UIEvents.AUDIO_MUTED, false, false);
580 578
             this.showMicMutedPopup(false);
@@ -588,7 +586,7 @@ export default class SharedVideoManager {
588 586
     smartAudioMute() {
589 587
         if (!APP.conference.isLocalAudioMuted()
590 588
             && this.isSharedVideoVolumeOn()) {
591
-            sendAnalyticsEvent(SHARED_VIDEO_AUDIO_MUTED);
589
+            sendAnalytics(createEvent('audio.muted'));
592 590
             logger.log('Shared video: audio muted');
593 591
             this.emitter.emit(UIEvents.AUDIO_MUTED, true, false);
594 592
             this.showMicMutedPopup(true);

+ 4
- 5
modules/UI/side_pannels/profile/Profile.js 查看文件

@@ -4,9 +4,8 @@ import UIEvents from '../../../../service/UI/UIEvents';
4 4
 import Settings from '../../../settings/Settings';
5 5
 
6 6
 import {
7
-    AUTHENTICATE_LOGIN_CLICKED,
8
-    AUTHENTICATE_LOGOUT_CLICKED,
9
-    sendAnalyticsEvent
7
+    createProfilePanelButtonEvent,
8
+    sendAnalytics
10 9
 } from '../../../../react/features/analytics';
11 10
 
12 11
 const sidePanelsContainerId = 'sideToolbarContainer';
@@ -95,7 +94,7 @@ export default {
95 94
          *
96 95
          */
97 96
         function loginClicked() {
98
-            sendAnalyticsEvent(AUTHENTICATE_LOGIN_CLICKED);
97
+            sendAnalytics(createProfilePanelButtonEvent('login.button'));
99 98
             emitter.emit(UIEvents.AUTH_CLICKED);
100 99
         }
101 100
 
@@ -108,7 +107,7 @@ export default {
108 107
             const titleKey = 'dialog.logoutTitle';
109 108
             const msgKey = 'dialog.logoutQuestion';
110 109
 
111
-            sendAnalyticsEvent(AUTHENTICATE_LOGOUT_CLICKED);
110
+            sendAnalytics(createProfilePanelButtonEvent('logout.button'));
112 111
 
113 112
             // Ask for confirmation
114 113
             APP.UI.messageHandler.openTwoButtonDialog({

+ 37
- 26
modules/UI/videolayout/Filmstrip.js 查看文件

@@ -6,8 +6,9 @@ import UIEvents from '../../../service/UI/UIEvents';
6 6
 import UIUtil from '../util/UIUtil';
7 7
 
8 8
 import {
9
-    TOOLBAR_FILMSTRIP_TOGGLED,
10
-    sendAnalyticsEvent
9
+    createShortcutEvent,
10
+    createToolbarEvent,
11
+    sendAnalytics
11 12
 } from '../../../react/features/analytics';
12 13
 
13 14
 const Filmstrip = {
@@ -75,8 +76,18 @@ const Filmstrip = {
75 76
         // Firing the event instead of executing toggleFilmstrip method because
76 77
         // it's important to hide the filmstrip by UI.toggleFilmstrip in order
77 78
         // to correctly resize the video area.
78
-        $('#toggleFilmstripButton').on('click',
79
-            () => this.eventEmitter.emit(UIEvents.TOGGLE_FILMSTRIP));
79
+        $('#toggleFilmstripButton').on(
80
+            'click',
81
+            () => {
82
+                // The 'enable' parameter is set to true if the action results
83
+                // in the filmstrip being hidden.
84
+                sendAnalytics(createToolbarEvent(
85
+                    'toggle.filmstrip.button',
86
+                    {
87
+                        enable: this.isFilmstripVisible()
88
+                    }));
89
+                this.eventEmitter.emit(UIEvents.TOGGLE_FILMSTRIP);
90
+            });
80 91
 
81 92
         this._registerToggleFilmstripShortcut();
82 93
     },
@@ -94,7 +105,14 @@ const Filmstrip = {
94 105
         // Firing the event instead of executing toggleFilmstrip method because
95 106
         // it's important to hide the filmstrip by UI.toggleFilmstrip in order
96 107
         // to correctly resize the video area.
97
-        const handler = () => this.eventEmitter.emit(UIEvents.TOGGLE_FILMSTRIP);
108
+        const handler = () => {
109
+            sendAnalytics(createShortcutEvent(
110
+                'toggle.filmstrip',
111
+                {
112
+                    enable: this.isFilmstripVisible()
113
+                }));
114
+            this.eventEmitter.emit(UIEvents.TOGGLE_FILMSTRIP);
115
+        };
98 116
 
99 117
         APP.keyboardshortcut.registerShortcut(
100 118
             shortcut,
@@ -129,50 +147,43 @@ const Filmstrip = {
129 147
     },
130 148
 
131 149
     /**
132
-     * Toggles the visibility of the filmstrip.
150
+     * Toggles the visibility of the filmstrip, or sets it to a specific value
151
+     * if the 'visible' parameter is specified.
133 152
      *
134 153
      * @param visible optional {Boolean} which specifies the desired visibility
135 154
      * of the filmstrip. If not specified, the visibility will be flipped
136 155
      * (i.e. toggled); otherwise, the visibility will be set to the specified
137 156
      * value.
138
-     * @param {Boolean} sendAnalytics - True to send an analytics event. The
139
-     * default value is true.
140 157
      *
141 158
      * Note:
142 159
      * This method shouldn't be executed directly to hide the filmstrip.
143 160
      * It's important to hide the filmstrip with UI.toggleFilmstrip in order
144 161
      * to correctly resize the video area.
145 162
      */
146
-    toggleFilmstrip(visible, sendAnalytics = true) {
147
-        const isVisibleDefined = typeof visible === 'boolean';
163
+    toggleFilmstrip(visible) {
164
+        const wasFilmstripVisible = this.isFilmstripVisible();
148 165
 
149
-        if (!isVisibleDefined) {
150
-            // eslint-disable-next-line no-param-reassign
151
-            visible = this.isFilmstripVisible();
152
-        } else if (this.isFilmstripVisible() === visible) {
166
+        // If 'visible' is defined and matches the current state, we have
167
+        // nothing to do. Otherwise (regardless of whether 'visible' is defined)
168
+        // we need to toggle the state.
169
+        if (visible === wasFilmstripVisible) {
153 170
             return;
154 171
         }
155
-        if (sendAnalytics) {
156
-            sendAnalyticsEvent(TOOLBAR_FILMSTRIP_TOGGLED);
157
-        }
172
+
158 173
         this.filmstrip.toggleClass('hidden');
159 174
 
160
-        if (visible) {
175
+        if (wasFilmstripVisible) {
161 176
             this.showMenuUpIcon();
162 177
         } else {
163 178
             this.showMenuDownIcon();
164 179
         }
165 180
 
166
-        // Emit/fire UIEvents.TOGGLED_FILMSTRIP.
167
-        const eventEmitter = this.eventEmitter;
168
-        const isFilmstripVisible = this.isFilmstripVisible();
169
-
170
-        if (eventEmitter) {
171
-            eventEmitter.emit(
181
+        if (this.eventEmitter) {
182
+            this.eventEmitter.emit(
172 183
                 UIEvents.TOGGLED_FILMSTRIP,
173
-                this.isFilmstripVisible());
184
+                !wasFilmstripVisible);
174 185
         }
175
-        APP.store.dispatch(setFilmstripVisibility(isFilmstripVisible));
186
+        APP.store.dispatch(setFilmstripVisibility(!wasFilmstripVisible));
176 187
     },
177 188
 
178 189
     /**

+ 13
- 12
modules/keyboardshortcut/keyboardshortcut.js 查看文件

@@ -2,11 +2,10 @@
2 2
 
3 3
 import { toggleDialog } from '../../react/features/base/dialog';
4 4
 import {
5
-    SHORTCUT_HELP,
6
-    SHORTCUT_SPEAKER_STATS_CLICKED,
7
-    SHORTCUT_TALK_CLICKED,
8
-    SHORTCUT_TALK_RELEASED,
9
-    sendAnalyticsEvent
5
+    ACTION_SHORTCUT_PRESSED as PRESSED,
6
+    ACTION_SHORTCUT_RELEASED as RELEASED,
7
+    createShortcutEvent,
8
+    sendAnalytics
10 9
 } from '../../react/features/analytics';
11 10
 import { KeyboardShortcutsDialog }
12 11
     from '../../react/features/keyboard-shortcuts';
@@ -72,8 +71,10 @@ const KeyboardShortcut = {
72 71
                 || $(':focus').is('textarea'))) {
73 72
                 if (this._getKeyboardKey(e).toUpperCase() === ' ') {
74 73
                     if (APP.conference.isLocalAudioMuted()) {
75
-                        sendAnalyticsEvent(SHORTCUT_TALK_RELEASED);
76
-                        logger.log('Talk shortcut released');
74
+                        sendAnalytics(createShortcutEvent(
75
+                            'push.to.talk',
76
+                            PRESSED));
77
+                        logger.log('Talk shortcut pressed');
77 78
                         APP.conference.muteAudio(false);
78 79
                     }
79 80
                 }
@@ -93,7 +94,7 @@ const KeyboardShortcut = {
93 94
      * Registers a new shortcut.
94 95
      *
95 96
      * @param shortcutChar the shortcut character triggering the action
96
-     * @param shortcutAttr the "shortcut" html element attribute mappring an
97
+     * @param shortcutAttr the "shortcut" html element attribute mapping an
97 98
      * element to this shortcut and used to show the shortcut character on the
98 99
      * element tooltip
99 100
      * @param exec the function to be executed when the shortcut is pressed
@@ -175,7 +176,7 @@ const KeyboardShortcut = {
175 176
      */
176 177
     _initGlobalShortcuts() {
177 178
         this.registerShortcut('?', null, () => {
178
-            sendAnalyticsEvent(SHORTCUT_HELP);
179
+            sendAnalytics(createShortcutEvent('help'));
179 180
             APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
180 181
                 shortcutDescriptions: _shortcutsHelp
181 182
             }));
@@ -184,15 +185,15 @@ const KeyboardShortcut = {
184 185
         // register SPACE shortcut in two steps to insure visibility of help
185 186
         // message
186 187
         this.registerShortcut(' ', null, () => {
187
-            sendAnalyticsEvent(SHORTCUT_TALK_CLICKED);
188
-            logger.log('Talk shortcut pressed');
188
+            sendAnalytics(createShortcutEvent('push.to.talk', RELEASED));
189
+            logger.log('Talk shortcut released');
189 190
             APP.conference.muteAudio(true);
190 191
         });
191 192
         this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
192 193
 
193 194
         if (!interfaceConfig.filmStripOnly) {
194 195
             this.registerShortcut('T', null, () => {
195
-                sendAnalyticsEvent(SHORTCUT_SPEAKER_STATS_CLICKED);
196
+                sendAnalytics(createShortcutEvent('speaker.stats'));
196 197
                 APP.store.dispatch(toggleDialog(SpeakerStats, {
197 198
                     conference: APP.conference
198 199
                 }));

+ 6
- 6
modules/util/JitsiMeetLogStorage.js 查看文件

@@ -37,20 +37,20 @@ export default class JitsiMeetLogStorage {
37 37
             return;
38 38
         }
39 39
 
40
-        let logJSON = `{"log${this.counter}":"\n`;
40
+        let logMessage = `{"log${this.counter}":"\n`;
41 41
 
42 42
         for (let i = 0, len = logEntries.length; i < len; i++) {
43 43
             const logEntry = logEntries[i];
44 44
 
45 45
             if (typeof logEntry === 'object') {
46 46
                 // Aggregated message
47
-                logJSON += `(${logEntry.count}) ${logEntry.text}\n`;
47
+                logMessage += `(${logEntry.count}) ${logEntry.text}\n`;
48 48
             } else {
49 49
                 // Regular message
50
-                logJSON += `${logEntry}\n`;
50
+                logMessage += `${logEntry}\n`;
51 51
             }
52 52
         }
53
-        logJSON += '"}';
53
+        logMessage += '"}';
54 54
 
55 55
         this.counter += 1;
56 56
 
@@ -58,11 +58,11 @@ export default class JitsiMeetLogStorage {
58 58
         // on the way that could be uninitialized if the storeLogs
59 59
         // attempt would be made very early (which is unlikely)
60 60
         try {
61
-            APP.conference.logJSON(logJSON);
61
+            APP.conference.room.sendApplicationLog(logMessage);
62 62
         } catch (error) {
63 63
             // NOTE console is intentional here
64 64
             console.error(
65
-                'Failed to store the logs: ', logJSON, error);
65
+                'Failed to store the logs: ', logMessage, error);
66 66
         }
67 67
     }
68 68
 }

+ 1
- 1
package-lock.json 查看文件

@@ -6386,7 +6386,7 @@
6386 6386
       }
6387 6387
     },
6388 6388
     "lib-jitsi-meet": {
6389
-      "version": "github:jitsi/lib-jitsi-meet#c7d6d158b9ab87f47b2bb8484565bcb17e687f7e",
6389
+      "version": "github:jitsi/lib-jitsi-meet#515374c8d383cb17df8ed76427e6f0fb5ea6ff1e",
6390 6390
       "requires": {
6391 6391
         "async": "0.9.0",
6392 6392
         "current-executing-script": "0.1.3",

+ 1
- 1
package.json 查看文件

@@ -44,7 +44,7 @@
44 44
     "jquery-i18next": "1.2.0",
45 45
     "js-md5": "0.6.1",
46 46
     "jwt-decode": "2.2.0",
47
-    "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#c7d6d158b9ab87f47b2bb8484565bcb17e687f7e",
47
+    "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#515374c8d383cb17df8ed76427e6f0fb5ea6ff1e",
48 48
     "lodash": "4.17.4",
49 49
     "moment": "2.19.4",
50 50
     "nuclear-js": "1.4.0",

+ 396
- 598
react/features/analytics/AnalyticsEvents.js
文件差異過大導致無法顯示
查看文件


+ 12
- 13
react/features/analytics/functions.js 查看文件

@@ -9,12 +9,14 @@ import { getJitsiMeetGlobalNS, loadScript } from '../base/util';
9 9
 const logger = require('jitsi-meet-logger').getLogger(__filename);
10 10
 
11 11
 /**
12
- * Sends an analytics event.
12
+ * Sends an event through the lib-jitsi-meet AnalyticsAdapter interface.
13 13
  *
14
- * @inheritdoc
14
+ * @param {Object} event - The event to send. It should be formatted as
15
+ * described in AnalyticsAdapter.js in lib-jitsi-meet.
16
+ * @returns {void}
15 17
  */
16
-export function sendAnalyticsEvent(...args: Array<any>) {
17
-    analytics.sendEvent(...args);
18
+export function sendAnalytics(event: Object) {
19
+    analytics.sendEvent(event);
18 20
 }
19 21
 
20 22
 /**
@@ -38,23 +40,17 @@ export function initAnalytics({ getState }: { getState: Function }) {
38 40
     const state = getState();
39 41
     const config = state['features/base/config'];
40 42
     const { analyticsScriptUrls } = config;
41
-    const machineId = JitsiMeetJS.getMachineId();
42 43
     const { user } = state['features/base/jwt'];
43 44
     const handlerConstructorOptions = {
44
-        product: 'lib-jitsi-meet',
45 45
         version: JitsiMeetJS.version,
46
-        session: machineId,
47
-        user: user ? user.id : `uid-${machineId}`,
48
-        server: state['features/base/connection'].locationURL.host
46
+        user
49 47
     };
50 48
 
51 49
     _loadHandlers(analyticsScriptUrls, handlerConstructorOptions)
52 50
         .then(handlers => {
53
-            const permanentProperties: Object = {
54
-                roomName: state['features/base/conference'].room,
55
-                userAgent: navigator.userAgent
56
-            };
51
+            const roomName = state['features/base/conference'].room;
57 52
             const { group, server } = state['features/base/jwt'];
53
+            const permanentProperties = {};
58 54
 
59 55
             if (server) {
60 56
                 permanentProperties.server = server;
@@ -76,6 +72,9 @@ export function initAnalytics({ getState }: { getState: Function }) {
76 72
             }
77 73
 
78 74
             analytics.addPermanentProperties(permanentProperties);
75
+            analytics.setConferenceName(roomName);
76
+
77
+            // Set the handlers last, since this triggers emptying of the cache
79 78
             analytics.setAnalyticsHandlers(handlers);
80 79
         },
81 80
         error => analytics.dispose() && logger.error(error));

+ 4
- 9
react/features/base/conference/actions.js 查看文件

@@ -3,9 +3,8 @@
3 3
 import UIEvents from '../../../../service/UI/UIEvents';
4 4
 
5 5
 import {
6
-    START_MUTED_SERVER_AUDIO_,
7
-    START_MUTED_SERVER_VIDEO_,
8
-    sendAnalyticsEvent
6
+    createStartMutedConfigurationEvent,
7
+    sendAnalytics
9 8
 } from '../../analytics';
10 9
 import { getName } from '../../app';
11 10
 import { JitsiConferenceEvents } from '../lib-jitsi-meet';
@@ -90,12 +89,8 @@ function _addConferenceListeners(conference, dispatch) {
90 89
             const audioMuted = Boolean(conference.startAudioMuted);
91 90
             const videoMuted = Boolean(conference.startVideoMuted);
92 91
 
93
-            sendAnalyticsEvent(
94
-                `${START_MUTED_SERVER_AUDIO_}.${
95
-                    audioMuted ? 'muted' : 'unmuted'}`);
96
-            sendAnalyticsEvent(
97
-                `${START_MUTED_SERVER_VIDEO_}.${
98
-                    videoMuted ? 'muted' : 'unmuted'}`);
92
+            sendAnalytics(createStartMutedConfigurationEvent(
93
+                'remote', audioMuted, videoMuted));
99 94
             logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${
100 95
                 videoMuted ? 'video' : ''}`);
101 96
 

+ 18
- 19
react/features/base/conference/middleware.js 查看文件

@@ -3,12 +3,11 @@
3 3
 import UIEvents from '../../../../service/UI/UIEvents';
4 4
 
5 5
 import {
6
-    _LOCAL,
7
-    _REMOTE,
8
-    AUDIO_ONLY_DISABLED,
9
-    PINNED_,
10
-    UNPINNED_,
11
-    sendAnalyticsEvent
6
+    ACTION_PINNED,
7
+    ACTION_UNPINNED,
8
+    createAudioOnlyDisableEvent,
9
+    createPinnedEvent,
10
+    sendAnalytics
12 11
 } from '../../analytics';
13 12
 import { CONNECTION_ESTABLISHED } from '../connection';
14 13
 import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../media';
@@ -131,7 +130,7 @@ function _conferenceFailedOrLeft({ dispatch, getState }, next, action) {
131 130
     const result = next(action);
132 131
 
133 132
     if (getState()['features/base/conference'].audioOnly) {
134
-        sendAnalyticsEvent(AUDIO_ONLY_DISABLED);
133
+        sendAnalytics(createAudioOnlyDisableEvent());
135 134
         logger.log('Audio only disabled');
136 135
         dispatch(setAudioOnly(false));
137 136
     }
@@ -193,19 +192,19 @@ function _pinParticipant(store, next, action) {
193 192
 
194 193
     if (typeof APP !== 'undefined') {
195 194
         const pinnedParticipant = getPinnedParticipant(participants);
196
-        const actionName = action.participant.id ? PINNED_ : UNPINNED_;
197
-        let videoType;
195
+        const actionName
196
+            = action.participant.id ? ACTION_PINNED : ACTION_UNPINNED;
197
+        const local = (participantById && participantById.local)
198
+                || (!id && pinnedParticipant && pinnedParticipant.local);
199
+
200
+        sendAnalytics(createPinnedEvent(
201
+            actionName,
202
+            local ? 'local' : id,
203
+            {
204
+                'participant_count': conference.getParticipantCount(),
205
+                local
206
+            }));
198 207
 
199
-        if ((participantById && participantById.local)
200
-                || (!id && pinnedParticipant && pinnedParticipant.local)) {
201
-            videoType = _LOCAL;
202
-        } else {
203
-            videoType = _REMOTE;
204
-        }
205
-
206
-        sendAnalyticsEvent(
207
-                `${actionName}.${videoType}`,
208
-                { value: conference.getParticipantCount() });
209 208
     }
210 209
 
211 210
     // The following condition prevents signaling to pin local participant and

+ 8
- 16
react/features/base/media/middleware.js 查看文件

@@ -1,11 +1,10 @@
1 1
 /* @flow */
2 2
 
3 3
 import {
4
-    START_AUDIO_ONLY_,
5
-    START_MUTED_CLIENT_AUDIO_,
6
-    START_MUTED_CLIENT_VIDEO_,
7
-    SYNC_TRACK_STATE_,
8
-    sendAnalyticsEvent
4
+    createStartAudioOnlyEvent,
5
+    createStartMutedConfigurationEvent,
6
+    createSyncTrackStateEvent,
7
+    sendAnalytics
9 8
 } from '../../analytics';
10 9
 import { SET_ROOM, setAudioOnly } from '../conference';
11 10
 import { parseURLParams } from '../config';
@@ -90,12 +89,8 @@ function _setRoom({ dispatch, getState }, next, action) {
90 89
     audioMuted = Boolean(audioMuted);
91 90
     videoMuted = Boolean(videoMuted);
92 91
 
93
-    // Apply the config.
94
-
95
-    sendAnalyticsEvent(
96
-        `${START_MUTED_CLIENT_AUDIO_}.${audioMuted ? 'muted' : 'unmuted'}`);
97
-    sendAnalyticsEvent(
98
-        `${START_MUTED_CLIENT_VIDEO_}.${videoMuted ? 'muted' : 'unmuted'}`);
92
+    sendAnalytics(createStartMutedConfigurationEvent(
93
+        'local', audioMuted, videoMuted));
99 94
 
100 95
     logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${
101 96
         videoMuted ? 'video' : ''}`);
@@ -128,8 +123,7 @@ function _setRoom({ dispatch, getState }, next, action) {
128 123
             audioOnly = true;
129 124
         }
130 125
 
131
-        sendAnalyticsEvent(
132
-            `${START_AUDIO_ONLY_}.${audioOnly ? 'enabled' : 'disabled'}`);
126
+        sendAnalytics(createStartAudioOnlyEvent(audioOnly));
133 127
         logger.log(`Start audio only set to ${audioOnly.toString()}`);
134 128
         dispatch(setAudioOnly(audioOnly));
135 129
     }
@@ -155,9 +149,7 @@ function _syncTrackMutedState({ getState }, track) {
155 149
     // not yet in redux state and JitsiTrackEvents.TRACK_MUTE_CHANGED may be
156 150
     // fired before track gets to state.
157 151
     if (track.muted !== muted) {
158
-        sendAnalyticsEvent(
159
-            `${SYNC_TRACK_STATE_}.${track.mediaType}.${
160
-                muted ? 'muted' : 'unmuted'}`);
152
+        sendAnalytics(createSyncTrackStateEvent(track.mediaType, muted));
161 153
         logger.log(`Sync ${track.mediaType} track muted state to ${
162 154
             muted ? 'muted' : 'unmuted'}`);
163 155
         track.muted = muted;

+ 6
- 5
react/features/base/tracks/actions.js 查看文件

@@ -1,6 +1,6 @@
1 1
 import {
2
-    REPLACE_TRACK_,
3
-    sendAnalyticsEvent
2
+    createTrackMutedEvent,
3
+    sendAnalytics
4 4
 } from '../../analytics';
5 5
 import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
6 6
 import {
@@ -220,9 +220,10 @@ export function replaceLocalTrack(oldTrack, newTrack, conference) {
220 220
                                     : setAudioMuted;
221 221
                             const isMuted = newTrack.isMuted();
222 222
 
223
-                            sendAnalyticsEvent(`${REPLACE_TRACK_}.${
224
-                                newTrack.getType()}.${
225
-                                isMuted ? 'muted' : 'unmuted'}`);
223
+                            sendAnalytics(createTrackMutedEvent(
224
+                                newTrack.getType(),
225
+                                'track.replaced',
226
+                                isMuted));
226 227
                             logger.log(`Replace ${newTrack.getType()} track - ${
227 228
                                 isMuted ? 'muted' : 'unmuted'}`);
228 229
 

+ 3
- 3
react/features/feedback/components/FeedbackDialog.web.js 查看文件

@@ -7,8 +7,8 @@ import React, { Component } from 'react';
7 7
 import { connect } from 'react-redux';
8 8
 
9 9
 import {
10
-    FEEDBACK_OPEN,
11
-    sendAnalyticsEvent
10
+    createFeedbackOpenEvent,
11
+    sendAnalytics
12 12
 } from '../../analytics';
13 13
 import { Dialog } from '../../base/dialog';
14 14
 import { translate } from '../../base/i18n';
@@ -148,7 +148,7 @@ class FeedbackDialog extends Component {
148 148
      * @inheritdoc
149 149
      */
150 150
     componentDidMount() {
151
-        sendAnalyticsEvent(FEEDBACK_OPEN);
151
+        sendAnalytics(createFeedbackOpenEvent());
152 152
     }
153 153
 
154 154
     /**

+ 1
- 1
react/features/filmstrip/middleware.js 查看文件

@@ -26,7 +26,7 @@ MiddlewareRegistry.register(({ getState }) => next => action => {
26 26
                 // not need the middleware implemented here, Filmstrip.init, and
27 27
                 // UI.start.
28 28
                 || (Filmstrip.filmstrip
29
-                    && Filmstrip.toggleFilmstrip(!newValue, false));
29
+                    && Filmstrip.toggleFilmstrip(!newValue));
30 30
 
31 31
             return result;
32 32
         }

+ 3
- 3
react/features/invite/components/InviteDialog.web.js 查看文件

@@ -3,8 +3,8 @@ import React, { Component } from 'react';
3 3
 import { connect } from 'react-redux';
4 4
 
5 5
 import {
6
-    TOOLBAR_INVITE_CLOSE,
7
-    sendAnalyticsEvent
6
+    createInviteDialogClosedEvent,
7
+    sendAnalytics
8 8
 } from '../../analytics';
9 9
 import { getInviteURL } from '../../base/connection';
10 10
 import { Dialog } from '../../base/dialog';
@@ -54,7 +54,7 @@ class InviteDialog extends Component {
54 54
      * @inheritdoc
55 55
      */
56 56
     componentWillUnmount() {
57
-        sendAnalyticsEvent(TOOLBAR_INVITE_CLOSE);
57
+        sendAnalytics(createInviteDialogClosedEvent());
58 58
     }
59 59
 
60 60
     /**

+ 5
- 3
react/features/mobile/background/actions.js 查看文件

@@ -1,8 +1,8 @@
1 1
 /* @flow */
2 2
 
3 3
 import {
4
-    CALLKIT_BACKGROUND_VIDEO_MUTED,
5
-    sendAnalyticsEvent
4
+    createTrackMutedEvent,
5
+    sendAnalytics
6 6
 } from '../../analytics';
7 7
 import { setLastN } from '../../base/conference';
8 8
 import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../../base/media';
@@ -46,7 +46,9 @@ export function _setBackgroundVideoMuted(muted: boolean) {
46 46
 
47 47
         audioOnly || dispatch(setLastN(muted ? 0 : undefined));
48 48
 
49
-        sendAnalyticsEvent(CALLKIT_BACKGROUND_VIDEO_MUTED);
49
+        sendAnalytics(createTrackMutedEvent(
50
+            'video',
51
+            'callkit.background.video'));
50 52
 
51 53
         dispatch(setVideoMuted(muted, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
52 54
     };

+ 3
- 4
react/features/mobile/callkit/middleware.js 查看文件

@@ -4,8 +4,8 @@ import { NativeModules } from 'react-native';
4 4
 import uuid from 'uuid';
5 5
 
6 6
 import {
7
-    CALLKIT_AUDIO_,
8
-    sendAnalyticsEvent
7
+    createTrackMutedEvent,
8
+    sendAnalytics
9 9
 } from '../../analytics';
10 10
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT, appNavigate } from '../../app';
11 11
 import {
@@ -279,8 +279,7 @@ function _onPerformSetMutedCallAction({ callUUID, muted: newValue }) {
279 279
         if (oldValue !== newValue) {
280 280
             const value = Boolean(newValue);
281 281
 
282
-            sendAnalyticsEvent(`${CALLKIT_AUDIO_}.${
283
-                value ? 'muted' : 'unmuted'}`);
282
+            sendAnalytics(createTrackMutedEvent('audio', 'callkit', value));
284 283
             dispatch(setAudioMuted(value));
285 284
         }
286 285
     }

+ 14
- 5
react/features/overlay/components/AbstractPageReloadOverlay.js 查看文件

@@ -3,7 +3,10 @@
3 3
 import PropTypes from 'prop-types';
4 4
 import React, { Component } from 'react';
5 5
 
6
-import { PAGE_RELOAD } from '../../analytics';
6
+import {
7
+    createPageReloadScheduledEvent,
8
+    sendAnalytics
9
+} from '../../analytics';
7 10
 import {
8 11
     isFatalJitsiConferenceError,
9 12
     isFatalJitsiConnectionError
@@ -159,12 +162,18 @@ export default class AbstractPageReloadOverlay extends Component<*, *> {
159 162
         // sent to the backed.
160 163
         // FIXME: We should dispatch action for this.
161 164
         if (typeof APP !== 'undefined') {
162
-            APP.conference.logEvent(
163
-                PAGE_RELOAD,
164
-                /* value */ undefined,
165
-                /* label */ this.props.reason);
165
+            if (APP.conference && APP.conference.room) {
166
+                APP.conference.room.sendApplicationLog(JSON.stringify(
167
+                    {
168
+                        name: 'page.reload',
169
+                        label: this.props.reason
170
+                    }));
171
+            }
166 172
         }
167 173
 
174
+        sendAnalytics(createPageReloadScheduledEvent(
175
+            this.props.reason, this.state.timeoutSeconds));
176
+
168 177
         logger.info(
169 178
             `The conference will be reloaded after ${
170 179
                 this.state.timeoutSeconds} seconds.`);

+ 7
- 8
react/features/remote-video-menu/components/KickButton.js 查看文件

@@ -3,8 +3,8 @@ import React, { Component } from 'react';
3 3
 import { connect } from 'react-redux';
4 4
 
5 5
 import {
6
-    REMOTE_VIDEO_MENU_KICK,
7
-    sendAnalyticsEvent
6
+    createRemoteVideoMenuButtonEvent,
7
+    sendAnalytics
8 8
 } from '../../analytics';
9 9
 import { translate } from '../../base/i18n';
10 10
 import { kickParticipant } from '../../base/participants';
@@ -86,13 +86,12 @@ class KickButton extends Component {
86 86
     _onClick() {
87 87
         const { dispatch, onClick, participantID } = this.props;
88 88
 
89
-        sendAnalyticsEvent(
90
-            REMOTE_VIDEO_MENU_KICK,
89
+        sendAnalytics(createRemoteVideoMenuButtonEvent(
90
+            'kick.button',
91 91
             {
92
-                value: 1,
93
-                label: participantID
94
-            }
95
-        );
92
+                'participant_id': participantID
93
+            }));
94
+
96 95
         dispatch(kickParticipant(participantID));
97 96
 
98 97
         if (onClick) {

+ 6
- 8
react/features/remote-video-menu/components/MuteButton.js 查看文件

@@ -3,8 +3,8 @@ import React, { Component } from 'react';
3 3
 import { connect } from 'react-redux';
4 4
 
5 5
 import {
6
-    REMOTE_VIDEO_MENU_MUTE_CLICKED,
7
-    sendAnalyticsEvent
6
+    createRemoteVideoMenuButtonEvent,
7
+    sendAnalytics
8 8
 } from '../../analytics';
9 9
 import { translate } from '../../base/i18n';
10 10
 import { openDialog } from '../../base/dialog';
@@ -101,13 +101,11 @@ class MuteButton extends Component {
101 101
     _onClick() {
102 102
         const { dispatch, onClick, participantID } = this.props;
103 103
 
104
-        sendAnalyticsEvent(
105
-            REMOTE_VIDEO_MENU_MUTE_CLICKED,
104
+        sendAnalytics(createRemoteVideoMenuButtonEvent(
105
+            'mute.button',
106 106
             {
107
-                value: 1,
108
-                label: participantID
109
-            }
110
-        );
107
+                'participant_id': participantID
108
+            }));
111 109
 
112 110
         dispatch(openDialog(MuteRemoteParticipantDialog, { participantID }));
113 111
 

+ 4
- 10
react/features/remote-video-menu/components/MuteRemoteParticipantDialog.web.js 查看文件

@@ -6,8 +6,8 @@ import { Dialog } from '../../base/dialog';
6 6
 import { translate } from '../../base/i18n';
7 7
 
8 8
 import {
9
-    REMOTE_VIDEO_MENU_MUTE_CONFIRMED,
10
-    sendAnalyticsEvent
9
+    createRemoteMuteConfirmedEvent,
10
+    sendAnalytics
11 11
 } from '../../analytics';
12 12
 import { muteRemoteParticipant } from '../../base/participants';
13 13
 
@@ -77,18 +77,12 @@ class MuteRemoteParticipantDialog extends Component {
77 77
      * Handles the submit button action.
78 78
      *
79 79
      * @private
80
-     * @returns {void}
80
+     * @returns {boolean} - True (to note that the modal should be closed).
81 81
      */
82 82
     _onSubmit() {
83 83
         const { dispatch, participantID } = this.props;
84 84
 
85
-        sendAnalyticsEvent(
86
-            REMOTE_VIDEO_MENU_MUTE_CONFIRMED,
87
-            {
88
-                value: 1,
89
-                label: participantID
90
-            }
91
-        );
85
+        sendAnalytics(createRemoteMuteConfirmedEvent(participantID));
92 86
 
93 87
         dispatch(muteRemoteParticipant(participantID));
94 88
 

+ 12
- 17
react/features/remote-video-menu/components/RemoteControlButton.js 查看文件

@@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
2 2
 import React, { Component } from 'react';
3 3
 
4 4
 import {
5
-    REMOTE_VIDEO_MENU_REMOTE_CONTROL_,
6
-    sendAnalyticsEvent
5
+    createRemoteVideoMenuButtonEvent,
6
+    sendAnalytics
7 7
 } from '../../analytics';
8 8
 import { translate } from '../../base/i18n';
9 9
 
@@ -122,24 +122,19 @@ class RemoteControlButton extends Component {
122 122
     _onClick() {
123 123
         const { onClick, participantID, remoteControlState } = this.props;
124 124
 
125
-        let eventName;
125
+        // TODO: What do we do in case the state is e.g. "requesting"?
126
+        if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED
127
+            || remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
126 128
 
127
-        if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
128
-            eventName = 'stop';
129
-        }
130
-
131
-        if (remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
132
-            eventName = 'start';
133
-        }
129
+            const enable
130
+                = remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
134 131
 
135
-        if (eventName) {
136
-            sendAnalyticsEvent(
137
-                `${REMOTE_VIDEO_MENU_REMOTE_CONTROL_}.${eventName}`,
132
+            sendAnalytics(createRemoteVideoMenuButtonEvent(
133
+                'remote.control.button',
138 134
                 {
139
-                    value: 1,
140
-                    label: participantID
141
-                }
142
-            );
135
+                    enable,
136
+                    'participant_id': participantID
137
+                }));
143 138
         }
144 139
 
145 140
         if (onClick) {

+ 7
- 2
react/features/toolbox/components/ProfileButton.web.js 查看文件

@@ -4,7 +4,10 @@ import PropTypes from 'prop-types';
4 4
 import React, { Component } from 'react';
5 5
 import { connect } from 'react-redux';
6 6
 
7
-import { TOOLBAR_PROFILE_TOGGLED, sendAnalyticsEvent } from '../../analytics';
7
+import {
8
+    createToolbarEvent,
9
+    sendAnalytics
10
+} from '../../analytics';
8 11
 import {
9 12
     getAvatarURL,
10 13
     getLocalParticipant
@@ -115,7 +118,9 @@ class ProfileButton extends Component<*> {
115 118
      */
116 119
     _onClick() {
117 120
         if (!this.props._unclickable) {
118
-            sendAnalyticsEvent(TOOLBAR_PROFILE_TOGGLED);
121
+            // TODO: Include an 'enable' attribute, which specifies whether
122
+            // the profile panel was opened or closed.
123
+            sendAnalytics(createToolbarEvent('profile'));
119 124
             APP.UI.emitEvent(UIEvents.TOGGLE_PROFILE);
120 125
         }
121 126
     }

+ 14
- 6
react/features/toolbox/components/Toolbox.native.js 查看文件

@@ -4,10 +4,10 @@ import { View } from 'react-native';
4 4
 import { connect } from 'react-redux';
5 5
 
6 6
 import {
7
-    TOOLBAR_AUDIO_MUTED,
8
-    TOOLBAR_AUDIO_UNMUTED,
9
-    TOOLBAR_VIDEO_,
10
-    sendAnalyticsEvent
7
+    AUDIO_MUTE,
8
+    VIDEO_MUTE,
9
+    createToolbarEvent,
10
+    sendAnalytics
11 11
 } from '../../analytics';
12 12
 import {
13 13
     isNarrowAspectRatio,
@@ -188,7 +188,11 @@ class Toolbox extends Component {
188 188
     _onToggleAudio() {
189 189
         const mute = !this.props._audioMuted;
190 190
 
191
-        sendAnalyticsEvent(mute ? TOOLBAR_AUDIO_MUTED : TOOLBAR_AUDIO_UNMUTED);
191
+        sendAnalytics(createToolbarEvent(
192
+            AUDIO_MUTE,
193
+            {
194
+                enable: mute
195
+            }));
192 196
 
193 197
         // The user sees the reality i.e. the state of base/tracks and intends
194 198
         // to change reality by tapping on the respective button i.e. the user
@@ -211,7 +215,11 @@ class Toolbox extends Component {
211 215
     _onToggleVideo() {
212 216
         const mute = !this.props._videoMuted;
213 217
 
214
-        sendAnalyticsEvent(`${TOOLBAR_VIDEO_}.${mute ? 'muted' : 'unmuted'}`);
218
+        sendAnalytics(createToolbarEvent(
219
+            VIDEO_MUTE,
220
+            {
221
+                enable: mute
222
+            }));
215 223
 
216 224
         // The user sees the reality i.e. the state of base/tracks and intends
217 225
         // to change reality by tapping on the respective button i.e. the user

+ 134
- 53
react/features/toolbox/defaultToolbarButtons.web.js 查看文件

@@ -3,29 +3,12 @@
3 3
 import React from 'react';
4 4
 
5 5
 import {
6
-    SHORTCUT_AUDIO_MUTE_TOGGLED,
7
-    SHORTCUT_CHAT_TOGGLED,
8
-    SHORTCUT_RAISE_HAND_CLICKED,
9
-    SHORTCUT_SCREEN_TOGGLED,
10
-    SHORTCUT_VIDEO_MUTE_TOGGLED,
11
-    TOOLBAR_AUDIO_MUTED,
12
-    TOOLBAR_AUDIO_UNMUTED,
13
-    TOOLBAR_CHAT_TOGGLED,
14
-    TOOLBAR_CONTACTS_TOGGLED,
15
-    TOOLBAR_ETHERPACK_CLICKED,
16
-    TOOLBAR_FILMSTRIP_ONLY_DEVICE_SELECTION_TOGGLED,
17
-    TOOLBAR_FULLSCREEN_ENABLED,
18
-    TOOLBAR_HANGUP,
19
-    TOOLBAR_INVITE_CLICKED,
20
-    TOOLBAR_RAISE_HAND_CLICKED,
21
-    TOOLBAR_SCREEN_DISABLED,
22
-    TOOLBAR_SCREEN_ENABLED,
23
-    TOOLBAR_SETTINGS_TOGGLED,
24
-    TOOLBAR_SHARED_VIDEO_CLICKED,
25
-    TOOLBAR_SIP_DIALPAD_CLICKED,
26
-    TOOLBAR_VIDEO_DISABLED,
27
-    TOOLBAR_VIDEO_ENABLED,
28
-    sendAnalyticsEvent
6
+    ACTION_SHORTCUT_TRIGGERED as TRIGGERED,
7
+    AUDIO_MUTE,
8
+    VIDEO_MUTE,
9
+    createShortcutEvent,
10
+    createToolbarEvent,
11
+    sendAnalytics
29 12
 } from '../analytics';
30 13
 import { ParticipantCounter } from '../contact-list';
31 14
 import { openDeviceSelectionDialog } from '../device-selection';
@@ -63,13 +46,18 @@ export default function getDefaultButtons() {
63 46
             isDisplayed: () => true,
64 47
             id: 'toolbar_button_camera',
65 48
             onClick() {
49
+                // TODO: Why is this different from the code which handles
50
+                // a keyboard shortcut?
66 51
                 const newVideoMutedState = !APP.conference.isLocalVideoMuted();
67 52
 
68
-                if (newVideoMutedState) {
69
-                    sendAnalyticsEvent(TOOLBAR_VIDEO_ENABLED);
70
-                } else {
71
-                    sendAnalyticsEvent(TOOLBAR_VIDEO_DISABLED);
72
-                }
53
+                // The 'enable' attribute in the event is set to true if the
54
+                // button click triggered a mute action, and set to false if it
55
+                // triggered an unmute action.
56
+                sendAnalytics(createToolbarEvent(
57
+                    VIDEO_MUTE,
58
+                    {
59
+                        enable: newVideoMutedState
60
+                    }));
73 61
                 APP.UI.emitEvent(UIEvents.VIDEO_MUTED, newVideoMutedState);
74 62
             },
75 63
             popups: [
@@ -88,7 +76,13 @@ export default function getDefaultButtons() {
88 76
                     return;
89 77
                 }
90 78
 
91
-                sendAnalyticsEvent(SHORTCUT_VIDEO_MUTE_TOGGLED);
79
+                // The 'enable' attribute in the event is set to true if the
80
+                // shortcut triggered a mute action, and set to false if it
81
+                // triggered an unmute action.
82
+                sendAnalytics(createShortcutEvent(
83
+                    VIDEO_MUTE,
84
+                    TRIGGERED,
85
+                    { enable: !APP.conference.isLocalVideoMuted() }));
92 86
                 APP.conference.toggleVideoMuted();
93 87
             },
94 88
             shortcutDescription: 'keyboardShortcuts.videoMute',
@@ -105,13 +99,26 @@ export default function getDefaultButtons() {
105 99
                 <span id = 'unreadMessages' /></span>,
106 100
             id: 'toolbar_button_chat',
107 101
             onClick() {
108
-                sendAnalyticsEvent(TOOLBAR_CHAT_TOGGLED);
102
+                // The 'enable' attribute is set to true if the click resulted
103
+                // in the chat panel being shown, and to false if it was hidden.
104
+                sendAnalytics(createToolbarEvent(
105
+                    'toggle.chat',
106
+                    {
107
+                        enable: !APP.UI.Chat.isVisible()
108
+                    }));
109 109
                 APP.UI.emitEvent(UIEvents.TOGGLE_CHAT);
110 110
             },
111 111
             shortcut: 'C',
112 112
             shortcutAttr: 'toggleChatPopover',
113 113
             shortcutFunc() {
114
-                sendAnalyticsEvent(SHORTCUT_CHAT_TOGGLED);
114
+                // The 'enable' attribute is set to true if the shortcut
115
+                // resulted in the chat panel being shown, and to false if it
116
+                // was hidden.
117
+                sendAnalytics(createShortcutEvent(
118
+                    'toggle.chat',
119
+                    {
120
+                        enable: !APP.UI.Chat.isVisible()
121
+                    }));
115 122
                 APP.UI.toggleChat();
116 123
             },
117 124
             shortcutDescription: 'keyboardShortcuts.toggleChat',
@@ -128,7 +135,9 @@ export default function getDefaultButtons() {
128 135
             enabled: true,
129 136
             id: 'toolbar_contact_list',
130 137
             onClick() {
131
-                sendAnalyticsEvent(TOOLBAR_CONTACTS_TOGGLED);
138
+                // TODO: Include an 'enable' attribute which specifies whether
139
+                // the contacts panel was shown or hidden.
140
+                sendAnalytics(createToolbarEvent('contacts'));
132 141
                 APP.UI.emitEvent(UIEvents.TOGGLE_CONTACT_LIST);
133 142
             },
134 143
             sideContainerId: 'contacts_container',
@@ -143,11 +152,14 @@ export default function getDefaultButtons() {
143 152
             enabled: true,
144 153
             id: 'toolbar_button_desktopsharing',
145 154
             onClick() {
146
-                if (APP.conference.isSharingScreen) {
147
-                    sendAnalyticsEvent(TOOLBAR_SCREEN_DISABLED);
148
-                } else {
149
-                    sendAnalyticsEvent(TOOLBAR_SCREEN_ENABLED);
150
-                }
155
+                // TODO: Why is the button clicked handled differently that
156
+                // a keyboard shortcut press (firing a TOGGLE_SCREENSHARING
157
+                // event vs. directly calling toggleScreenSharing())?
158
+                sendAnalytics(createToolbarEvent(
159
+                    'screen.sharing',
160
+                    {
161
+                        enable: !APP.conference.isSharingScreen
162
+                    }));
151 163
                 APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
152 164
             },
153 165
             popups: [
@@ -160,7 +172,13 @@ export default function getDefaultButtons() {
160 172
             shortcut: 'D',
161 173
             shortcutAttr: 'toggleDesktopSharingPopover',
162 174
             shortcutFunc() {
163
-                sendAnalyticsEvent(SHORTCUT_SCREEN_TOGGLED);
175
+                // The 'enable' attribute is set to true if pressing the
176
+                // shortcut resulted in screen sharing being enabled, and false
177
+                // if it resulted in screen sharing being disabled.
178
+                sendAnalytics(createShortcutEvent(
179
+                    'toggle.screen.sharing',
180
+                    TRIGGERED,
181
+                    { enable: !APP.conference.isSharingScreen }));
164 182
 
165 183
                 // eslint-disable-next-line no-empty-function
166 184
                 APP.conference.toggleScreenSharing().catch(() => {});
@@ -180,8 +198,8 @@ export default function getDefaultButtons() {
180 198
             },
181 199
             id: 'toolbar_button_fodeviceselection',
182 200
             onClick(dispatch: Function) {
183
-                sendAnalyticsEvent(
184
-                    TOOLBAR_FILMSTRIP_ONLY_DEVICE_SELECTION_TOGGLED);
201
+                sendAnalytics(
202
+                    createToolbarEvent('filmstrip.only.device.selection'));
185 203
 
186 204
                 dispatch(openDeviceSelectionDialog());
187 205
             },
@@ -200,7 +218,7 @@ export default function getDefaultButtons() {
200 218
             hidden: true,
201 219
             id: 'toolbar_button_dialpad',
202 220
             onClick() {
203
-                sendAnalyticsEvent(TOOLBAR_SIP_DIALPAD_CLICKED);
221
+                sendAnalytics(createToolbarEvent('dialpad'));
204 222
             },
205 223
             tooltipKey: 'toolbar.dialpad'
206 224
         },
@@ -214,7 +232,13 @@ export default function getDefaultButtons() {
214 232
             hidden: true,
215 233
             id: 'toolbar_button_etherpad',
216 234
             onClick() {
217
-                sendAnalyticsEvent(TOOLBAR_ETHERPACK_CLICKED);
235
+                // The 'enable' attribute is set to true if the click resulted
236
+                // in the etherpad panel being shown, or false it it was hidden.
237
+                sendAnalytics(createToolbarEvent(
238
+                    'toggle.etherpad',
239
+                    {
240
+                        enable: !APP.UI.isEtherpadVisible()
241
+                    }));
218 242
                 APP.UI.emitEvent(UIEvents.ETHERPAD_CLICKED);
219 243
             },
220 244
             tooltipKey: 'toolbar.etherpad'
@@ -228,7 +252,18 @@ export default function getDefaultButtons() {
228 252
             enabled: true,
229 253
             id: 'toolbar_button_fullScreen',
230 254
             onClick() {
231
-                sendAnalyticsEvent(TOOLBAR_FULLSCREEN_ENABLED);
255
+                // TODO: why is the fullscreen button handled differently than
256
+                // the fullscreen keyboard shortcut (one results in a direct
257
+                // call to toggleFullScreen, while the other fires an
258
+                // UIEvents.TOGGLE_FULLSCREEN event)?
259
+
260
+                // The 'enable' attribute is set to true if the action resulted
261
+                // in fullscreen mode being enabled.
262
+                sendAnalytics(createToolbarEvent(
263
+                    'toggle.fullscreen',
264
+                        {
265
+                            enable: !APP.UI.isFullScreen()
266
+                        }));
232 267
 
233 268
                 APP.UI.emitEvent(UIEvents.TOGGLE_FULLSCREEN);
234 269
             },
@@ -236,7 +271,13 @@ export default function getDefaultButtons() {
236 271
             shortcutAttr: 'toggleFullscreenPopover',
237 272
             shortcutDescription: 'keyboardShortcuts.fullScreen',
238 273
             shortcutFunc() {
239
-                sendAnalyticsEvent('shortcut.fullscreen.toggled');
274
+                // The 'enable' attribute is set to true if the action resulted
275
+                // in fullscreen mode being enabled.
276
+                sendAnalytics(createShortcutEvent(
277
+                    'toggle.fullscreen',
278
+                    {
279
+                        enable: !APP.UI.isFullScreen()
280
+                    }));
240 281
                 APP.UI.toggleFullScreen();
241 282
             },
242 283
             tooltipKey: 'toolbar.fullscreen'
@@ -252,7 +293,7 @@ export default function getDefaultButtons() {
252 293
             isDisplayed: () => true,
253 294
             id: 'toolbar_button_hangup',
254 295
             onClick() {
255
-                sendAnalyticsEvent(TOOLBAR_HANGUP);
296
+                sendAnalytics(createToolbarEvent('hangup'));
256 297
                 APP.UI.emitEvent(UIEvents.HANGUP);
257 298
             },
258 299
             tooltipKey: 'toolbar.hangup'
@@ -275,7 +316,7 @@ export default function getDefaultButtons() {
275 316
             enabled: true,
276 317
             id: 'toolbar_button_link',
277 318
             onClick(dispatch: Function) {
278
-                sendAnalyticsEvent(TOOLBAR_INVITE_CLICKED);
319
+                sendAnalytics(createToolbarEvent('invite'));
279 320
 
280 321
                 dispatch(openInviteDialog());
281 322
             },
@@ -293,6 +334,13 @@ export default function getDefaultButtons() {
293 334
             onClick() {
294 335
                 const sharedVideoManager = APP.UI.getSharedVideoManager();
295 336
 
337
+                // TODO: Clicking the mute button and pressing the mute shortcut
338
+                // could be handled in a uniform manner. The code below checks
339
+                // the mute status and fires the appropriate event (MUTED or
340
+                // UNMUTED), while the code which handles the keyboard shortcut
341
+                // calls toggleAudioMuted(). Also strangely the the user is
342
+                // only warned if they click the button (and not if they use
343
+                // the shortcut).
296 344
                 if (APP.conference.isLocalAudioMuted()) {
297 345
                     // If there's a shared video with the volume "on" and we
298 346
                     // aren't the video owner, we warn the user
@@ -303,11 +351,15 @@ export default function getDefaultButtons() {
303 351
                         APP.UI.showCustomToolbarPopup(
304 352
                             'microphone', 'unableToUnmutePopup', true, 5000);
305 353
                     } else {
306
-                        sendAnalyticsEvent(TOOLBAR_AUDIO_UNMUTED);
354
+                        sendAnalytics(createToolbarEvent(
355
+                            AUDIO_MUTE,
356
+                            { enable: false }));
307 357
                         APP.UI.emitEvent(UIEvents.AUDIO_MUTED, false, true);
308 358
                     }
309 359
                 } else {
310
-                    sendAnalyticsEvent(TOOLBAR_AUDIO_MUTED);
360
+                    sendAnalytics(createToolbarEvent(
361
+                        AUDIO_MUTE,
362
+                        { enable: true }));
311 363
                     APP.UI.emitEvent(UIEvents.AUDIO_MUTED, true, true);
312 364
                 }
313 365
             },
@@ -328,7 +380,13 @@ export default function getDefaultButtons() {
328 380
             shortcut: 'M',
329 381
             shortcutAttr: 'mutePopover',
330 382
             shortcutFunc() {
331
-                sendAnalyticsEvent(SHORTCUT_AUDIO_MUTE_TOGGLED);
383
+                // The 'enable' attribute in the event is set to true if the
384
+                // shortcut triggered a mute action, and set to false if it
385
+                // triggered an unmute action.
386
+                sendAnalytics(createShortcutEvent(
387
+                    AUDIO_MUTE,
388
+                    TRIGGERED,
389
+                    { enable: !APP.conference.isLocalAudioMuted() }));
332 390
                 APP.conference.toggleAudioMuted();
333 391
             },
334 392
             shortcutDescription: 'keyboardShortcuts.mute',
@@ -351,14 +409,27 @@ export default function getDefaultButtons() {
351 409
             enabled: true,
352 410
             id: 'toolbar_button_raisehand',
353 411
             onClick() {
354
-                sendAnalyticsEvent(TOOLBAR_RAISE_HAND_CLICKED);
412
+                // TODO: reduce duplication with shortcutFunc below.
413
+
414
+                // The 'enable' attribute is set to true if the pressing of the
415
+                // shortcut resulted in the hand being raised, and to false
416
+                // if it resulted in the hand being 'lowered'.
417
+                sendAnalytics(createToolbarEvent(
418
+                    'raise.hand',
419
+                    { enable: !APP.conference.isHandRaised }));
355 420
                 APP.conference.maybeToggleRaisedHand();
356 421
             },
357 422
             shortcut: 'R',
358 423
             shortcutAttr: 'raiseHandPopover',
359 424
             shortcutDescription: 'keyboardShortcuts.raiseHand',
360 425
             shortcutFunc() {
361
-                sendAnalyticsEvent(SHORTCUT_RAISE_HAND_CLICKED);
426
+                // The 'enable' attribute is set to true if the pressing of the
427
+                // shortcut resulted in the hand being raised, and to false
428
+                // if it resulted in the hand being 'lowered'.
429
+                sendAnalytics(createShortcutEvent(
430
+                    'toggle.raise.hand',
431
+                    TRIGGERED,
432
+                    { enable: !APP.conference.isHandRaised }));
362 433
                 APP.conference.maybeToggleRaisedHand();
363 434
             },
364 435
             tooltipKey: 'toolbar.raiseHand'
@@ -386,7 +457,9 @@ export default function getDefaultButtons() {
386 457
             enabled: true,
387 458
             id: 'toolbar_button_settings',
388 459
             onClick() {
389
-                sendAnalyticsEvent(TOOLBAR_SETTINGS_TOGGLED);
460
+                // TODO: Include an 'enable' attribute which specifies whether
461
+                // the settings panel was shown or hidden.
462
+                sendAnalytics(createToolbarEvent('settings'));
390 463
                 APP.UI.emitEvent(UIEvents.TOGGLE_SETTINGS);
391 464
             },
392 465
             sideContainerId: 'settings_container',
@@ -401,7 +474,15 @@ export default function getDefaultButtons() {
401 474
             enabled: true,
402 475
             id: 'toolbar_button_sharedvideo',
403 476
             onClick() {
404
-                sendAnalyticsEvent(TOOLBAR_SHARED_VIDEO_CLICKED);
477
+                // The 'enable' attribute is set to true if the click resulted
478
+                // in the "start sharing video" dialog being shown, and false
479
+                // if it resulted in the "stop sharing video" dialog being
480
+                // shown.
481
+                sendAnalytics(createToolbarEvent(
482
+                    'shared.video.toggled',
483
+                    {
484
+                        enable: !APP.UI.isSharedVideoShown()
485
+                    }));
405 486
                 APP.UI.emitEvent(UIEvents.SHARED_VIDEO_CLICKED);
406 487
             },
407 488
             popups: [

+ 25
- 11
react/features/video-quality/components/VideoQualityDialog.web.js 查看文件

@@ -4,16 +4,13 @@ import React, { Component } from 'react';
4 4
 import { connect } from 'react-redux';
5 5
 
6 6
 import {
7
-    TOOLBAR_AUDIO_ONLY_ENABLED,
8
-    TOOLBAR_VIDEO_QUALITY_HIGH,
9
-    TOOLBAR_VIDEO_QUALITY_LOW,
10
-    TOOLBAR_VIDEO_QUALITY_STANDARD,
11
-    sendAnalyticsEvent
7
+    createToolbarEvent,
8
+    sendAnalytics
12 9
 } from '../../analytics';
13 10
 import {
11
+    VIDEO_QUALITY_LEVELS,
14 12
     setAudioOnly,
15
-    setReceiveVideoQuality,
16
-    VIDEO_QUALITY_LEVELS
13
+    setReceiveVideoQuality
17 14
 } from '../../base/conference';
18 15
 import { translate } from '../../base/i18n';
19 16
 import JitsiMeetJS from '../../base/lib-jitsi-meet';
@@ -26,6 +23,22 @@ const {
26 23
     LOW
27 24
 } = VIDEO_QUALITY_LEVELS;
28 25
 
26
+/**
27
+ * Creates an analytics event for a press of one of the buttons in the video
28
+ * quality dialog.
29
+ *
30
+ * @param {string} quality - The quality which was selected.
31
+ * @returns {Object} The event in a format suitable for sending via
32
+ *      sendAnalytics.
33
+ */
34
+const createEvent = function(quality) {
35
+    return createToolbarEvent(
36
+        'video.quality',
37
+        {
38
+            quality
39
+        });
40
+};
41
+
29 42
 /**
30 43
  * Implements a React {@link Component} which displays a dialog with a slider
31 44
  * for selecting a new receive video quality.
@@ -255,12 +268,13 @@ class VideoQualityDialog extends Component {
255 268
      * @returns {void}
256 269
      */
257 270
     _enableAudioOnly() {
258
-        sendAnalyticsEvent(TOOLBAR_AUDIO_ONLY_ENABLED);
271
+        sendAnalytics(createEvent('audio.only'));
259 272
         logger.log('Video quality: audio only enabled');
260 273
         this.props.dispatch(setAudioOnly(true));
261 274
     }
262 275
 
263 276
     /**
277
+     * Handles the action of the high definition video being selected.
264 278
      * Dispatches an action to receive high quality video from remote
265 279
      * participants.
266 280
      *
@@ -268,7 +282,7 @@ class VideoQualityDialog extends Component {
268 282
      * @returns {void}
269 283
      */
270 284
     _enableHighDefinition() {
271
-        sendAnalyticsEvent(TOOLBAR_VIDEO_QUALITY_HIGH);
285
+        sendAnalytics(createEvent('high'));
272 286
         logger.log('Video quality: high enabled');
273 287
         this.props.dispatch(setReceiveVideoQuality(HIGH));
274 288
     }
@@ -281,7 +295,7 @@ class VideoQualityDialog extends Component {
281 295
      * @returns {void}
282 296
      */
283 297
     _enableLowDefinition() {
284
-        sendAnalyticsEvent(TOOLBAR_VIDEO_QUALITY_LOW);
298
+        sendAnalytics(createEvent('low'));
285 299
         logger.log('Video quality: low enabled');
286 300
         this.props.dispatch(setReceiveVideoQuality(LOW));
287 301
     }
@@ -294,7 +308,7 @@ class VideoQualityDialog extends Component {
294 308
      * @returns {void}
295 309
      */
296 310
     _enableStandardDefinition() {
297
-        sendAnalyticsEvent(TOOLBAR_VIDEO_QUALITY_STANDARD);
311
+        sendAnalytics(createEvent('standard'));
298 312
         logger.log('Video quality: standard enabled');
299 313
         this.props.dispatch(setReceiveVideoQuality(STANDARD));
300 314
     }

Loading…
取消
儲存