Browse Source

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 years ago
parent
commit
090f2f9ccb
35 changed files with 1046 additions and 1024 deletions
  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 View File

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

+ 146
- 0
analytics-ga.js View File

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 View File

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 View File

16
 import * as JitsiMeetConferenceEvents from './ConferenceEvents';
16
 import * as JitsiMeetConferenceEvents from './ConferenceEvents';
17
 
17
 
18
 import {
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
     initAnalytics,
24
     initAnalytics,
31
-    sendAnalyticsEvent
25
+    sendAnalytics
32
 } from './react/features/analytics';
26
 } from './react/features/analytics';
33
 
27
 
34
 import EventEmitter from 'events';
28
 import EventEmitter from 'events';
741
             })
735
             })
742
             .then(([ tracks, con ]) => {
736
             .then(([ tracks, con ]) => {
743
                 tracks.forEach(track => {
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
                         track.mute();
745
                         track.mute();
753
                     }
746
                     }
754
                 });
747
                 });
1453
             promise = createLocalTracksF({ devices: [ 'video' ] })
1446
             promise = createLocalTracksF({ devices: [ 'video' ] })
1454
                 .then(([ stream ]) => this.useVideoStream(stream))
1447
                 .then(([ stream ]) => this.useVideoStream(stream))
1455
                 .then(() => {
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
                     if (!this.localVideo && wasVideoMuted) {
1452
                     if (!this.localVideo && wasVideoMuted) {
1459
                         return Promise.reject('No local video to be muted!');
1453
                         return Promise.reject('No local video to be muted!');
1460
                     } else if (wasVideoMuted && this.localVideo) {
1454
                     } else if (wasVideoMuted && this.localVideo) {
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
      * replacing it with a desktop one.
1607
      * replacing it with a desktop one.
1614
      *
1608
      *
1615
      * @param {Object} [options] - Screen sharing options that will be passed to
1609
      * @param {Object} [options] - Screen sharing options that will be passed to
1632
             .then(stream => this.useVideoStream(stream))
1626
             .then(stream => this.useVideoStream(stream))
1633
             .then(() => {
1627
             .then(() => {
1634
                 this.videoSwitchInProgress = false;
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
             .catch(error => {
1632
             .catch(error => {
1639
                 this.videoSwitchInProgress = false;
1633
                 this.videoSwitchInProgress = false;
1928
 
1922
 
1929
                     room.selectParticipant(id);
1923
                     room.selectParticipant(id);
1930
                 } catch (e) {
1924
                 } catch (e) {
1931
-                    sendAnalyticsEvent(SELECT_PARTICIPANT_FAILED);
1925
+                    sendAnalytics(createSelectParticipantFailedEvent(e));
1932
                     reportError(e);
1926
                     reportError(e);
1933
                 }
1927
                 }
1934
             });
1928
             });
2152
         APP.UI.addListener(
2146
         APP.UI.addListener(
2153
             UIEvents.RESOLUTION_CHANGED,
2147
             UIEvents.RESOLUTION_CHANGED,
2154
             (id, oldResolution, newResolution, delay) => {
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
         /* eslint-enable max-params */
2157
         /* eslint-enable max-params */
2193
             cameraDeviceId => {
2177
             cameraDeviceId => {
2194
                 const videoWasMuted = this.isLocalVideoMuted();
2178
                 const videoWasMuted = this.isLocalVideoMuted();
2195
 
2179
 
2196
-                sendAnalyticsEvent(SETTINGS_CHANGE_DEVICE_VIDEO);
2180
+                sendAnalytics(createDeviceChangedEvent('video', 'input'));
2197
                 createLocalTracksF({
2181
                 createLocalTracksF({
2198
                     devices: [ 'video' ],
2182
                     devices: [ 'video' ],
2199
                     cameraDeviceId,
2183
                     cameraDeviceId,
2232
             micDeviceId => {
2216
             micDeviceId => {
2233
                 const audioWasMuted = this.isLocalAudioMuted();
2217
                 const audioWasMuted = this.isLocalAudioMuted();
2234
 
2218
 
2235
-                sendAnalyticsEvent(SETTINGS_CHANGE_DEVICE_AUDIO_IN);
2219
+                sendAnalytics(createDeviceChangedEvent('audio', 'input'));
2236
                 createLocalTracksF({
2220
                 createLocalTracksF({
2237
                     devices: [ 'audio' ],
2221
                     devices: [ 'audio' ],
2238
                     cameraDeviceId: null,
2222
                     cameraDeviceId: null,
2262
         APP.UI.addListener(
2246
         APP.UI.addListener(
2263
             UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED,
2247
             UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED,
2264
             audioOutputDeviceId => {
2248
             audioOutputDeviceId => {
2265
-                sendAnalyticsEvent(SETTINGS_CHANGE_DEVICE_AUDIO_OUT);
2249
+                sendAnalytics(createDeviceChangedEvent('audio', 'output'));
2266
                 APP.settings.setAudioOutputDeviceId(audioOutputDeviceId)
2250
                 APP.settings.setAudioOutputDeviceId(audioOutputDeviceId)
2267
                     .then(() => logger.log('changed audio output device'))
2251
                     .then(() => logger.log('changed audio output device'))
2268
                     .catch(err => {
2252
                     .catch(err => {
2528
                     // If audio was muted before, or we unplugged current device
2512
                     // If audio was muted before, or we unplugged current device
2529
                     // and selected new one, then mute new audio track.
2513
                     // and selected new one, then mute new audio track.
2530
                     if (audioWasMuted) {
2514
                     if (audioWasMuted) {
2531
-                        sendAnalyticsEvent(DEVICE_LIST_CHANGED_AUDIO_MUTED);
2515
+                        sendAnalytics(createTrackMutedEvent(
2516
+                            'audio',
2517
+                            'device list changed'));
2532
                         logger.log('Audio mute: device list changed');
2518
                         logger.log('Audio mute: device list changed');
2533
                         muteLocalAudio(true);
2519
                         muteLocalAudio(true);
2534
                     }
2520
                     }
2536
                     // If video was muted before, or we unplugged current device
2522
                     // If video was muted before, or we unplugged current device
2537
                     // and selected new one, then mute new video track.
2523
                     // and selected new one, then mute new video track.
2538
                     if (!this.isSharingScreen && videoWasMuted) {
2524
                     if (!this.isSharingScreen && videoWasMuted) {
2539
-                        sendAnalyticsEvent(DEVICE_LIST_CHANGED_VIDEO_MUTED);
2525
+                        sendAnalytics(createTrackMutedEvent(
2526
+                            'video',
2527
+                            'device list changed'));
2540
                         logger.log('Video mute: device list changed');
2528
                         logger.log('Video mute: device list changed');
2541
                         muteLocalVideo(true);
2529
                         muteLocalVideo(true);
2542
                     }
2530
                     }
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
      * Disconnect from the conference and optionally request user feedback.
2614
      * Disconnect from the conference and optionally request user feedback.
2658
      * @param {boolean} [requestFeedback=false] if user feedback should be
2615
      * @param {boolean} [requestFeedback=false] if user feedback should be

+ 6
- 3
config.js View File

307
         // backToP2PDelay: 5
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
     // Information about the jitsi-meet instance we are connecting to, including
316
     // Information about the jitsi-meet instance we are connecting to, including
312
     // the user region as seen by the server.
317
     // the user region as seen by the server.
313
-    //
314
-
315
     deploymentInfo: {
318
     deploymentInfo: {
316
         // shard: "shard1",
319
         // shard: "shard1",
317
         // region: "europe",
320
         // region: "europe",
318
         // userRegion: "asia"
321
         // userRegion: "asia"
319
     }
322
     }
320
 
323
 
324
+
321
     // List of undocumented settings used in jitsi-meet
325
     // List of undocumented settings used in jitsi-meet
322
     /**
326
     /**
323
      alwaysVisibleToolbar
327
      alwaysVisibleToolbar
324
-     analyticsScriptUrls
325
      autoEnableDesktopSharing
328
      autoEnableDesktopSharing
326
      autoRecord
329
      autoRecord
327
      autoRecordToken
330
      autoRecordToken

+ 36
- 14
modules/API/API.js View File

3
 import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
3
 import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
4
 import { parseJWTFromURLParams } from '../../react/features/base/jwt';
4
 import { parseJWTFromURLParams } from '../../react/features/base/jwt';
5
 import {
5
 import {
6
-    API_TOGGLE_AUDIO,
7
-    API_TOGGLE_VIDEO,
8
-    sendAnalyticsEvent
6
+    createApiEvent,
7
+    sendAnalytics
9
 } from '../../react/features/analytics';
8
 } from '../../react/features/analytics';
10
 import { getJitsiMeetTransport } from '../transport';
9
 import { getJitsiMeetTransport } from '../transport';
11
 
10
 
56
  */
55
  */
57
 function initCommands() {
56
 function initCommands() {
58
     commands = {
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
         'toggle-audio': () => {
62
         'toggle-audio': () => {
62
-            sendAnalyticsEvent(API_TOGGLE_AUDIO);
63
+            sendAnalytics(createApiEvent('toggle-audio'));
63
             logger.log('Audio toggle: API command received');
64
             logger.log('Audio toggle: API command received');
64
             APP.conference.toggleAudioMuted(false /* no UI */);
65
             APP.conference.toggleAudioMuted(false /* no UI */);
65
         },
66
         },
66
         'toggle-video': () => {
67
         'toggle-video': () => {
67
-            sendAnalyticsEvent(API_TOGGLE_VIDEO);
68
+            sendAnalytics(createApiEvent('toggle-video'));
68
             logger.log('Video toggle: API command received');
69
             logger.log('Video toggle: API command received');
69
             APP.conference.toggleVideoMuted(false /* no UI */);
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
     transport.on('event', ({ data, name }) => {
101
     transport.on('event', ({ data, name }) => {
80
         if (name && commands[name]) {
102
         if (name && commands[name]) {

+ 26
- 0
modules/UI/UI.js View File

142
     UIUtil.isFullScreen() ? UIUtil.exitFullScreen() : UIUtil.enterFullScreen();
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
  * Notify user that server has shut down.
172
  * Notify user that server has shut down.
147
  */
173
  */

+ 35
- 17
modules/UI/recording/Recording.js View File

24
     JitsiRecordingStatus
24
     JitsiRecordingStatus
25
 } from '../../../react/features/base/lib-jitsi-meet';
25
 } from '../../../react/features/base/lib-jitsi-meet';
26
 import {
26
 import {
27
-    RECORDING_CANCELED,
28
-    RECORDING_CLICKED,
29
-    RECORDING_STARTED,
30
-    RECORDING_STOPPED,
31
-    sendAnalyticsEvent
27
+    createToolbarEvent,
28
+    createRecordingDialogEvent,
29
+    sendAnalytics
32
 } from '../../../react/features/analytics';
30
 } from '../../../react/features/analytics';
33
 import { setToolboxEnabled } from '../../../react/features/toolbox';
31
 import { setToolboxEnabled } from '../../../react/features/toolbox';
34
 import { setNotificationsEnabled } from '../../../react/features/notifications';
32
 import { setNotificationsEnabled } from '../../../react/features/notifications';
452
     },
450
     },
453
 
451
 
454
     // checks whether recording is enabled and whether we have params
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
     checkAutoRecord() {
454
     checkAutoRecord() {
457
         if (_isRecordingButtonEnabled && config.autoRecord) {
455
         if (_isRecordingButtonEnabled && config.autoRecord) {
458
             this.predefinedToken = UIUtil.escapeHtml(config.autoRecordToken);
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
      * @returns {void}
466
      * @returns {void}
468
      */
467
      */
469
     _onToolbarButtonClick() {
468
     _onToolbarButtonClick() {
469
+        sendAnalytics(createToolbarEvent(
470
+            'recording.button',
471
+            {
472
+                'dialog_present': Boolean(dialog)
473
+            }));
474
+
470
         if (dialog) {
475
         if (dialog) {
471
             return;
476
             return;
472
         }
477
         }
473
 
478
 
474
-        sendAnalyticsEvent(RECORDING_CLICKED);
475
         switch (this.currentState) {
479
         switch (this.currentState) {
476
         case JitsiRecordingStatus.ON:
480
         case JitsiRecordingStatus.ON:
477
         case JitsiRecordingStatus.RETRYING:
481
         case JitsiRecordingStatus.RETRYING:
479
             _showStopRecordingPrompt(this.recordingType).then(
483
             _showStopRecordingPrompt(this.recordingType).then(
480
                 () => {
484
                 () => {
481
                     this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED);
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
                 () => {}); // eslint-disable-line no-empty-function
494
                 () => {}); // eslint-disable-line no-empty-function
485
             break;
495
             break;
492
                     this.eventEmitter.emit(
502
                     this.eventEmitter.emit(
493
                         UIEvents.RECORDING_TOGGLED,
503
                         UIEvents.RECORDING_TOGGLED,
494
                         { streamId });
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
                 .catch(reason => {
513
                 .catch(reason => {
498
                     if (reason === APP.UI.messageHandler.CANCEL) {
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
                     } else {
521
                     } else {
501
                         logger.error(reason);
522
                         logger.error(reason);
502
                     }
523
                     }
503
                 });
524
                 });
504
             } else {
525
             } else {
526
+                // Note that we only fire analytics events for Jibri.
505
                 if (this.predefinedToken) {
527
                 if (this.predefinedToken) {
506
                     this.eventEmitter.emit(
528
                     this.eventEmitter.emit(
507
                         UIEvents.RECORDING_TOGGLED,
529
                         UIEvents.RECORDING_TOGGLED,
508
                         { token: this.predefinedToken });
530
                         { token: this.predefinedToken });
509
-                    sendAnalyticsEvent(RECORDING_STARTED);
510
 
531
 
511
                     return;
532
                     return;
512
                 }
533
                 }
515
                     this.eventEmitter.emit(
536
                     this.eventEmitter.emit(
516
                         UIEvents.RECORDING_TOGGLED,
537
                         UIEvents.RECORDING_TOGGLED,
517
                         { token });
538
                         { token });
518
-                    sendAnalyticsEvent(RECORDING_STARTED);
519
                 })
539
                 })
520
                 .catch(reason => {
540
                 .catch(reason => {
521
-                    if (reason === APP.UI.messageHandler.CANCEL) {
522
-                        sendAnalyticsEvent(RECORDING_CANCELED);
523
-                    } else {
541
+                    if (reason !== APP.UI.messageHandler.CANCEL) {
524
                         logger.error(reason);
542
                         logger.error(reason);
525
                     }
543
                     }
526
                 });
544
                 });

+ 17
- 19
modules/UI/shared_video/SharedVideo.js View File

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

+ 4
- 5
modules/UI/side_pannels/profile/Profile.js View File

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

+ 37
- 26
modules/UI/videolayout/Filmstrip.js View File

6
 import UIUtil from '../util/UIUtil';
6
 import UIUtil from '../util/UIUtil';
7
 
7
 
8
 import {
8
 import {
9
-    TOOLBAR_FILMSTRIP_TOGGLED,
10
-    sendAnalyticsEvent
9
+    createShortcutEvent,
10
+    createToolbarEvent,
11
+    sendAnalytics
11
 } from '../../../react/features/analytics';
12
 } from '../../../react/features/analytics';
12
 
13
 
13
 const Filmstrip = {
14
 const Filmstrip = {
75
         // Firing the event instead of executing toggleFilmstrip method because
76
         // Firing the event instead of executing toggleFilmstrip method because
76
         // it's important to hide the filmstrip by UI.toggleFilmstrip in order
77
         // it's important to hide the filmstrip by UI.toggleFilmstrip in order
77
         // to correctly resize the video area.
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
         this._registerToggleFilmstripShortcut();
92
         this._registerToggleFilmstripShortcut();
82
     },
93
     },
94
         // Firing the event instead of executing toggleFilmstrip method because
105
         // Firing the event instead of executing toggleFilmstrip method because
95
         // it's important to hide the filmstrip by UI.toggleFilmstrip in order
106
         // it's important to hide the filmstrip by UI.toggleFilmstrip in order
96
         // to correctly resize the video area.
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
         APP.keyboardshortcut.registerShortcut(
117
         APP.keyboardshortcut.registerShortcut(
100
             shortcut,
118
             shortcut,
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
      * @param visible optional {Boolean} which specifies the desired visibility
153
      * @param visible optional {Boolean} which specifies the desired visibility
135
      * of the filmstrip. If not specified, the visibility will be flipped
154
      * of the filmstrip. If not specified, the visibility will be flipped
136
      * (i.e. toggled); otherwise, the visibility will be set to the specified
155
      * (i.e. toggled); otherwise, the visibility will be set to the specified
137
      * value.
156
      * value.
138
-     * @param {Boolean} sendAnalytics - True to send an analytics event. The
139
-     * default value is true.
140
      *
157
      *
141
      * Note:
158
      * Note:
142
      * This method shouldn't be executed directly to hide the filmstrip.
159
      * This method shouldn't be executed directly to hide the filmstrip.
143
      * It's important to hide the filmstrip with UI.toggleFilmstrip in order
160
      * It's important to hide the filmstrip with UI.toggleFilmstrip in order
144
      * to correctly resize the video area.
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
             return;
170
             return;
154
         }
171
         }
155
-        if (sendAnalytics) {
156
-            sendAnalyticsEvent(TOOLBAR_FILMSTRIP_TOGGLED);
157
-        }
172
+
158
         this.filmstrip.toggleClass('hidden');
173
         this.filmstrip.toggleClass('hidden');
159
 
174
 
160
-        if (visible) {
175
+        if (wasFilmstripVisible) {
161
             this.showMenuUpIcon();
176
             this.showMenuUpIcon();
162
         } else {
177
         } else {
163
             this.showMenuDownIcon();
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
                 UIEvents.TOGGLED_FILMSTRIP,
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 View File

2
 
2
 
3
 import { toggleDialog } from '../../react/features/base/dialog';
3
 import { toggleDialog } from '../../react/features/base/dialog';
4
 import {
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
 } from '../../react/features/analytics';
9
 } from '../../react/features/analytics';
11
 import { KeyboardShortcutsDialog }
10
 import { KeyboardShortcutsDialog }
12
     from '../../react/features/keyboard-shortcuts';
11
     from '../../react/features/keyboard-shortcuts';
72
                 || $(':focus').is('textarea'))) {
71
                 || $(':focus').is('textarea'))) {
73
                 if (this._getKeyboardKey(e).toUpperCase() === ' ') {
72
                 if (this._getKeyboardKey(e).toUpperCase() === ' ') {
74
                     if (APP.conference.isLocalAudioMuted()) {
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
                         APP.conference.muteAudio(false);
78
                         APP.conference.muteAudio(false);
78
                     }
79
                     }
79
                 }
80
                 }
93
      * Registers a new shortcut.
94
      * Registers a new shortcut.
94
      *
95
      *
95
      * @param shortcutChar the shortcut character triggering the action
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
      * element to this shortcut and used to show the shortcut character on the
98
      * element to this shortcut and used to show the shortcut character on the
98
      * element tooltip
99
      * element tooltip
99
      * @param exec the function to be executed when the shortcut is pressed
100
      * @param exec the function to be executed when the shortcut is pressed
175
      */
176
      */
176
     _initGlobalShortcuts() {
177
     _initGlobalShortcuts() {
177
         this.registerShortcut('?', null, () => {
178
         this.registerShortcut('?', null, () => {
178
-            sendAnalyticsEvent(SHORTCUT_HELP);
179
+            sendAnalytics(createShortcutEvent('help'));
179
             APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
180
             APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
180
                 shortcutDescriptions: _shortcutsHelp
181
                 shortcutDescriptions: _shortcutsHelp
181
             }));
182
             }));
184
         // register SPACE shortcut in two steps to insure visibility of help
185
         // register SPACE shortcut in two steps to insure visibility of help
185
         // message
186
         // message
186
         this.registerShortcut(' ', null, () => {
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
             APP.conference.muteAudio(true);
190
             APP.conference.muteAudio(true);
190
         });
191
         });
191
         this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
192
         this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
192
 
193
 
193
         if (!interfaceConfig.filmStripOnly) {
194
         if (!interfaceConfig.filmStripOnly) {
194
             this.registerShortcut('T', null, () => {
195
             this.registerShortcut('T', null, () => {
195
-                sendAnalyticsEvent(SHORTCUT_SPEAKER_STATS_CLICKED);
196
+                sendAnalytics(createShortcutEvent('speaker.stats'));
196
                 APP.store.dispatch(toggleDialog(SpeakerStats, {
197
                 APP.store.dispatch(toggleDialog(SpeakerStats, {
197
                     conference: APP.conference
198
                     conference: APP.conference
198
                 }));
199
                 }));

+ 6
- 6
modules/util/JitsiMeetLogStorage.js View File

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

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

+ 1
- 1
package.json View File

44
     "jquery-i18next": "1.2.0",
44
     "jquery-i18next": "1.2.0",
45
     "js-md5": "0.6.1",
45
     "js-md5": "0.6.1",
46
     "jwt-decode": "2.2.0",
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
     "lodash": "4.17.4",
48
     "lodash": "4.17.4",
49
     "moment": "2.19.4",
49
     "moment": "2.19.4",
50
     "nuclear-js": "1.4.0",
50
     "nuclear-js": "1.4.0",

+ 396
- 598
react/features/analytics/AnalyticsEvents.js
File diff suppressed because it is too large
View File


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

9
 const logger = require('jitsi-meet-logger').getLogger(__filename);
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
     const state = getState();
40
     const state = getState();
39
     const config = state['features/base/config'];
41
     const config = state['features/base/config'];
40
     const { analyticsScriptUrls } = config;
42
     const { analyticsScriptUrls } = config;
41
-    const machineId = JitsiMeetJS.getMachineId();
42
     const { user } = state['features/base/jwt'];
43
     const { user } = state['features/base/jwt'];
43
     const handlerConstructorOptions = {
44
     const handlerConstructorOptions = {
44
-        product: 'lib-jitsi-meet',
45
         version: JitsiMeetJS.version,
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
     _loadHandlers(analyticsScriptUrls, handlerConstructorOptions)
49
     _loadHandlers(analyticsScriptUrls, handlerConstructorOptions)
52
         .then(handlers => {
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
             const { group, server } = state['features/base/jwt'];
52
             const { group, server } = state['features/base/jwt'];
53
+            const permanentProperties = {};
58
 
54
 
59
             if (server) {
55
             if (server) {
60
                 permanentProperties.server = server;
56
                 permanentProperties.server = server;
76
             }
72
             }
77
 
73
 
78
             analytics.addPermanentProperties(permanentProperties);
74
             analytics.addPermanentProperties(permanentProperties);
75
+            analytics.setConferenceName(roomName);
76
+
77
+            // Set the handlers last, since this triggers emptying of the cache
79
             analytics.setAnalyticsHandlers(handlers);
78
             analytics.setAnalyticsHandlers(handlers);
80
         },
79
         },
81
         error => analytics.dispose() && logger.error(error));
80
         error => analytics.dispose() && logger.error(error));

+ 4
- 9
react/features/base/conference/actions.js View File

3
 import UIEvents from '../../../../service/UI/UIEvents';
3
 import UIEvents from '../../../../service/UI/UIEvents';
4
 
4
 
5
 import {
5
 import {
6
-    START_MUTED_SERVER_AUDIO_,
7
-    START_MUTED_SERVER_VIDEO_,
8
-    sendAnalyticsEvent
6
+    createStartMutedConfigurationEvent,
7
+    sendAnalytics
9
 } from '../../analytics';
8
 } from '../../analytics';
10
 import { getName } from '../../app';
9
 import { getName } from '../../app';
11
 import { JitsiConferenceEvents } from '../lib-jitsi-meet';
10
 import { JitsiConferenceEvents } from '../lib-jitsi-meet';
90
             const audioMuted = Boolean(conference.startAudioMuted);
89
             const audioMuted = Boolean(conference.startAudioMuted);
91
             const videoMuted = Boolean(conference.startVideoMuted);
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
             logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${
94
             logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${
100
                 videoMuted ? 'video' : ''}`);
95
                 videoMuted ? 'video' : ''}`);
101
 
96
 

+ 18
- 19
react/features/base/conference/middleware.js View File

3
 import UIEvents from '../../../../service/UI/UIEvents';
3
 import UIEvents from '../../../../service/UI/UIEvents';
4
 
4
 
5
 import {
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
 } from '../../analytics';
11
 } from '../../analytics';
13
 import { CONNECTION_ESTABLISHED } from '../connection';
12
 import { CONNECTION_ESTABLISHED } from '../connection';
14
 import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../media';
13
 import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../media';
131
     const result = next(action);
130
     const result = next(action);
132
 
131
 
133
     if (getState()['features/base/conference'].audioOnly) {
132
     if (getState()['features/base/conference'].audioOnly) {
134
-        sendAnalyticsEvent(AUDIO_ONLY_DISABLED);
133
+        sendAnalytics(createAudioOnlyDisableEvent());
135
         logger.log('Audio only disabled');
134
         logger.log('Audio only disabled');
136
         dispatch(setAudioOnly(false));
135
         dispatch(setAudioOnly(false));
137
     }
136
     }
193
 
192
 
194
     if (typeof APP !== 'undefined') {
193
     if (typeof APP !== 'undefined') {
195
         const pinnedParticipant = getPinnedParticipant(participants);
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
     // The following condition prevents signaling to pin local participant and
210
     // The following condition prevents signaling to pin local participant and

+ 8
- 16
react/features/base/media/middleware.js View File

1
 /* @flow */
1
 /* @flow */
2
 
2
 
3
 import {
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
 } from '../../analytics';
8
 } from '../../analytics';
10
 import { SET_ROOM, setAudioOnly } from '../conference';
9
 import { SET_ROOM, setAudioOnly } from '../conference';
11
 import { parseURLParams } from '../config';
10
 import { parseURLParams } from '../config';
90
     audioMuted = Boolean(audioMuted);
89
     audioMuted = Boolean(audioMuted);
91
     videoMuted = Boolean(videoMuted);
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
     logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${
95
     logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${
101
         videoMuted ? 'video' : ''}`);
96
         videoMuted ? 'video' : ''}`);
128
             audioOnly = true;
123
             audioOnly = true;
129
         }
124
         }
130
 
125
 
131
-        sendAnalyticsEvent(
132
-            `${START_AUDIO_ONLY_}.${audioOnly ? 'enabled' : 'disabled'}`);
126
+        sendAnalytics(createStartAudioOnlyEvent(audioOnly));
133
         logger.log(`Start audio only set to ${audioOnly.toString()}`);
127
         logger.log(`Start audio only set to ${audioOnly.toString()}`);
134
         dispatch(setAudioOnly(audioOnly));
128
         dispatch(setAudioOnly(audioOnly));
135
     }
129
     }
155
     // not yet in redux state and JitsiTrackEvents.TRACK_MUTE_CHANGED may be
149
     // not yet in redux state and JitsiTrackEvents.TRACK_MUTE_CHANGED may be
156
     // fired before track gets to state.
150
     // fired before track gets to state.
157
     if (track.muted !== muted) {
151
     if (track.muted !== muted) {
158
-        sendAnalyticsEvent(
159
-            `${SYNC_TRACK_STATE_}.${track.mediaType}.${
160
-                muted ? 'muted' : 'unmuted'}`);
152
+        sendAnalytics(createSyncTrackStateEvent(track.mediaType, muted));
161
         logger.log(`Sync ${track.mediaType} track muted state to ${
153
         logger.log(`Sync ${track.mediaType} track muted state to ${
162
             muted ? 'muted' : 'unmuted'}`);
154
             muted ? 'muted' : 'unmuted'}`);
163
         track.muted = muted;
155
         track.muted = muted;

+ 6
- 5
react/features/base/tracks/actions.js View File

1
 import {
1
 import {
2
-    REPLACE_TRACK_,
3
-    sendAnalyticsEvent
2
+    createTrackMutedEvent,
3
+    sendAnalytics
4
 } from '../../analytics';
4
 } from '../../analytics';
5
 import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
5
 import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
6
 import {
6
 import {
220
                                     : setAudioMuted;
220
                                     : setAudioMuted;
221
                             const isMuted = newTrack.isMuted();
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
                             logger.log(`Replace ${newTrack.getType()} track - ${
227
                             logger.log(`Replace ${newTrack.getType()} track - ${
227
                                 isMuted ? 'muted' : 'unmuted'}`);
228
                                 isMuted ? 'muted' : 'unmuted'}`);
228
 
229
 

+ 3
- 3
react/features/feedback/components/FeedbackDialog.web.js View File

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

+ 1
- 1
react/features/filmstrip/middleware.js View File

26
                 // not need the middleware implemented here, Filmstrip.init, and
26
                 // not need the middleware implemented here, Filmstrip.init, and
27
                 // UI.start.
27
                 // UI.start.
28
                 || (Filmstrip.filmstrip
28
                 || (Filmstrip.filmstrip
29
-                    && Filmstrip.toggleFilmstrip(!newValue, false));
29
+                    && Filmstrip.toggleFilmstrip(!newValue));
30
 
30
 
31
             return result;
31
             return result;
32
         }
32
         }

+ 3
- 3
react/features/invite/components/InviteDialog.web.js View File

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

+ 5
- 3
react/features/mobile/background/actions.js View File

1
 /* @flow */
1
 /* @flow */
2
 
2
 
3
 import {
3
 import {
4
-    CALLKIT_BACKGROUND_VIDEO_MUTED,
5
-    sendAnalyticsEvent
4
+    createTrackMutedEvent,
5
+    sendAnalytics
6
 } from '../../analytics';
6
 } from '../../analytics';
7
 import { setLastN } from '../../base/conference';
7
 import { setLastN } from '../../base/conference';
8
 import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../../base/media';
8
 import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../../base/media';
46
 
46
 
47
         audioOnly || dispatch(setLastN(muted ? 0 : undefined));
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
         dispatch(setVideoMuted(muted, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
53
         dispatch(setVideoMuted(muted, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
52
     };
54
     };

+ 3
- 4
react/features/mobile/callkit/middleware.js View File

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

+ 14
- 5
react/features/overlay/components/AbstractPageReloadOverlay.js View File

3
 import PropTypes from 'prop-types';
3
 import PropTypes from 'prop-types';
4
 import React, { Component } from 'react';
4
 import React, { Component } from 'react';
5
 
5
 
6
-import { PAGE_RELOAD } from '../../analytics';
6
+import {
7
+    createPageReloadScheduledEvent,
8
+    sendAnalytics
9
+} from '../../analytics';
7
 import {
10
 import {
8
     isFatalJitsiConferenceError,
11
     isFatalJitsiConferenceError,
9
     isFatalJitsiConnectionError
12
     isFatalJitsiConnectionError
159
         // sent to the backed.
162
         // sent to the backed.
160
         // FIXME: We should dispatch action for this.
163
         // FIXME: We should dispatch action for this.
161
         if (typeof APP !== 'undefined') {
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
         logger.info(
177
         logger.info(
169
             `The conference will be reloaded after ${
178
             `The conference will be reloaded after ${
170
                 this.state.timeoutSeconds} seconds.`);
179
                 this.state.timeoutSeconds} seconds.`);

+ 7
- 8
react/features/remote-video-menu/components/KickButton.js View File

3
 import { connect } from 'react-redux';
3
 import { connect } from 'react-redux';
4
 
4
 
5
 import {
5
 import {
6
-    REMOTE_VIDEO_MENU_KICK,
7
-    sendAnalyticsEvent
6
+    createRemoteVideoMenuButtonEvent,
7
+    sendAnalytics
8
 } from '../../analytics';
8
 } from '../../analytics';
9
 import { translate } from '../../base/i18n';
9
 import { translate } from '../../base/i18n';
10
 import { kickParticipant } from '../../base/participants';
10
 import { kickParticipant } from '../../base/participants';
86
     _onClick() {
86
     _onClick() {
87
         const { dispatch, onClick, participantID } = this.props;
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
         dispatch(kickParticipant(participantID));
95
         dispatch(kickParticipant(participantID));
97
 
96
 
98
         if (onClick) {
97
         if (onClick) {

+ 6
- 8
react/features/remote-video-menu/components/MuteButton.js View File

3
 import { connect } from 'react-redux';
3
 import { connect } from 'react-redux';
4
 
4
 
5
 import {
5
 import {
6
-    REMOTE_VIDEO_MENU_MUTE_CLICKED,
7
-    sendAnalyticsEvent
6
+    createRemoteVideoMenuButtonEvent,
7
+    sendAnalytics
8
 } from '../../analytics';
8
 } from '../../analytics';
9
 import { translate } from '../../base/i18n';
9
 import { translate } from '../../base/i18n';
10
 import { openDialog } from '../../base/dialog';
10
 import { openDialog } from '../../base/dialog';
101
     _onClick() {
101
     _onClick() {
102
         const { dispatch, onClick, participantID } = this.props;
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
         dispatch(openDialog(MuteRemoteParticipantDialog, { participantID }));
110
         dispatch(openDialog(MuteRemoteParticipantDialog, { participantID }));
113
 
111
 

+ 4
- 10
react/features/remote-video-menu/components/MuteRemoteParticipantDialog.web.js View File

6
 import { translate } from '../../base/i18n';
6
 import { translate } from '../../base/i18n';
7
 
7
 
8
 import {
8
 import {
9
-    REMOTE_VIDEO_MENU_MUTE_CONFIRMED,
10
-    sendAnalyticsEvent
9
+    createRemoteMuteConfirmedEvent,
10
+    sendAnalytics
11
 } from '../../analytics';
11
 } from '../../analytics';
12
 import { muteRemoteParticipant } from '../../base/participants';
12
 import { muteRemoteParticipant } from '../../base/participants';
13
 
13
 
77
      * Handles the submit button action.
77
      * Handles the submit button action.
78
      *
78
      *
79
      * @private
79
      * @private
80
-     * @returns {void}
80
+     * @returns {boolean} - True (to note that the modal should be closed).
81
      */
81
      */
82
     _onSubmit() {
82
     _onSubmit() {
83
         const { dispatch, participantID } = this.props;
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
         dispatch(muteRemoteParticipant(participantID));
87
         dispatch(muteRemoteParticipant(participantID));
94
 
88
 

+ 12
- 17
react/features/remote-video-menu/components/RemoteControlButton.js View File

2
 import React, { Component } from 'react';
2
 import React, { Component } from 'react';
3
 
3
 
4
 import {
4
 import {
5
-    REMOTE_VIDEO_MENU_REMOTE_CONTROL_,
6
-    sendAnalyticsEvent
5
+    createRemoteVideoMenuButtonEvent,
6
+    sendAnalytics
7
 } from '../../analytics';
7
 } from '../../analytics';
8
 import { translate } from '../../base/i18n';
8
 import { translate } from '../../base/i18n';
9
 
9
 
122
     _onClick() {
122
     _onClick() {
123
         const { onClick, participantID, remoteControlState } = this.props;
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
         if (onClick) {
140
         if (onClick) {

+ 7
- 2
react/features/toolbox/components/ProfileButton.web.js View File

4
 import React, { Component } from 'react';
4
 import React, { Component } from 'react';
5
 import { connect } from 'react-redux';
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
 import {
11
 import {
9
     getAvatarURL,
12
     getAvatarURL,
10
     getLocalParticipant
13
     getLocalParticipant
115
      */
118
      */
116
     _onClick() {
119
     _onClick() {
117
         if (!this.props._unclickable) {
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
             APP.UI.emitEvent(UIEvents.TOGGLE_PROFILE);
124
             APP.UI.emitEvent(UIEvents.TOGGLE_PROFILE);
120
         }
125
         }
121
     }
126
     }

+ 14
- 6
react/features/toolbox/components/Toolbox.native.js View File

4
 import { connect } from 'react-redux';
4
 import { connect } from 'react-redux';
5
 
5
 
6
 import {
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
 } from '../../analytics';
11
 } from '../../analytics';
12
 import {
12
 import {
13
     isNarrowAspectRatio,
13
     isNarrowAspectRatio,
188
     _onToggleAudio() {
188
     _onToggleAudio() {
189
         const mute = !this.props._audioMuted;
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
         // The user sees the reality i.e. the state of base/tracks and intends
197
         // The user sees the reality i.e. the state of base/tracks and intends
194
         // to change reality by tapping on the respective button i.e. the user
198
         // to change reality by tapping on the respective button i.e. the user
211
     _onToggleVideo() {
215
     _onToggleVideo() {
212
         const mute = !this.props._videoMuted;
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
         // The user sees the reality i.e. the state of base/tracks and intends
224
         // The user sees the reality i.e. the state of base/tracks and intends
217
         // to change reality by tapping on the respective button i.e. the user
225
         // to change reality by tapping on the respective button i.e. the user

+ 134
- 53
react/features/toolbox/defaultToolbarButtons.web.js View File

3
 import React from 'react';
3
 import React from 'react';
4
 
4
 
5
 import {
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
 } from '../analytics';
12
 } from '../analytics';
30
 import { ParticipantCounter } from '../contact-list';
13
 import { ParticipantCounter } from '../contact-list';
31
 import { openDeviceSelectionDialog } from '../device-selection';
14
 import { openDeviceSelectionDialog } from '../device-selection';
63
             isDisplayed: () => true,
46
             isDisplayed: () => true,
64
             id: 'toolbar_button_camera',
47
             id: 'toolbar_button_camera',
65
             onClick() {
48
             onClick() {
49
+                // TODO: Why is this different from the code which handles
50
+                // a keyboard shortcut?
66
                 const newVideoMutedState = !APP.conference.isLocalVideoMuted();
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
                 APP.UI.emitEvent(UIEvents.VIDEO_MUTED, newVideoMutedState);
61
                 APP.UI.emitEvent(UIEvents.VIDEO_MUTED, newVideoMutedState);
74
             },
62
             },
75
             popups: [
63
             popups: [
88
                     return;
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
                 APP.conference.toggleVideoMuted();
86
                 APP.conference.toggleVideoMuted();
93
             },
87
             },
94
             shortcutDescription: 'keyboardShortcuts.videoMute',
88
             shortcutDescription: 'keyboardShortcuts.videoMute',
105
                 <span id = 'unreadMessages' /></span>,
99
                 <span id = 'unreadMessages' /></span>,
106
             id: 'toolbar_button_chat',
100
             id: 'toolbar_button_chat',
107
             onClick() {
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
                 APP.UI.emitEvent(UIEvents.TOGGLE_CHAT);
109
                 APP.UI.emitEvent(UIEvents.TOGGLE_CHAT);
110
             },
110
             },
111
             shortcut: 'C',
111
             shortcut: 'C',
112
             shortcutAttr: 'toggleChatPopover',
112
             shortcutAttr: 'toggleChatPopover',
113
             shortcutFunc() {
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
                 APP.UI.toggleChat();
122
                 APP.UI.toggleChat();
116
             },
123
             },
117
             shortcutDescription: 'keyboardShortcuts.toggleChat',
124
             shortcutDescription: 'keyboardShortcuts.toggleChat',
128
             enabled: true,
135
             enabled: true,
129
             id: 'toolbar_contact_list',
136
             id: 'toolbar_contact_list',
130
             onClick() {
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
                 APP.UI.emitEvent(UIEvents.TOGGLE_CONTACT_LIST);
141
                 APP.UI.emitEvent(UIEvents.TOGGLE_CONTACT_LIST);
133
             },
142
             },
134
             sideContainerId: 'contacts_container',
143
             sideContainerId: 'contacts_container',
143
             enabled: true,
152
             enabled: true,
144
             id: 'toolbar_button_desktopsharing',
153
             id: 'toolbar_button_desktopsharing',
145
             onClick() {
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
                 APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
163
                 APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
152
             },
164
             },
153
             popups: [
165
             popups: [
160
             shortcut: 'D',
172
             shortcut: 'D',
161
             shortcutAttr: 'toggleDesktopSharingPopover',
173
             shortcutAttr: 'toggleDesktopSharingPopover',
162
             shortcutFunc() {
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
                 // eslint-disable-next-line no-empty-function
183
                 // eslint-disable-next-line no-empty-function
166
                 APP.conference.toggleScreenSharing().catch(() => {});
184
                 APP.conference.toggleScreenSharing().catch(() => {});
180
             },
198
             },
181
             id: 'toolbar_button_fodeviceselection',
199
             id: 'toolbar_button_fodeviceselection',
182
             onClick(dispatch: Function) {
200
             onClick(dispatch: Function) {
183
-                sendAnalyticsEvent(
184
-                    TOOLBAR_FILMSTRIP_ONLY_DEVICE_SELECTION_TOGGLED);
201
+                sendAnalytics(
202
+                    createToolbarEvent('filmstrip.only.device.selection'));
185
 
203
 
186
                 dispatch(openDeviceSelectionDialog());
204
                 dispatch(openDeviceSelectionDialog());
187
             },
205
             },
200
             hidden: true,
218
             hidden: true,
201
             id: 'toolbar_button_dialpad',
219
             id: 'toolbar_button_dialpad',
202
             onClick() {
220
             onClick() {
203
-                sendAnalyticsEvent(TOOLBAR_SIP_DIALPAD_CLICKED);
221
+                sendAnalytics(createToolbarEvent('dialpad'));
204
             },
222
             },
205
             tooltipKey: 'toolbar.dialpad'
223
             tooltipKey: 'toolbar.dialpad'
206
         },
224
         },
214
             hidden: true,
232
             hidden: true,
215
             id: 'toolbar_button_etherpad',
233
             id: 'toolbar_button_etherpad',
216
             onClick() {
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
                 APP.UI.emitEvent(UIEvents.ETHERPAD_CLICKED);
242
                 APP.UI.emitEvent(UIEvents.ETHERPAD_CLICKED);
219
             },
243
             },
220
             tooltipKey: 'toolbar.etherpad'
244
             tooltipKey: 'toolbar.etherpad'
228
             enabled: true,
252
             enabled: true,
229
             id: 'toolbar_button_fullScreen',
253
             id: 'toolbar_button_fullScreen',
230
             onClick() {
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
                 APP.UI.emitEvent(UIEvents.TOGGLE_FULLSCREEN);
268
                 APP.UI.emitEvent(UIEvents.TOGGLE_FULLSCREEN);
234
             },
269
             },
236
             shortcutAttr: 'toggleFullscreenPopover',
271
             shortcutAttr: 'toggleFullscreenPopover',
237
             shortcutDescription: 'keyboardShortcuts.fullScreen',
272
             shortcutDescription: 'keyboardShortcuts.fullScreen',
238
             shortcutFunc() {
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
                 APP.UI.toggleFullScreen();
281
                 APP.UI.toggleFullScreen();
241
             },
282
             },
242
             tooltipKey: 'toolbar.fullscreen'
283
             tooltipKey: 'toolbar.fullscreen'
252
             isDisplayed: () => true,
293
             isDisplayed: () => true,
253
             id: 'toolbar_button_hangup',
294
             id: 'toolbar_button_hangup',
254
             onClick() {
295
             onClick() {
255
-                sendAnalyticsEvent(TOOLBAR_HANGUP);
296
+                sendAnalytics(createToolbarEvent('hangup'));
256
                 APP.UI.emitEvent(UIEvents.HANGUP);
297
                 APP.UI.emitEvent(UIEvents.HANGUP);
257
             },
298
             },
258
             tooltipKey: 'toolbar.hangup'
299
             tooltipKey: 'toolbar.hangup'
275
             enabled: true,
316
             enabled: true,
276
             id: 'toolbar_button_link',
317
             id: 'toolbar_button_link',
277
             onClick(dispatch: Function) {
318
             onClick(dispatch: Function) {
278
-                sendAnalyticsEvent(TOOLBAR_INVITE_CLICKED);
319
+                sendAnalytics(createToolbarEvent('invite'));
279
 
320
 
280
                 dispatch(openInviteDialog());
321
                 dispatch(openInviteDialog());
281
             },
322
             },
293
             onClick() {
334
             onClick() {
294
                 const sharedVideoManager = APP.UI.getSharedVideoManager();
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
                 if (APP.conference.isLocalAudioMuted()) {
344
                 if (APP.conference.isLocalAudioMuted()) {
297
                     // If there's a shared video with the volume "on" and we
345
                     // If there's a shared video with the volume "on" and we
298
                     // aren't the video owner, we warn the user
346
                     // aren't the video owner, we warn the user
303
                         APP.UI.showCustomToolbarPopup(
351
                         APP.UI.showCustomToolbarPopup(
304
                             'microphone', 'unableToUnmutePopup', true, 5000);
352
                             'microphone', 'unableToUnmutePopup', true, 5000);
305
                     } else {
353
                     } else {
306
-                        sendAnalyticsEvent(TOOLBAR_AUDIO_UNMUTED);
354
+                        sendAnalytics(createToolbarEvent(
355
+                            AUDIO_MUTE,
356
+                            { enable: false }));
307
                         APP.UI.emitEvent(UIEvents.AUDIO_MUTED, false, true);
357
                         APP.UI.emitEvent(UIEvents.AUDIO_MUTED, false, true);
308
                     }
358
                     }
309
                 } else {
359
                 } else {
310
-                    sendAnalyticsEvent(TOOLBAR_AUDIO_MUTED);
360
+                    sendAnalytics(createToolbarEvent(
361
+                        AUDIO_MUTE,
362
+                        { enable: true }));
311
                     APP.UI.emitEvent(UIEvents.AUDIO_MUTED, true, true);
363
                     APP.UI.emitEvent(UIEvents.AUDIO_MUTED, true, true);
312
                 }
364
                 }
313
             },
365
             },
328
             shortcut: 'M',
380
             shortcut: 'M',
329
             shortcutAttr: 'mutePopover',
381
             shortcutAttr: 'mutePopover',
330
             shortcutFunc() {
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
                 APP.conference.toggleAudioMuted();
390
                 APP.conference.toggleAudioMuted();
333
             },
391
             },
334
             shortcutDescription: 'keyboardShortcuts.mute',
392
             shortcutDescription: 'keyboardShortcuts.mute',
351
             enabled: true,
409
             enabled: true,
352
             id: 'toolbar_button_raisehand',
410
             id: 'toolbar_button_raisehand',
353
             onClick() {
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
                 APP.conference.maybeToggleRaisedHand();
420
                 APP.conference.maybeToggleRaisedHand();
356
             },
421
             },
357
             shortcut: 'R',
422
             shortcut: 'R',
358
             shortcutAttr: 'raiseHandPopover',
423
             shortcutAttr: 'raiseHandPopover',
359
             shortcutDescription: 'keyboardShortcuts.raiseHand',
424
             shortcutDescription: 'keyboardShortcuts.raiseHand',
360
             shortcutFunc() {
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
                 APP.conference.maybeToggleRaisedHand();
433
                 APP.conference.maybeToggleRaisedHand();
363
             },
434
             },
364
             tooltipKey: 'toolbar.raiseHand'
435
             tooltipKey: 'toolbar.raiseHand'
386
             enabled: true,
457
             enabled: true,
387
             id: 'toolbar_button_settings',
458
             id: 'toolbar_button_settings',
388
             onClick() {
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
                 APP.UI.emitEvent(UIEvents.TOGGLE_SETTINGS);
463
                 APP.UI.emitEvent(UIEvents.TOGGLE_SETTINGS);
391
             },
464
             },
392
             sideContainerId: 'settings_container',
465
             sideContainerId: 'settings_container',
401
             enabled: true,
474
             enabled: true,
402
             id: 'toolbar_button_sharedvideo',
475
             id: 'toolbar_button_sharedvideo',
403
             onClick() {
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
                 APP.UI.emitEvent(UIEvents.SHARED_VIDEO_CLICKED);
486
                 APP.UI.emitEvent(UIEvents.SHARED_VIDEO_CLICKED);
406
             },
487
             },
407
             popups: [
488
             popups: [

+ 25
- 11
react/features/video-quality/components/VideoQualityDialog.web.js View File

4
 import { connect } from 'react-redux';
4
 import { connect } from 'react-redux';
5
 
5
 
6
 import {
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
 } from '../../analytics';
9
 } from '../../analytics';
13
 import {
10
 import {
11
+    VIDEO_QUALITY_LEVELS,
14
     setAudioOnly,
12
     setAudioOnly,
15
-    setReceiveVideoQuality,
16
-    VIDEO_QUALITY_LEVELS
13
+    setReceiveVideoQuality
17
 } from '../../base/conference';
14
 } from '../../base/conference';
18
 import { translate } from '../../base/i18n';
15
 import { translate } from '../../base/i18n';
19
 import JitsiMeetJS from '../../base/lib-jitsi-meet';
16
 import JitsiMeetJS from '../../base/lib-jitsi-meet';
26
     LOW
23
     LOW
27
 } = VIDEO_QUALITY_LEVELS;
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
  * Implements a React {@link Component} which displays a dialog with a slider
43
  * Implements a React {@link Component} which displays a dialog with a slider
31
  * for selecting a new receive video quality.
44
  * for selecting a new receive video quality.
255
      * @returns {void}
268
      * @returns {void}
256
      */
269
      */
257
     _enableAudioOnly() {
270
     _enableAudioOnly() {
258
-        sendAnalyticsEvent(TOOLBAR_AUDIO_ONLY_ENABLED);
271
+        sendAnalytics(createEvent('audio.only'));
259
         logger.log('Video quality: audio only enabled');
272
         logger.log('Video quality: audio only enabled');
260
         this.props.dispatch(setAudioOnly(true));
273
         this.props.dispatch(setAudioOnly(true));
261
     }
274
     }
262
 
275
 
263
     /**
276
     /**
277
+     * Handles the action of the high definition video being selected.
264
      * Dispatches an action to receive high quality video from remote
278
      * Dispatches an action to receive high quality video from remote
265
      * participants.
279
      * participants.
266
      *
280
      *
268
      * @returns {void}
282
      * @returns {void}
269
      */
283
      */
270
     _enableHighDefinition() {
284
     _enableHighDefinition() {
271
-        sendAnalyticsEvent(TOOLBAR_VIDEO_QUALITY_HIGH);
285
+        sendAnalytics(createEvent('high'));
272
         logger.log('Video quality: high enabled');
286
         logger.log('Video quality: high enabled');
273
         this.props.dispatch(setReceiveVideoQuality(HIGH));
287
         this.props.dispatch(setReceiveVideoQuality(HIGH));
274
     }
288
     }
281
      * @returns {void}
295
      * @returns {void}
282
      */
296
      */
283
     _enableLowDefinition() {
297
     _enableLowDefinition() {
284
-        sendAnalyticsEvent(TOOLBAR_VIDEO_QUALITY_LOW);
298
+        sendAnalytics(createEvent('low'));
285
         logger.log('Video quality: low enabled');
299
         logger.log('Video quality: low enabled');
286
         this.props.dispatch(setReceiveVideoQuality(LOW));
300
         this.props.dispatch(setReceiveVideoQuality(LOW));
287
     }
301
     }
294
      * @returns {void}
308
      * @returns {void}
295
      */
309
      */
296
     _enableStandardDefinition() {
310
     _enableStandardDefinition() {
297
-        sendAnalyticsEvent(TOOLBAR_VIDEO_QUALITY_STANDARD);
311
+        sendAnalytics(createEvent('standard'));
298
         logger.log('Video quality: standard enabled');
312
         logger.log('Video quality: standard enabled');
299
         this.props.dispatch(setReceiveVideoQuality(STANDARD));
313
         this.props.dispatch(setReceiveVideoQuality(STANDARD));
300
     }
314
     }

Loading…
Cancel
Save