diff --git a/Makefile b/Makefile index 4c8d3c1e6..68e0b5a50 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ deploy-appbundle: $(BUILD_DIR)/device_selection_popup_bundle.min.map \ $(BUILD_DIR)/alwaysontop.min.js \ $(BUILD_DIR)/alwaysontop.min.map \ - $(OUTPUT_DIR)/analytics.js \ + $(OUTPUT_DIR)/analytics-ga.js \ $(DEPLOY_DIR) deploy-lib-jitsi-meet: diff --git a/analytics-ga.js b/analytics-ga.js new file mode 100644 index 000000000..c8a53f8aa --- /dev/null +++ b/analytics-ga.js @@ -0,0 +1,146 @@ +/* global ga */ + +(function(ctx) { + /** + * + */ + function Analytics() { + /* eslint-disable */ + + /** + * Google Analytics + */ + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (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) + })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); + ga('create', 'UA-319188-14', 'jit.si'); + ga('send', 'pageview'); + + /* eslint-enable */ + } + + /** + * Extracts the integer to use for a Google Analytics event's value field + * from a lib-jitsi-meet analytics event. + * @param {Object} event - The lib-jitsi-meet analytics event. + * @returns {Object} - The integer to use for the 'value' of a Google + * Analytics event. + * @private + */ + Analytics.prototype._extractAction = function(event) { + // Page events have a single 'name' field. + if (event.type === 'page') { + return event.name; + } + + // All other events have action, actionSubject, and source fields. All + // three fields are required, and the often jitsi-meet and + // lib-jitsi-meet use the same value when separate values are not + // necessary (i.e. event.action == event.actionSubject). + // Here we concatenate these three fields, but avoid adding the same + // value twice, because it would only make the GA event's action harder + // to read. + let action = event.action; + + if (event.actionSubject && event.actionSubject !== event.action) { + // Intentionally use string concatenation as analytics needs to + // work on IE but this file does not go through babel. For some + // reason disabling this globally for the file does not have an + // effect. + // eslint-disable-next-line prefer-template + action = event.actionSubject + '.' + action; + } + if (event.source && event.source !== event.action + && event.source !== event.action) { + // eslint-disable-next-line prefer-template + action = event.source + '.' + action; + } + + return action; + }; + + /** + * Extracts the integer to use for a Google Analytics event's value field + * from a lib-jitsi-meet analytics event. + * @param {Object} event - The lib-jitsi-meet analytics event. + * @returns {Object} - The integer to use for the 'value' of a Google + * Analytics event, or NaN if the lib-jitsi-meet event doesn't contain a + * suitable value. + * @private + */ + Analytics.prototype._extractValue = function(event) { + let value = event && event.attributes && event.attributes.value; + + // Try to extract an integer from the "value" attribute. + value = Math.round(parseFloat(value)); + + return value; + }; + + /** + * Extracts the string to use for a Google Analytics event's label field + * from a lib-jitsi-meet analytics event. + * @param {Object} event - The lib-jitsi-meet analytics event. + * @returns {string} - The string to use for the 'label' of a Google + * Analytics event. + * @private + */ + Analytics.prototype._extractLabel = function(event) { + let label = ''; + + // The label field is limited to 500B. We will concatenate all + // attributes of the event, except the user agent because it may be + // lengthy and is probably included from elsewhere. + for (const property in event.attributes) { + if (property !== 'permanent_user_agent' + && event.attributes.hasOwnProperty(property)) { + // eslint-disable-next-line prefer-template + label += property + '=' + event.attributes[property] + '&'; + } + } + + if (label.length > 0) { + label = label.slice(0, -1); + } + + return label; + }; + + /** + * This is the entry point of the API. The function sends an event to + * google analytics. The format of the event is described in + * AnalyticsAdapter in lib-jitsi-meet. + * @param {Object} event - the event in the format specified by + * lib-jitsi-meet. + */ + Analytics.prototype.sendEvent = function(event) { + if (!event) { + return; + } + + const gaEvent = { + 'eventCategory': 'jitsi-meet', + 'eventAction': this._extractAction(event), + 'eventLabel': this._extractLabel(event) + }; + const value = this._extractValue(event); + + if (!isNaN(value)) { + gaEvent.eventValue = value; + } + + ga('send', 'event', gaEvent); + }; + + if (typeof ctx.JitsiMeetJS === 'undefined') { + ctx.JitsiMeetJS = {}; + } + if (typeof ctx.JitsiMeetJS.app === 'undefined') { + ctx.JitsiMeetJS.app = {}; + } + if (typeof ctx.JitsiMeetJS.app.analyticsHandlers === 'undefined') { + ctx.JitsiMeetJS.app.analyticsHandlers = []; + } + ctx.JitsiMeetJS.app.analyticsHandlers.push(Analytics); +})(window); +/* eslint-enable prefer-template */ diff --git a/analytics.js b/analytics.js deleted file mode 100644 index 00db3f671..000000000 --- a/analytics.js +++ /dev/null @@ -1,47 +0,0 @@ -/* global ga */ - -(function(ctx) { - /** - * - */ - function Analytics() { - /* eslint-disable */ - - /** - * Google Analytics - */ - (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ - (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) - })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); - ga('create', 'UA-319188-14', 'jit.si'); - ga('send', 'pageview'); - - /* eslint-enable */ - } - - Analytics.prototype.sendEvent = function(action, data) { - // empty label if missing value for it and add the value, - // the value should be integer or null - let value = data.value; - - value = value ? Math.round(parseFloat(value)) : null; - const label = data.label || ''; - - // Intentionally use string concatenation as analytics needs to work on - // IE but this file does not go through babel. - // eslint-disable-next-line prefer-template - ga('send', 'event', 'jit.si', action + '.' + data.browserName, - label, value); - }; - - if (typeof ctx.JitsiMeetJS === 'undefined') { - ctx.JitsiMeetJS = {}; - } - if (typeof ctx.JitsiMeetJS.app === 'undefined') { - ctx.JitsiMeetJS.app = {}; - } - if (typeof ctx.JitsiMeetJS.app.analyticsHandlers === 'undefined') { - ctx.JitsiMeetJS.app.analyticsHandlers = []; - } - ctx.JitsiMeetJS.app.analyticsHandlers.push(Analytics); -})(window); diff --git a/conference.js b/conference.js index 081d2e094..57ecec6de 100644 --- a/conference.js +++ b/conference.js @@ -16,19 +16,13 @@ import UIUtil from './modules/UI/util/UIUtil'; import * as JitsiMeetConferenceEvents from './ConferenceEvents'; import { - CONFERENCE_AUDIO_INITIALLY_MUTED, - CONFERENCE_SHARING_DESKTOP_START, - CONFERENCE_SHARING_DESKTOP_STOP, - CONFERENCE_VIDEO_INITIALLY_MUTED, - DEVICE_LIST_CHANGED_AUDIO_MUTED, - DEVICE_LIST_CHANGED_VIDEO_MUTED, - SELECT_PARTICIPANT_FAILED, - SETTINGS_CHANGE_DEVICE_AUDIO_OUT, - SETTINGS_CHANGE_DEVICE_AUDIO_IN, - SETTINGS_CHANGE_DEVICE_VIDEO, - STREAM_SWITCH_DELAY, + createDeviceChangedEvent, + createScreenSharingEvent, + createSelectParticipantFailedEvent, + createStreamSwitchDelayEvent, + createTrackMutedEvent, initAnalytics, - sendAnalyticsEvent + sendAnalytics } from './react/features/analytics'; import EventEmitter from 'events'; @@ -741,14 +735,13 @@ export default { }) .then(([ tracks, con ]) => { tracks.forEach(track => { - if (track.isAudioTrack() && this.isLocalAudioMuted()) { - sendAnalyticsEvent(CONFERENCE_AUDIO_INITIALLY_MUTED); - logger.log('Audio mute: initially muted'); - track.mute(); - } else if (track.isVideoTrack() - && this.isLocalVideoMuted()) { - sendAnalyticsEvent(CONFERENCE_VIDEO_INITIALLY_MUTED); - logger.log('Video mute: initially muted'); + if ((track.isAudioTrack() && this.isLocalAudioMuted()) + || (track.isVideoTrack() && this.isLocalVideoMuted())) { + const mediaType = track.getType(); + + sendAnalytics( + createTrackMutedEvent(mediaType, 'initial mute')); + logger.log(`${mediaType} mute: initially muted.`); track.mute(); } }); @@ -1453,8 +1446,9 @@ export default { promise = createLocalTracksF({ devices: [ 'video' ] }) .then(([ stream ]) => this.useVideoStream(stream)) .then(() => { - sendAnalyticsEvent(CONFERENCE_SHARING_DESKTOP_STOP); - logger.log('switched back to local video'); + sendAnalytics(createScreenSharingEvent('stopped')); + logger.log('Screen sharing stopped, switching to video.'); + if (!this.localVideo && wasVideoMuted) { return Promise.reject('No local video to be muted!'); } else if (wasVideoMuted && this.localVideo) { @@ -1609,7 +1603,7 @@ export default { }, /** - * Tries to switch to the screenshairng mode by disposing camera stream and + * Tries to switch to the screensharing mode by disposing camera stream and * replacing it with a desktop one. * * @param {Object} [options] - Screen sharing options that will be passed to @@ -1632,8 +1626,8 @@ export default { .then(stream => this.useVideoStream(stream)) .then(() => { this.videoSwitchInProgress = false; - sendAnalyticsEvent(CONFERENCE_SHARING_DESKTOP_START); - logger.log('sharing local desktop'); + sendAnalytics(createScreenSharingEvent('started')); + logger.log('Screen sharing started'); }) .catch(error => { this.videoSwitchInProgress = false; @@ -1928,7 +1922,7 @@ export default { room.selectParticipant(id); } catch (e) { - sendAnalyticsEvent(SELECT_PARTICIPANT_FAILED); + sendAnalytics(createSelectParticipantFailedEvent(e)); reportError(e); } }); @@ -2152,22 +2146,12 @@ export default { APP.UI.addListener( UIEvents.RESOLUTION_CHANGED, (id, oldResolution, newResolution, delay) => { - const logObject = { - id: 'resolution_change', - participant: id, - oldValue: oldResolution, - newValue: newResolution, - delay - }; - - room.sendApplicationLog(JSON.stringify(logObject)); - - // We only care about the delay between simulcast streams. - // Longer delays will be caused by something else and will just - // poison the data. - if (delay < 2000) { - sendAnalyticsEvent(STREAM_SWITCH_DELAY, { value: delay }); - } + sendAnalytics(createStreamSwitchDelayEvent( + { + 'old_resolution': oldResolution, + 'new_resolution': newResolution, + value: delay + })); }); /* eslint-enable max-params */ @@ -2193,7 +2177,7 @@ export default { cameraDeviceId => { const videoWasMuted = this.isLocalVideoMuted(); - sendAnalyticsEvent(SETTINGS_CHANGE_DEVICE_VIDEO); + sendAnalytics(createDeviceChangedEvent('video', 'input')); createLocalTracksF({ devices: [ 'video' ], cameraDeviceId, @@ -2232,7 +2216,7 @@ export default { micDeviceId => { const audioWasMuted = this.isLocalAudioMuted(); - sendAnalyticsEvent(SETTINGS_CHANGE_DEVICE_AUDIO_IN); + sendAnalytics(createDeviceChangedEvent('audio', 'input')); createLocalTracksF({ devices: [ 'audio' ], cameraDeviceId: null, @@ -2262,7 +2246,7 @@ export default { APP.UI.addListener( UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, audioOutputDeviceId => { - sendAnalyticsEvent(SETTINGS_CHANGE_DEVICE_AUDIO_OUT); + sendAnalytics(createDeviceChangedEvent('audio', 'output')); APP.settings.setAudioOutputDeviceId(audioOutputDeviceId) .then(() => logger.log('changed audio output device')) .catch(err => { @@ -2528,7 +2512,9 @@ export default { // If audio was muted before, or we unplugged current device // and selected new one, then mute new audio track. if (audioWasMuted) { - sendAnalyticsEvent(DEVICE_LIST_CHANGED_AUDIO_MUTED); + sendAnalytics(createTrackMutedEvent( + 'audio', + 'device list changed')); logger.log('Audio mute: device list changed'); muteLocalAudio(true); } @@ -2536,7 +2522,9 @@ export default { // If video was muted before, or we unplugged current device // and selected new one, then mute new video track. if (!this.isSharingScreen && videoWasMuted) { - sendAnalyticsEvent(DEVICE_LIST_CHANGED_VIDEO_MUTED); + sendAnalytics(createTrackMutedEvent( + 'video', + 'device list changed')); logger.log('Video mute: device list changed'); muteLocalVideo(true); } @@ -2622,37 +2610,6 @@ export default { } }, - /** - * Log event to callstats and analytics. - * @param {string} name the event name - * @param {int} value the value (it's int because google analytics supports - * only int). - * @param {string} label short text which provides more info about the event - * which allows to distinguish between few event cases of the same name - * NOTE: Should be used after conference.init - */ - logEvent(name, value, label) { - sendAnalyticsEvent(name, { - value, - label - }); - if (room) { - room.sendApplicationLog(JSON.stringify({ name, - value, - label })); - } - }, - - /** - * Methods logs an application event given in the JSON format. - * @param {string} logJSON an event to be logged in JSON format - */ - logJSON(logJSON) { - if (room) { - room.sendApplicationLog(logJSON); - } - }, - /** * Disconnect from the conference and optionally request user feedback. * @param {boolean} [requestFeedback=false] if user feedback should be diff --git a/config.js b/config.js index e6f31aef7..0b0c63b97 100644 --- a/config.js +++ b/config.js @@ -307,21 +307,24 @@ var config = { // backToP2PDelay: 5 }, + // A list of scripts to load as lib-jitsi-meet "analytics handlers". + // analyticsScriptUrls: [ + // "libs/analytics-ga.js", // google-analytics + // "https://example.com/my-custom-analytics.js" + // ], // Information about the jitsi-meet instance we are connecting to, including // the user region as seen by the server. - // - deploymentInfo: { // shard: "shard1", // region: "europe", // userRegion: "asia" } + // List of undocumented settings used in jitsi-meet /** alwaysVisibleToolbar - analyticsScriptUrls autoEnableDesktopSharing autoRecord autoRecordToken diff --git a/modules/API/API.js b/modules/API/API.js index 281ab70cc..07943baa5 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -3,9 +3,8 @@ import * as JitsiMeetConferenceEvents from '../../ConferenceEvents'; import { parseJWTFromURLParams } from '../../react/features/base/jwt'; import { - API_TOGGLE_AUDIO, - API_TOGGLE_VIDEO, - sendAnalyticsEvent + createApiEvent, + sendAnalytics } from '../../react/features/analytics'; import { getJitsiMeetTransport } from '../transport'; @@ -56,25 +55,48 @@ let videoAvailable = true; */ function initCommands() { commands = { - 'display-name': - APP.conference.changeLocalDisplayName.bind(APP.conference), + 'display-name': displayName => { + sendAnalytics(createApiEvent('display.name.changed')); + APP.conference.changeLocalDisplayName(displayName); + }, 'toggle-audio': () => { - sendAnalyticsEvent(API_TOGGLE_AUDIO); + sendAnalytics(createApiEvent('toggle-audio')); logger.log('Audio toggle: API command received'); APP.conference.toggleAudioMuted(false /* no UI */); }, 'toggle-video': () => { - sendAnalyticsEvent(API_TOGGLE_VIDEO); + sendAnalytics(createApiEvent('toggle-video')); logger.log('Video toggle: API command received'); APP.conference.toggleVideoMuted(false /* no UI */); }, - 'toggle-film-strip': APP.UI.toggleFilmstrip, - 'toggle-chat': APP.UI.toggleChat, - 'toggle-contact-list': APP.UI.toggleContactList, - 'toggle-share-screen': toggleScreenSharing, - 'video-hangup': () => APP.conference.hangup(true), - 'email': APP.conference.changeLocalEmail, - 'avatar-url': APP.conference.changeLocalAvatarUrl + 'toggle-film-strip': () => { + sendAnalytics(createApiEvent('film.strip.toggled')); + APP.UI.toggleFilmstrip(); + }, + 'toggle-chat': () => { + sendAnalytics(createApiEvent('chat.toggled')); + APP.UI.toggleChat(); + }, + 'toggle-contact-list': () => { + sendAnalytics(createApiEvent('contact.list.toggled')); + APP.UI.toggleContactList(); + }, + 'toggle-share-screen': () => { + sendAnalytics(createApiEvent('screen.sharing.toggled')); + toggleScreenSharing(); + }, + 'video-hangup': () => { + sendAnalytics(createApiEvent('video.hangup')); + APP.conference.hangup(true); + }, + 'email': email => { + sendAnalytics(createApiEvent('email.changed')); + APP.conference.changeLocalEmail(email); + }, + 'avatar-url': avatarUrl => { + sendAnalytics(createApiEvent('avatar.url.changed')); + APP.conference.changeLocalAvatarUrl(avatarUrl); + } }; transport.on('event', ({ data, name }) => { if (name && commands[name]) { diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 62eabaf2e..d661ecd5c 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -142,6 +142,32 @@ UI.toggleFullScreen = function() { UIUtil.isFullScreen() ? UIUtil.exitFullScreen() : UIUtil.enterFullScreen(); }; +/** + * Indicates if we're currently in full screen mode. + * + * @return {boolean} {true} to indicate that we're currently in full screen + * mode, {false} otherwise + */ +UI.isFullScreen = function() { + return UIUtil.isFullScreen(); +}; + +/** + * Returns true if the etherpad window is currently visible. + * @returns {Boolean} - true if the etherpad window is currently visible. + */ +UI.isEtherpadVisible = function() { + return Boolean(etherpadManager && etherpadManager.isVisible()); +}; + +/** + * Returns true if there is a shared video which is being shown (?). + * @returns {boolean} - true if there is a shared video which is being shown. + */ +UI.isSharedVideoShown = function() { + return Boolean(sharedVideoManager && sharedVideoManager.isSharedVideoShown); +}; + /** * Notify user that server has shut down. */ diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js index e8aac6f96..2151d1b99 100644 --- a/modules/UI/recording/Recording.js +++ b/modules/UI/recording/Recording.js @@ -24,11 +24,9 @@ import { JitsiRecordingStatus } from '../../../react/features/base/lib-jitsi-meet'; import { - RECORDING_CANCELED, - RECORDING_CLICKED, - RECORDING_STARTED, - RECORDING_STOPPED, - sendAnalyticsEvent + createToolbarEvent, + createRecordingDialogEvent, + sendAnalytics } from '../../../react/features/analytics'; import { setToolboxEnabled } from '../../../react/features/toolbox'; import { setNotificationsEnabled } from '../../../react/features/notifications'; @@ -452,12 +450,13 @@ const Recording = { }, // checks whether recording is enabled and whether we have params - // to start automatically recording + // to start automatically recording (XXX: No, it doesn't do that). checkAutoRecord() { if (_isRecordingButtonEnabled && config.autoRecord) { this.predefinedToken = UIUtil.escapeHtml(config.autoRecordToken); - this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED, - this.predefinedToken); + this.eventEmitter.emit( + UIEvents.RECORDING_TOGGLED, + { token: this.predefinedToken }); } }, @@ -467,11 +466,16 @@ const Recording = { * @returns {void} */ _onToolbarButtonClick() { + sendAnalytics(createToolbarEvent( + 'recording.button', + { + 'dialog_present': Boolean(dialog) + })); + if (dialog) { return; } - sendAnalyticsEvent(RECORDING_CLICKED); switch (this.currentState) { case JitsiRecordingStatus.ON: case JitsiRecordingStatus.RETRYING: @@ -479,7 +483,13 @@ const Recording = { _showStopRecordingPrompt(this.recordingType).then( () => { this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED); - sendAnalyticsEvent(RECORDING_STOPPED); + + // The confirm button on the stop recording dialog was + // clicked + sendAnalytics( + createRecordingDialogEvent( + 'stop', + 'confirm.button')); }, () => {}); // eslint-disable-line no-empty-function break; @@ -492,21 +502,32 @@ const Recording = { this.eventEmitter.emit( UIEvents.RECORDING_TOGGLED, { streamId }); - sendAnalyticsEvent(RECORDING_STARTED); + + // The confirm button on the start recording dialog was + // clicked + sendAnalytics( + createRecordingDialogEvent( + 'start', + 'confirm.button')); }) .catch(reason => { if (reason === APP.UI.messageHandler.CANCEL) { - sendAnalyticsEvent(RECORDING_CANCELED); + // The cancel button on the start recording dialog was + // clicked + sendAnalytics( + createRecordingDialogEvent( + 'start', + 'cancel.button')); } else { logger.error(reason); } }); } else { + // Note that we only fire analytics events for Jibri. if (this.predefinedToken) { this.eventEmitter.emit( UIEvents.RECORDING_TOGGLED, { token: this.predefinedToken }); - sendAnalyticsEvent(RECORDING_STARTED); return; } @@ -515,12 +536,9 @@ const Recording = { this.eventEmitter.emit( UIEvents.RECORDING_TOGGLED, { token }); - sendAnalyticsEvent(RECORDING_STARTED); }) .catch(reason => { - if (reason === APP.UI.messageHandler.CANCEL) { - sendAnalyticsEvent(RECORDING_CANCELED); - } else { + if (reason !== APP.UI.messageHandler.CANCEL) { logger.error(reason); } }); diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index 94c5132a1..b8290ed4d 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -11,15 +11,8 @@ import LargeContainer from '../videolayout/LargeContainer'; import Filmstrip from '../videolayout/Filmstrip'; import { - SHARED_VIDEO_ALREADY_SHARED, - SHARED_VIDEO_AUDIO_MUTED, - SHARED_VIDEO_AUDIO_UNMUTED, - SHARED_VIDEO_CANCELED, - SHARED_VIDEO_PAUSED, - SHARED_VIDEO_STARTED, - SHARED_VIDEO_STOPPED, - SHARED_VIDEO_VOLUME_CHANGED, - sendAnalyticsEvent + createSharedVideoEvent as createEvent, + sendAnalytics } from '../../../react/features/analytics'; import { participantJoined, @@ -95,11 +88,11 @@ export default class SharedVideoManager { url => { this.emitter.emit( UIEvents.UPDATE_SHARED_VIDEO, url, 'start'); - sendAnalyticsEvent(SHARED_VIDEO_STARTED); + sendAnalytics(createEvent('started')); }, err => { logger.log('SHARED VIDEO CANCELED', err); - sendAnalyticsEvent(SHARED_VIDEO_CANCELED); + sendAnalytics(createEvent('canceled')); } ); @@ -119,7 +112,7 @@ export default class SharedVideoManager { } this.emitter.emit( UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop'); - sendAnalyticsEvent(SHARED_VIDEO_STOPPED); + sendAnalytics(createEvent('stopped')); }, () => {}); // eslint-disable-line no-empty-function } else { @@ -127,7 +120,7 @@ export default class SharedVideoManager { descriptionKey: 'dialog.alreadySharedVideoMsg', titleKey: 'dialog.alreadySharedVideoTitle' }); - sendAnalyticsEvent(SHARED_VIDEO_ALREADY_SHARED); + sendAnalytics(createEvent('already.shared')); } } @@ -236,7 +229,7 @@ export default class SharedVideoManager { // eslint-disable-next-line eqeqeq } else if (event.data == YT.PlayerState.PAUSED) { self.smartAudioUnmute(); - sendAnalyticsEvent(SHARED_VIDEO_PAUSED); + sendAnalytics(createEvent('paused')); } // eslint-disable-next-line eqeqeq self.fireSharedVideoEvent(event.data == YT.PlayerState.PAUSED); @@ -268,7 +261,12 @@ export default class SharedVideoManager { } else if (event.data.volume <= 0 || event.data.muted) { self.smartAudioUnmute(); } - sendAnalyticsEvent(SHARED_VIDEO_VOLUME_CHANGED); + sendAnalytics(createEvent( + 'volume.changed', + { + volume: event.data.volume, + muted: event.data.muted + })); }; window.onPlayerReady = function(event) { @@ -434,8 +432,8 @@ export default class SharedVideoManager { } /** - * Updates video, if its not playing and needs starting or - * if its playing and needs to be paysed + * Updates video, if it's not playing and needs starting or if it's playing + * and needs to be paused. * @param id the id of the sender of the command * @param url the video url * @param attributes @@ -574,7 +572,7 @@ export default class SharedVideoManager { if (APP.conference.isLocalAudioMuted() && !this.mutedWithUserInteraction && !this.isSharedVideoVolumeOn()) { - sendAnalyticsEvent(SHARED_VIDEO_AUDIO_UNMUTED); + sendAnalytics(createEvent('audio.unmuted')); logger.log('Shared video: audio unmuted'); this.emitter.emit(UIEvents.AUDIO_MUTED, false, false); this.showMicMutedPopup(false); @@ -588,7 +586,7 @@ export default class SharedVideoManager { smartAudioMute() { if (!APP.conference.isLocalAudioMuted() && this.isSharedVideoVolumeOn()) { - sendAnalyticsEvent(SHARED_VIDEO_AUDIO_MUTED); + sendAnalytics(createEvent('audio.muted')); logger.log('Shared video: audio muted'); this.emitter.emit(UIEvents.AUDIO_MUTED, true, false); this.showMicMutedPopup(true); diff --git a/modules/UI/side_pannels/profile/Profile.js b/modules/UI/side_pannels/profile/Profile.js index 2669b5294..439ee0340 100644 --- a/modules/UI/side_pannels/profile/Profile.js +++ b/modules/UI/side_pannels/profile/Profile.js @@ -4,9 +4,8 @@ import UIEvents from '../../../../service/UI/UIEvents'; import Settings from '../../../settings/Settings'; import { - AUTHENTICATE_LOGIN_CLICKED, - AUTHENTICATE_LOGOUT_CLICKED, - sendAnalyticsEvent + createProfilePanelButtonEvent, + sendAnalytics } from '../../../../react/features/analytics'; const sidePanelsContainerId = 'sideToolbarContainer'; @@ -95,7 +94,7 @@ export default { * */ function loginClicked() { - sendAnalyticsEvent(AUTHENTICATE_LOGIN_CLICKED); + sendAnalytics(createProfilePanelButtonEvent('login.button')); emitter.emit(UIEvents.AUTH_CLICKED); } @@ -108,7 +107,7 @@ export default { const titleKey = 'dialog.logoutTitle'; const msgKey = 'dialog.logoutQuestion'; - sendAnalyticsEvent(AUTHENTICATE_LOGOUT_CLICKED); + sendAnalytics(createProfilePanelButtonEvent('logout.button')); // Ask for confirmation APP.UI.messageHandler.openTwoButtonDialog({ diff --git a/modules/UI/videolayout/Filmstrip.js b/modules/UI/videolayout/Filmstrip.js index de4a9c988..a41849b67 100644 --- a/modules/UI/videolayout/Filmstrip.js +++ b/modules/UI/videolayout/Filmstrip.js @@ -6,8 +6,9 @@ import UIEvents from '../../../service/UI/UIEvents'; import UIUtil from '../util/UIUtil'; import { - TOOLBAR_FILMSTRIP_TOGGLED, - sendAnalyticsEvent + createShortcutEvent, + createToolbarEvent, + sendAnalytics } from '../../../react/features/analytics'; const Filmstrip = { @@ -75,8 +76,18 @@ const Filmstrip = { // Firing the event instead of executing toggleFilmstrip method because // it's important to hide the filmstrip by UI.toggleFilmstrip in order // to correctly resize the video area. - $('#toggleFilmstripButton').on('click', - () => this.eventEmitter.emit(UIEvents.TOGGLE_FILMSTRIP)); + $('#toggleFilmstripButton').on( + 'click', + () => { + // The 'enable' parameter is set to true if the action results + // in the filmstrip being hidden. + sendAnalytics(createToolbarEvent( + 'toggle.filmstrip.button', + { + enable: this.isFilmstripVisible() + })); + this.eventEmitter.emit(UIEvents.TOGGLE_FILMSTRIP); + }); this._registerToggleFilmstripShortcut(); }, @@ -94,7 +105,14 @@ const Filmstrip = { // Firing the event instead of executing toggleFilmstrip method because // it's important to hide the filmstrip by UI.toggleFilmstrip in order // to correctly resize the video area. - const handler = () => this.eventEmitter.emit(UIEvents.TOGGLE_FILMSTRIP); + const handler = () => { + sendAnalytics(createShortcutEvent( + 'toggle.filmstrip', + { + enable: this.isFilmstripVisible() + })); + this.eventEmitter.emit(UIEvents.TOGGLE_FILMSTRIP); + }; APP.keyboardshortcut.registerShortcut( shortcut, @@ -129,50 +147,43 @@ const Filmstrip = { }, /** - * Toggles the visibility of the filmstrip. + * Toggles the visibility of the filmstrip, or sets it to a specific value + * if the 'visible' parameter is specified. * * @param visible optional {Boolean} which specifies the desired visibility * of the filmstrip. If not specified, the visibility will be flipped * (i.e. toggled); otherwise, the visibility will be set to the specified * value. - * @param {Boolean} sendAnalytics - True to send an analytics event. The - * default value is true. * * Note: * This method shouldn't be executed directly to hide the filmstrip. * It's important to hide the filmstrip with UI.toggleFilmstrip in order * to correctly resize the video area. */ - toggleFilmstrip(visible, sendAnalytics = true) { - const isVisibleDefined = typeof visible === 'boolean'; + toggleFilmstrip(visible) { + const wasFilmstripVisible = this.isFilmstripVisible(); - if (!isVisibleDefined) { - // eslint-disable-next-line no-param-reassign - visible = this.isFilmstripVisible(); - } else if (this.isFilmstripVisible() === visible) { + // If 'visible' is defined and matches the current state, we have + // nothing to do. Otherwise (regardless of whether 'visible' is defined) + // we need to toggle the state. + if (visible === wasFilmstripVisible) { return; } - if (sendAnalytics) { - sendAnalyticsEvent(TOOLBAR_FILMSTRIP_TOGGLED); - } + this.filmstrip.toggleClass('hidden'); - if (visible) { + if (wasFilmstripVisible) { this.showMenuUpIcon(); } else { this.showMenuDownIcon(); } - // Emit/fire UIEvents.TOGGLED_FILMSTRIP. - const eventEmitter = this.eventEmitter; - const isFilmstripVisible = this.isFilmstripVisible(); - - if (eventEmitter) { - eventEmitter.emit( + if (this.eventEmitter) { + this.eventEmitter.emit( UIEvents.TOGGLED_FILMSTRIP, - this.isFilmstripVisible()); + !wasFilmstripVisible); } - APP.store.dispatch(setFilmstripVisibility(isFilmstripVisible)); + APP.store.dispatch(setFilmstripVisibility(!wasFilmstripVisible)); }, /** diff --git a/modules/keyboardshortcut/keyboardshortcut.js b/modules/keyboardshortcut/keyboardshortcut.js index 09472da87..28c7a499f 100644 --- a/modules/keyboardshortcut/keyboardshortcut.js +++ b/modules/keyboardshortcut/keyboardshortcut.js @@ -2,11 +2,10 @@ import { toggleDialog } from '../../react/features/base/dialog'; import { - SHORTCUT_HELP, - SHORTCUT_SPEAKER_STATS_CLICKED, - SHORTCUT_TALK_CLICKED, - SHORTCUT_TALK_RELEASED, - sendAnalyticsEvent + ACTION_SHORTCUT_PRESSED as PRESSED, + ACTION_SHORTCUT_RELEASED as RELEASED, + createShortcutEvent, + sendAnalytics } from '../../react/features/analytics'; import { KeyboardShortcutsDialog } from '../../react/features/keyboard-shortcuts'; @@ -72,8 +71,10 @@ const KeyboardShortcut = { || $(':focus').is('textarea'))) { if (this._getKeyboardKey(e).toUpperCase() === ' ') { if (APP.conference.isLocalAudioMuted()) { - sendAnalyticsEvent(SHORTCUT_TALK_RELEASED); - logger.log('Talk shortcut released'); + sendAnalytics(createShortcutEvent( + 'push.to.talk', + PRESSED)); + logger.log('Talk shortcut pressed'); APP.conference.muteAudio(false); } } @@ -93,7 +94,7 @@ const KeyboardShortcut = { * Registers a new shortcut. * * @param shortcutChar the shortcut character triggering the action - * @param shortcutAttr the "shortcut" html element attribute mappring an + * @param shortcutAttr the "shortcut" html element attribute mapping an * element to this shortcut and used to show the shortcut character on the * element tooltip * @param exec the function to be executed when the shortcut is pressed @@ -175,7 +176,7 @@ const KeyboardShortcut = { */ _initGlobalShortcuts() { this.registerShortcut('?', null, () => { - sendAnalyticsEvent(SHORTCUT_HELP); + sendAnalytics(createShortcutEvent('help')); APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, { shortcutDescriptions: _shortcutsHelp })); @@ -184,15 +185,15 @@ const KeyboardShortcut = { // register SPACE shortcut in two steps to insure visibility of help // message this.registerShortcut(' ', null, () => { - sendAnalyticsEvent(SHORTCUT_TALK_CLICKED); - logger.log('Talk shortcut pressed'); + sendAnalytics(createShortcutEvent('push.to.talk', RELEASED)); + logger.log('Talk shortcut released'); APP.conference.muteAudio(true); }); this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk'); if (!interfaceConfig.filmStripOnly) { this.registerShortcut('T', null, () => { - sendAnalyticsEvent(SHORTCUT_SPEAKER_STATS_CLICKED); + sendAnalytics(createShortcutEvent('speaker.stats')); APP.store.dispatch(toggleDialog(SpeakerStats, { conference: APP.conference })); diff --git a/modules/util/JitsiMeetLogStorage.js b/modules/util/JitsiMeetLogStorage.js index f56a6f2a1..e544917ba 100644 --- a/modules/util/JitsiMeetLogStorage.js +++ b/modules/util/JitsiMeetLogStorage.js @@ -37,20 +37,20 @@ export default class JitsiMeetLogStorage { return; } - let logJSON = `{"log${this.counter}":"\n`; + let logMessage = `{"log${this.counter}":"\n`; for (let i = 0, len = logEntries.length; i < len; i++) { const logEntry = logEntries[i]; if (typeof logEntry === 'object') { // Aggregated message - logJSON += `(${logEntry.count}) ${logEntry.text}\n`; + logMessage += `(${logEntry.count}) ${logEntry.text}\n`; } else { // Regular message - logJSON += `${logEntry}\n`; + logMessage += `${logEntry}\n`; } } - logJSON += '"}'; + logMessage += '"}'; this.counter += 1; @@ -58,11 +58,11 @@ export default class JitsiMeetLogStorage { // on the way that could be uninitialized if the storeLogs // attempt would be made very early (which is unlikely) try { - APP.conference.logJSON(logJSON); + APP.conference.room.sendApplicationLog(logMessage); } catch (error) { // NOTE console is intentional here console.error( - 'Failed to store the logs: ', logJSON, error); + 'Failed to store the logs: ', logMessage, error); } } } diff --git a/package-lock.json b/package-lock.json index 7d7d6c0ac..699f6405f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6386,7 +6386,7 @@ } }, "lib-jitsi-meet": { - "version": "github:jitsi/lib-jitsi-meet#c7d6d158b9ab87f47b2bb8484565bcb17e687f7e", + "version": "github:jitsi/lib-jitsi-meet#515374c8d383cb17df8ed76427e6f0fb5ea6ff1e", "requires": { "async": "0.9.0", "current-executing-script": "0.1.3", diff --git a/package.json b/package.json index 51cb97c50..5ed48ddf0 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "jquery-i18next": "1.2.0", "js-md5": "0.6.1", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#c7d6d158b9ab87f47b2bb8484565bcb17e687f7e", + "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#515374c8d383cb17df8ed76427e6f0fb5ea6ff1e", "lodash": "4.17.4", "moment": "2.19.4", "nuclear-js": "1.4.0", diff --git a/react/features/analytics/AnalyticsEvents.js b/react/features/analytics/AnalyticsEvents.js index d6e6dc660..2fe0c9aca 100644 --- a/react/features/analytics/AnalyticsEvents.js +++ b/react/features/analytics/AnalyticsEvents.js @@ -1,663 +1,461 @@ /** - * The target of a pin or unpin event was the local participant. - * - * Known full event names: - * pinned.local - * unpinned.local - * - * @type {String} + * The constant for the event type 'track'. + * TODO: keep these constants in a single place. Can we import them from + * lib-jitsi-meet's AnalyticsEvents somehow? + * @type {string} */ -export const _LOCAL = 'local'; +const TYPE_TRACK = 'track'; /** - * The target of a pin or unpin event was a remote participant. - * - * Known full event names: - * pinned.remote - * unpinned.remote - * - * @type {String} + * The constant for the event type 'UI' (User Interaction). + * TODO: keep these constants in a single place. Can we import them from + * lib-jitsi-meet's AnalyticsEvents somehow? + * @type {string} */ -export const _REMOTE = 'remote'; +const TYPE_UI = 'ui'; /** - * Audio mute toggled was triggered through the jitsi-meet api. + * The identifier for the "pinned" action. The local participant has pinned a + * participant to remain on large video. * * @type {String} */ -export const API_TOGGLE_AUDIO = 'api.toggle.audio'; +export const ACTION_PINNED = 'pinned'; /** - * Video mute toggling was triggered through the jitsi-meet api. + * The identifier for the "unpinned" action. The local participant has unpinned + * a participant so the participant doesn't remain permanently on local large + * video. * * @type {String} */ -export const API_TOGGLE_VIDEO = 'api.toggle.video'; +export const ACTION_UNPINNED = 'unpinned'; /** - * Audio only mode has been turned off. + * The identifier for the "pressed" action for shortcut events. This action + * means that a button was pressed (and not yet released). * * @type {String} */ -export const AUDIO_ONLY_DISABLED = 'audioonly.disabled'; +export const ACTION_SHORTCUT_PRESSED = 'pressed'; /** - * The login button in the profile pane was clicked. + * The identifier for the "released" action for shortcut events. This action + * means that a button which was previously pressed was released. * * @type {String} */ -export const AUTHENTICATE_LOGIN_CLICKED = 'authenticate.login.clicked'; +export const ACTION_SHORTCUT_RELEASED = 'released'; /** - * The logout button in the profile pane was clicked. + * The identifier for the "triggered" action for shortcut events. This action + * means that a button was pressed, and we don't care about whether it was + * released or will be released in the future. * * @type {String} */ -export const AUTHENTICATE_LOGOUT_CLICKED = 'authenticate.logout.clicked'; +export const ACTION_SHORTCUT_TRIGGERED = 'triggered'; /** - * Performing a mute or unmute event based on a callkit setMuted event. - * - * Known full event names: - * callkit.audio.muted - * callkit.audio.unmuted - * - * @type {String} + * The name of the keyboard shortcut or toolbar button for muting audio. */ -export const CALLKIT_AUDIO_ = 'callkit.audio'; +export const AUDIO_MUTE = 'audio.mute'; /** - * Toggling remote and local video display when entering or exiting backgrounded - * app state. - * - * @type {String} + * The name of the keyboard shortcut or toolbar button for muting video. */ -export const CALLKIT_BACKGROUND_VIDEO_MUTED = 'callkit.background.video.muted'; +export const VIDEO_MUTE = 'video.mute'; /** - * The local participant joined audio muted. + * Creates an event which indicates that a certain action was requested through + * the jitsi-meet API. * - * @type {String} + * @param {Object} action - The action which was requested through the + * jitsi-meet API. + * @param {Object} attributes - Attributes to attach to the event. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const CONFERENCE_AUDIO_INITIALLY_MUTED - = 'conference.audio.initiallyMuted'; +export const createApiEvent = function(action, attributes = {}) { + return { + action, + attributes, + source: 'jitsi-meet-api' + }; +}; /** - * The local participant has started desktop sharing. + * Creates an event which indicates that the audio-only mode has been turned + * off. * - * @type {String} + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const CONFERENCE_SHARING_DESKTOP_START - = 'conference.sharingDesktop.start'; +export const createAudioOnlyDisableEvent = function() { + return { + action: 'audio.only.disabled' + }; +}; /** - * The local participant was desktop sharing but has stopped. + * Creates an event which indicates that a device was changed. * - * @type {String} + * @param {string} mediaType - The media type of the device ('audio' or + * 'video'). + * @param {string} deviceType - The type of the device ('input' or 'output'). + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const CONFERENCE_SHARING_DESKTOP_STOP - = 'conference.sharingDesktop.stop'; +export const createDeviceChangedEvent = function(mediaType, deviceType) { + return { + action: 'device.changed', + attributes: { + 'device_type': deviceType, + 'media_type': mediaType + } + }; +}; /** - * The local participant joined video muted. + * Creates an event which specifies that the feedback dialog has been opened. * - * @type {String} + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const CONFERENCE_VIDEO_INITIALLY_MUTED - = 'conference.video.initiallyMuted'; +export const createFeedbackOpenEvent = function() { + return { + action: 'feedback.opened' + }; +}; /** - * The list of known input/output devices was changed and new audio input has - * been used and should start as muted. + * Creates an event which indicates that the invite dialog was closed. This is + * not a TYPE_UI event, since it is not necessarily the result of a user + * interaction. * - * @type {String} + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const DEVICE_LIST_CHANGED_AUDIO_MUTED = 'deviceListChanged.audio.muted'; +export const createInviteDialogClosedEvent = function() { + return { + action: 'invite.dialog.closed' + }; +}; /** - * The list of known devices was changed and new video input has been used - * and should start as muted. + * Creates a "page reload" event. * - * @type {String} + * @param {string} reason - The reason for the reload. + * @param {number} timeout - The timeout in seconds after which the page is + * scheduled to reload. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const DEVICE_LIST_CHANGED_VIDEO_MUTED = 'deviceListChanged.video.muted'; +export const createPageReloadScheduledEvent = function(reason, timeout) { + return { + action: 'page.reload.scheduled', + attributes: { + reason, + timeout + } + }; +}; /** - * The feedback dialog is displayed. + * Creates a "pinned" or "unpinned" event. * - * @type {String} + * @param {string} action - The action ("pinned" or "unpinned"). + * @param {string} participantId - The ID of the participant which was pinned. + * @param {Object} attributes - Attributes to attach to the event. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const FEEDBACK_OPEN = 'feedback.open'; +export const createPinnedEvent + = function(action, participantId, attributes) { + return { + type: TYPE_TRACK, + action, + actionSubject: 'participant', + objectType: 'participant', + objectId: participantId, + attributes + }; + }; /** - * Page reload overlay has been displayed. + * Creates an event which indicates that a button in the profile panel was + * clicked. * - * Properties: label: reason for reload - * - * @type {String} + * @param {string} buttonName - The name of the button. + * @param {Object} attributes - Attributes to attach to the event. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const PAGE_RELOAD = 'page.reload'; +export const createProfilePanelButtonEvent + = function(buttonName, attributes = {}) { + return { + action: 'clicked', + actionSubject: buttonName, + attributes, + source: 'profile.panel', + type: TYPE_UI + }; + }; /** - * The local participant has pinned a participant to remain on large video. + * Creates an event which indicates that a specific button on one of the + * recording-related dialogs was clicked. * - * Known full event names: - * pinned.local - * pinned.remote - * - * @type {String} + * @param {string} dialogName - The name of the dialog (e.g. 'start' or 'stop'). + * @param {string} buttonName - The name of the button (e.g. 'confirm' or + * 'cancel'). + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const PINNED_ = 'pinned'; +export const createRecordingDialogEvent = function(dialogName, buttonName) { + return { + action: 'clicked', + actionSubject: buttonName, + source: `${dialogName}.recording.dialog`, + type: TYPE_UI + }; +}; /** - * Recording start was attempted but the local user canceled the request. + * Creates an event which specifies that the "confirm" button on the remote + * mute dialog has been clicked. * - * @type {String} + * @param {string} participantId - The ID of the participant that was remotely + * muted. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const RECORDING_CANCELED = 'recording.canceled'; +export const createRemoteMuteConfirmedEvent = function(participantId) { + return { + action: 'clicked', + actionSubject: 'remote.mute.dialog.confirm.button', + attributes: { + 'participant_id': participantId + }, + source: 'remote.mute.dialog', + type: TYPE_UI + }; +}; /** - * Recording button has been clicked. + * Creates an event which indicates that one of the buttons in the "remote + * video menu" was clicked. * - * @type {String} + * @param {string} buttonName - The name of the button. + * @param {Object} attributes - Attributes to attach to the event. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const RECORDING_CLICKED = 'recording.clicked'; +export const createRemoteVideoMenuButtonEvent + = function(buttonName, attributes) { + return { + action: 'clicked', + actionSubject: buttonName, + attributes, + source: 'remote.video.menu', + type: TYPE_UI + }; + }; /** - * Recording has been started. + * Creates an event indicating that an action related to screen sharing + * occurred (e.g. it was started or stopped). * - * @type {String} + * @param {string} action - The action which occurred. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const RECORDING_STARTED = 'recording.started'; +export const createScreenSharingEvent = function(action) { + return { + action, + actionSubject: 'screen.sharing' + }; +}; /** - * Recording has been stopped by clicking the recording button. + * The local participant failed to send a "selected endpoint" message to the + * bridge. * - * @type {String} + * @param {Error} error - The error which caused the failure. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const RECORDING_STOPPED = 'recording.stopped'; +export const createSelectParticipantFailedEvent = function(error) { + const event = { + action: 'select.participant.failed' + }; + + if (error) { + event.error = error.toString(); + } + + return event; +}; /** - * Clicked on the button to kick a remote participant from the conference. + * Creates an event associated with the "shared video" feature. * - * Properties: value: 1, label: participantID - * - * @type {String} + * @param {string} action - The action that the event represents. + * @param {Object} attributes - Attributes to attach to the event. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const REMOTE_VIDEO_MENU_KICK = 'remotevideomenu.kick'; +export const createSharedVideoEvent = function(action, attributes = {}) { + return { + action, + attributes, + actionSubject: 'shared.video' + }; +}; /** - * Clicked on the button to audio mute a remote participant. + * Creates an event associated with a shortcut being pressed, released or + * triggered. By convention, where appropriate an attribute named 'enable' + * should be used to indicate the action which resulted by the shortcut being + * pressed (e.g. whether screen sharing was enabled or disabled). * - * Properties: value: 1, label: participantID - * - * @type {String} + * @param {string} shortcut - The identifier of the shortcut which produced + * an action. + * @param {string} action - The action that the event represents (one + * of ACTION_SHORTCUT_PRESSED, ACTION_SHORTCUT_RELEASED + * or ACTION_SHORTCUT_TRIGGERED). + * @param {Object} attributes - Attributes to attach to the event. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const REMOTE_VIDEO_MENU_MUTE_CLICKED = 'remotevideomenu.mute.clicked'; +export const createShortcutEvent + = function(shortcut, action = ACTION_SHORTCUT_TRIGGERED, attributes = {}) { + return { + action, + actionSubject: 'keyboard.shortcut', + actionSubjectId: shortcut, + attributes, + source: 'keyboard.shortcut', + type: TYPE_UI + }; + }; /** - * Confirmed the muting of a remote participant. + * Creates an event which indicates the "start audio only" configuration. * - * Properties: value: 1, label: participantID - * - * @type {String} + * @param {boolean} audioOnly - Whether "start audio only" is enabled or not. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const REMOTE_VIDEO_MENU_MUTE_CONFIRMED - = 'remotevideomenu.mute.confirmed'; +export const createStartAudioOnlyEvent = function(audioOnly) { + return { + action: 'start.audio.only', + attributes: { + enabled: audioOnly + } + }; +}; /** - * Clicked on the remote control option in the remote menu. + * Creates an event which indicates the "start muted" configuration. * - * Properties: value: 1, label: participantID - * - * Known full event names: - * remotevideomenu.remotecontrol.stop - * remotevideomenu.remotecontrol.start - * - * @type {String} + * @param {string} source - The source of the configuration, 'local' or + * 'remote' depending on whether it comes from the static configuration (i.e. + * config.js) or comes dynamically from Jicofo. + * @param {boolean} audioMute - Whether the configuration requests that audio + * is muted. + * @param {boolean} videoMute - Whether the configuration requests that video + * is muted. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const REMOTE_VIDEO_MENU_REMOTE_CONTROL_ - = 'remotevideomenu.remotecontrol'; +export const createStartMutedConfigurationEvent + = function(source, audioMute, videoMute) { + return { + action: 'start.muted.configuration', + attributes: { + source, + 'audio_mute': audioMute, + 'video_mute': videoMute + } + }; + }; /** - * Replacing the currently used track of specified type with a new track of the - * same type. The event is fired when changing devices. + * Creates an event which indicates the delay for switching between simulcast + * streams. * - * Known full event names: - * replacetrack.audio - * replacetrack.video - * - * @type {String} + * @param {Object} attributes - Attributes to attach to the event. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const REPLACE_TRACK_ = 'replacetrack'; - -/** - * The local participant failed to start receiving high quality video from - * a remote participant, which is usually initiated by the remote participant - * being put on large video. - * - * @type {String} - */ -export const SELECT_PARTICIPANT_FAILED = 'selectParticipant.failed'; - -/** - * The local participant began using a different audio input device (mic). - * - * @type {String} - */ -export const SETTINGS_CHANGE_DEVICE_AUDIO_IN = 'settings.changeDevice.audioIn'; - -/** - * The local participant began using a different audio output device (speaker). - * - * @type {String} - */ -export const SETTINGS_CHANGE_DEVICE_AUDIO_OUT - = 'settings.changeDevice.audioOut'; - -/** - * The local participant began using a different camera. - * - * @type {String} - */ -export const SETTINGS_CHANGE_DEVICE_VIDEO = 'settings.changeDevice.video'; - -/** - * Attempted to start sharing a YouTube video but one is already being shared. - * - * @type {String} - */ -export const SHARED_VIDEO_ALREADY_SHARED = 'sharedvideo.alreadyshared'; - -/** - * The local participant's mic was muted automatically during a shared video. - * - * @type {String} - */ -export const SHARED_VIDEO_AUDIO_MUTED = 'sharedvideo.audio.muted'; - -/** - * The local participant's mic was unmuted automatically during a shared video. - * - * @type {String} - */ -export const SHARED_VIDEO_AUDIO_UNMUTED = 'sharedvideo.audio.unmuted'; - -/** - * Canceled the prompt to enter a YouTube video to share. - * - * @type {String} - */ -export const SHARED_VIDEO_CANCELED = 'sharedvideo.canceled'; - -/** - * The shared YouTube video has been paused. - * - * @type {String} - */ -export const SHARED_VIDEO_PAUSED = 'sharedvideo.paused'; - -/** - * Started sharing a YouTube video. - * - * @type {String} - */ -export const SHARED_VIDEO_STARTED = 'sharedvideo.started'; - -/** - * Confirmed stoppage of the shared YouTube video. - * - * @type {String} - */ -export const SHARED_VIDEO_STOPPED = 'sharedvideo.stoped'; - -/** - * The shared YouTube video had its volume change. - * - * @type {String} - */ -export const SHARED_VIDEO_VOLUME_CHANGED = 'sharedvideo.volumechanged'; - -/** - * Pressed the keyboard shortcut for toggling audio mute. - * - * @type {String} - */ -export const SHORTCUT_AUDIO_MUTE_TOGGLED = 'shortcut.audiomute.toggled'; - -/** - * Pressed the keyboard shortcut for toggling chat panel display. - * - * @type {String} - */ -export const SHORTCUT_CHAT_TOGGLED = 'shortcut.chat.toggled'; - -/** - * Toggled the display of the keyboard shortcuts help dialog. - * - * @type {String} - */ -export const SHORTCUT_HELP = 'shortcut.shortcut.help'; - -/** - * Pressed the keyboard shortcut for togglgin raise hand status. - * - * @type {String} - */ -export const SHORTCUT_RAISE_HAND_CLICKED = 'shortcut.raisehand.clicked'; - -/** - * Pressed the keyboard shortcut for toggling screenshare. - * - * @type {String} - */ -export const SHORTCUT_SCREEN_TOGGLED = 'shortcut.screen.toggled'; - -/** - * Toggled the display of the speaker stats dialog. - * - * @type {String} - */ -export const SHORTCUT_SPEAKER_STATS_CLICKED = 'shortcut.speakerStats.clicked'; - -/** - * Started pressing the key that undoes audio mute while the key is pressed. - * - * @type {String} - */ -export const SHORTCUT_TALK_CLICKED = 'shortcut.talk.clicked'; - -/** - * Released the key used to talk while audio muted, returning to the audio muted - * state. - * - * @type {String} - */ -export const SHORTCUT_TALK_RELEASED = 'shortcut.talk.released'; - -/** - * Toggling video mute state using a keyboard shortcut. - * - * @type {String} - */ -export const SHORTCUT_VIDEO_MUTE_TOGGLED = 'shortcut.videomute.toggled'; - -/** - * The config specifies the local participant should start with audio only mode - * enabled or disabled. - * - * Known full event names: - * startaudioonly.enabled - * startaudioonly.disabled - * - * @type {String} - */ -export const START_AUDIO_ONLY_ = 'startaudioonly'; - -/** - * The config specifies the local participant should start with audio mute - * enabled or disabled. - * - * Known full event names: - * startmuted.client.audio.muted - * startmuted.client.audio.unmuted - * - * @type {String} - */ -export const START_MUTED_CLIENT_AUDIO_ = 'startmuted.client.audio'; - -/** - * The config specifies the local participant should start with video mute - * enabled or disabled. - * - * Known full event names: - * startmuted.client.video.muted - * startmuted.client.video.unmuted - * - * @type {String} - */ -export const START_MUTED_CLIENT_VIDEO_ = 'startmuted.client.video'; - -/** - * The local participant has received an event from the server stating to - * start audio muted or unmuted. - * - * Known full event names: - * startmuted.server.audio.muted - * startmuted.server.audio.unmuted - * - * @type {String} - */ -export const START_MUTED_SERVER_AUDIO_ = 'startmuted.server.audio'; - -/** - * The local participant has received an event from the server stating to - * start video muted or unmuted. - * - * Known full event names: - * startmuted.server.video.muted - * startmuted.server.video.unmuted - * - * @type {String} - */ -export const START_MUTED_SERVER_VIDEO_ = 'startmuted.server.video'; - -/** - * How long it took to switch between simulcast streams. - * - * Properties: value - * - * @type {String} - */ -export const STREAM_SWITCH_DELAY = 'stream.switch.delay'; +export const createStreamSwitchDelayEvent = function(attributes) { + return { + action: 'stream.switch.delay', + attributes + }; +}; /** * Automatically changing the mute state of a media track in order to match * the current stored state in redux. * - * Known full event names: - * synctrackstate.audio.muted - * synctrackstate.audio.unmuted - * synctrackstate.video.muted - * synctrackstate.video.unmuted - * - * @type {String} + * @param {string} mediaType - The track's media type ('audio' or 'video'). + * @param {boolean} muted - Whether the track is being muted or unmuted as + * as result of the sync operation. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const SYNC_TRACK_STATE_ = 'synctrackstate'; +export const createSyncTrackStateEvent = function(mediaType, muted) { + return { + action: 'sync.track.state', + attributes: { + 'media_type': mediaType, + muted + } + }; +}; /** - * Clicked the toolbar button to enter audio mute state. + * Creates an event associated with a toolbar button being clicked/pressed. By + * convention, where appropriate an attribute named 'enable' should be used to + * indicate the action which resulted by the shortcut being pressed (e.g. + * whether screen sharing was enabled or disabled). * - * @type {String} + * @param {string} buttonName - The identifier of the toolbar button which was + * clicked/pressed. + * @param {Object} attributes - Attributes to attach to the event. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const TOOLBAR_AUDIO_MUTED = 'toolbar.audio.muted'; +export const createToolbarEvent = function(buttonName, attributes = {}) { + return { + action: 'clicked', + actionSubject: buttonName, + attributes, + source: 'toolbar.button', + type: TYPE_UI + }; +}; /** - * Clicked within a toolbar menu to enable audio only. + * Creates an event which indicates that a local track was muted. * - * @type {String} + * @param {string} mediaType - The track's media type ('audio' or 'video'). + * @param {string} reason - The reason the track was muted (e.g. it was + * triggered by the "initial mute" option, or a previously muted track was + * replaced (e.g. when a new device was used)). + * @param {boolean} muted - Whether the track was muted or unmuted. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. */ -export const TOOLBAR_AUDIO_ONLY_ENABLED = 'toolbar.audioonly.enabled'; - -/** - * Clicked the toolbar button to exit audio mute state. - * - * @type {String} - */ -export const TOOLBAR_AUDIO_UNMUTED = 'toolbar.audio.unmuted'; - -/** - * Clicked the toolbar button for toggling chat panel display. - * - * @type {String} - */ -export const TOOLBAR_CHAT_TOGGLED = 'toolbar.chat.toggled'; - -/** - * Clicked the toolbar button for toggling contact list panel display. - * - * @type {String} - */ -export const TOOLBAR_CONTACTS_TOGGLED = 'toolbar.contacts.toggled'; - -/** - * Clicked the toolbar button to toggle display of etherpad (collaborative - * document writing). - * - * @type {String} - */ -export const TOOLBAR_ETHERPACK_CLICKED = 'toolbar.etherpad.clicked'; - -/** - * Pressed the keyboard shortcut to open the device selection window while in - * filmstrip only mode. - * - * @type {String} - */ -export const TOOLBAR_FILMSTRIP_ONLY_DEVICE_SELECTION_TOGGLED - = 'toolbar.fodeviceselection.toggled'; - -/** - * Visibility of the filmstrip has been toggled. - * - * @type {String} - */ -export const TOOLBAR_FILMSTRIP_TOGGLED = 'toolbar.filmstrip.toggled'; - -/** - * Clicked the toolbar button to toggle display full screen mode. - * - * @type {String} - */ -export const TOOLBAR_FULLSCREEN_ENABLED = 'toolbar.fullscreen.enabled'; - -/** - * Clicked the toolbar button to leave the conference. - * - * @type {String} - */ -export const TOOLBAR_HANGUP = 'toolbar.hangup'; - -/** - * Clicked the toolbar button to open the invite dialog. - * - * @type {String} - */ -export const TOOLBAR_INVITE_CLICKED = 'toolbar.invite.clicked'; - -/** - * The invite dialog has been dismissed. - * - * @type {String} - */ -export const TOOLBAR_INVITE_CLOSE = 'toolbar.invite.close'; - -/** - * Clicked the toolbar button for toggling the display of the profile panel. - * - * @type {String} - */ -export const TOOLBAR_PROFILE_TOGGLED = 'toolbar.profile.toggled'; - -/** - * Clicked the toolbar button for toggling raise hand status. - * - * @type {String} - */ -export const TOOLBAR_RAISE_HAND_CLICKED = 'toolbar.raiseHand.clicked'; - -/** - * Clicked the toolbar button to stop screensharing. - * - * @type {String} - */ -export const TOOLBAR_SCREEN_DISABLED = 'toolbar.screen.disabled'; - -/** - * Clicked the toolbar button to start screensharing. - * - * @type {String} - */ -export const TOOLBAR_SCREEN_ENABLED = 'toolbar.screen.enabled'; - -/** - * Clicked the toolbar button for toggling display of the settings menu. - * - * @type {String} - */ -export const TOOLBAR_SETTINGS_TOGGLED = 'toolbar.settings.toggled'; - -/** - * Clicked the toolbar button for toggling a shared YouTube video. - * - * @type {String} - */ -export const TOOLBAR_SHARED_VIDEO_CLICKED = 'toolbar.sharedvideo.clicked'; - -/** - * Clicked the toolbar button to open the dial-out feature. - * - * @type {String} - */ -export const TOOLBAR_SIP_DIALPAD_CLICKED = 'toolbar.sip.dialpad.clicked'; - -/** - * In the mobile app, clicked on the toolbar button to toggle video mute. - * - * Known full event names: - * toolbar.video.muted - * toolbar.video.unmuted - * - * @type {String} - */ -export const TOOLBAR_VIDEO_ = 'toolbar.video'; - -/** - * Clicked on the toolbar to video unmute. - * - * @type {String} - */ -export const TOOLBAR_VIDEO_DISABLED = 'toolbar.video.disabled'; - -/** - * Clicked on the toolbar to video mute. - * - * @type {String} - */ -export const TOOLBAR_VIDEO_ENABLED = 'toolbar.video.enabled'; - -/** - * Clicked within a toolbar menu to set max incoming video quality to high - * definition. - * - * @type {String} - */ -export const TOOLBAR_VIDEO_QUALITY_HIGH = 'toolbar.videoquality.high'; - -/** - * Clicked within a toolbar menu to set max incoming video quality to low - * definition. - * - * @type {String} - */ -export const TOOLBAR_VIDEO_QUALITY_LOW = 'toolbar.videoquality.low'; - -/** - * Clicked within a toolbar menu to set max incoming video quality to standard - * definition. - * - * @type {String} - */ -export const TOOLBAR_VIDEO_QUALITY_STANDARD = 'toolbar.videoquality.standard'; - -/** - * The local participant has unpinned a participant so the participant doesn't - * remain permanently on local large video. - * - * Known full event names: - * unpinned.local - * unpinned.remote - * - * @type {String} - */ -export const UNPINNED_ = 'unpinned'; +export const createTrackMutedEvent = function(mediaType, reason, muted = true) { + return { + action: 'track.muted', + attributes: { + 'media_type': mediaType, + muted, + reason + } + }; +}; diff --git a/react/features/analytics/functions.js b/react/features/analytics/functions.js index 55a38b41a..5b5b47832 100644 --- a/react/features/analytics/functions.js +++ b/react/features/analytics/functions.js @@ -9,12 +9,14 @@ import { getJitsiMeetGlobalNS, loadScript } from '../base/util'; const logger = require('jitsi-meet-logger').getLogger(__filename); /** - * Sends an analytics event. + * Sends an event through the lib-jitsi-meet AnalyticsAdapter interface. * - * @inheritdoc + * @param {Object} event - The event to send. It should be formatted as + * described in AnalyticsAdapter.js in lib-jitsi-meet. + * @returns {void} */ -export function sendAnalyticsEvent(...args: Array) { - analytics.sendEvent(...args); +export function sendAnalytics(event: Object) { + analytics.sendEvent(event); } /** @@ -38,23 +40,17 @@ export function initAnalytics({ getState }: { getState: Function }) { const state = getState(); const config = state['features/base/config']; const { analyticsScriptUrls } = config; - const machineId = JitsiMeetJS.getMachineId(); const { user } = state['features/base/jwt']; const handlerConstructorOptions = { - product: 'lib-jitsi-meet', version: JitsiMeetJS.version, - session: machineId, - user: user ? user.id : `uid-${machineId}`, - server: state['features/base/connection'].locationURL.host + user }; _loadHandlers(analyticsScriptUrls, handlerConstructorOptions) .then(handlers => { - const permanentProperties: Object = { - roomName: state['features/base/conference'].room, - userAgent: navigator.userAgent - }; + const roomName = state['features/base/conference'].room; const { group, server } = state['features/base/jwt']; + const permanentProperties = {}; if (server) { permanentProperties.server = server; @@ -76,6 +72,9 @@ export function initAnalytics({ getState }: { getState: Function }) { } analytics.addPermanentProperties(permanentProperties); + analytics.setConferenceName(roomName); + + // Set the handlers last, since this triggers emptying of the cache analytics.setAnalyticsHandlers(handlers); }, error => analytics.dispose() && logger.error(error)); diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js index 55c8d9f69..09eaac843 100644 --- a/react/features/base/conference/actions.js +++ b/react/features/base/conference/actions.js @@ -3,9 +3,8 @@ import UIEvents from '../../../../service/UI/UIEvents'; import { - START_MUTED_SERVER_AUDIO_, - START_MUTED_SERVER_VIDEO_, - sendAnalyticsEvent + createStartMutedConfigurationEvent, + sendAnalytics } from '../../analytics'; import { getName } from '../../app'; import { JitsiConferenceEvents } from '../lib-jitsi-meet'; @@ -90,12 +89,8 @@ function _addConferenceListeners(conference, dispatch) { const audioMuted = Boolean(conference.startAudioMuted); const videoMuted = Boolean(conference.startVideoMuted); - sendAnalyticsEvent( - `${START_MUTED_SERVER_AUDIO_}.${ - audioMuted ? 'muted' : 'unmuted'}`); - sendAnalyticsEvent( - `${START_MUTED_SERVER_VIDEO_}.${ - videoMuted ? 'muted' : 'unmuted'}`); + sendAnalytics(createStartMutedConfigurationEvent( + 'remote', audioMuted, videoMuted)); logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${ videoMuted ? 'video' : ''}`); diff --git a/react/features/base/conference/middleware.js b/react/features/base/conference/middleware.js index f401a22e1..5c8cba052 100644 --- a/react/features/base/conference/middleware.js +++ b/react/features/base/conference/middleware.js @@ -3,12 +3,11 @@ import UIEvents from '../../../../service/UI/UIEvents'; import { - _LOCAL, - _REMOTE, - AUDIO_ONLY_DISABLED, - PINNED_, - UNPINNED_, - sendAnalyticsEvent + ACTION_PINNED, + ACTION_UNPINNED, + createAudioOnlyDisableEvent, + createPinnedEvent, + sendAnalytics } from '../../analytics'; import { CONNECTION_ESTABLISHED } from '../connection'; import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../media'; @@ -131,7 +130,7 @@ function _conferenceFailedOrLeft({ dispatch, getState }, next, action) { const result = next(action); if (getState()['features/base/conference'].audioOnly) { - sendAnalyticsEvent(AUDIO_ONLY_DISABLED); + sendAnalytics(createAudioOnlyDisableEvent()); logger.log('Audio only disabled'); dispatch(setAudioOnly(false)); } @@ -193,19 +192,19 @@ function _pinParticipant(store, next, action) { if (typeof APP !== 'undefined') { const pinnedParticipant = getPinnedParticipant(participants); - const actionName = action.participant.id ? PINNED_ : UNPINNED_; - let videoType; + const actionName + = action.participant.id ? ACTION_PINNED : ACTION_UNPINNED; + const local = (participantById && participantById.local) + || (!id && pinnedParticipant && pinnedParticipant.local); - if ((participantById && participantById.local) - || (!id && pinnedParticipant && pinnedParticipant.local)) { - videoType = _LOCAL; - } else { - videoType = _REMOTE; - } + sendAnalytics(createPinnedEvent( + actionName, + local ? 'local' : id, + { + 'participant_count': conference.getParticipantCount(), + local + })); - sendAnalyticsEvent( - `${actionName}.${videoType}`, - { value: conference.getParticipantCount() }); } // The following condition prevents signaling to pin local participant and diff --git a/react/features/base/media/middleware.js b/react/features/base/media/middleware.js index 6001ce382..d999f754f 100644 --- a/react/features/base/media/middleware.js +++ b/react/features/base/media/middleware.js @@ -1,11 +1,10 @@ /* @flow */ import { - START_AUDIO_ONLY_, - START_MUTED_CLIENT_AUDIO_, - START_MUTED_CLIENT_VIDEO_, - SYNC_TRACK_STATE_, - sendAnalyticsEvent + createStartAudioOnlyEvent, + createStartMutedConfigurationEvent, + createSyncTrackStateEvent, + sendAnalytics } from '../../analytics'; import { SET_ROOM, setAudioOnly } from '../conference'; import { parseURLParams } from '../config'; @@ -90,12 +89,8 @@ function _setRoom({ dispatch, getState }, next, action) { audioMuted = Boolean(audioMuted); videoMuted = Boolean(videoMuted); - // Apply the config. - - sendAnalyticsEvent( - `${START_MUTED_CLIENT_AUDIO_}.${audioMuted ? 'muted' : 'unmuted'}`); - sendAnalyticsEvent( - `${START_MUTED_CLIENT_VIDEO_}.${videoMuted ? 'muted' : 'unmuted'}`); + sendAnalytics(createStartMutedConfigurationEvent( + 'local', audioMuted, videoMuted)); logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${ videoMuted ? 'video' : ''}`); @@ -128,8 +123,7 @@ function _setRoom({ dispatch, getState }, next, action) { audioOnly = true; } - sendAnalyticsEvent( - `${START_AUDIO_ONLY_}.${audioOnly ? 'enabled' : 'disabled'}`); + sendAnalytics(createStartAudioOnlyEvent(audioOnly)); logger.log(`Start audio only set to ${audioOnly.toString()}`); dispatch(setAudioOnly(audioOnly)); } @@ -155,9 +149,7 @@ function _syncTrackMutedState({ getState }, track) { // not yet in redux state and JitsiTrackEvents.TRACK_MUTE_CHANGED may be // fired before track gets to state. if (track.muted !== muted) { - sendAnalyticsEvent( - `${SYNC_TRACK_STATE_}.${track.mediaType}.${ - muted ? 'muted' : 'unmuted'}`); + sendAnalytics(createSyncTrackStateEvent(track.mediaType, muted)); logger.log(`Sync ${track.mediaType} track muted state to ${ muted ? 'muted' : 'unmuted'}`); track.muted = muted; diff --git a/react/features/base/tracks/actions.js b/react/features/base/tracks/actions.js index 02326066b..076c399b7 100644 --- a/react/features/base/tracks/actions.js +++ b/react/features/base/tracks/actions.js @@ -1,6 +1,6 @@ import { - REPLACE_TRACK_, - sendAnalyticsEvent + createTrackMutedEvent, + sendAnalytics } from '../../analytics'; import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet'; import { @@ -220,9 +220,10 @@ export function replaceLocalTrack(oldTrack, newTrack, conference) { : setAudioMuted; const isMuted = newTrack.isMuted(); - sendAnalyticsEvent(`${REPLACE_TRACK_}.${ - newTrack.getType()}.${ - isMuted ? 'muted' : 'unmuted'}`); + sendAnalytics(createTrackMutedEvent( + newTrack.getType(), + 'track.replaced', + isMuted)); logger.log(`Replace ${newTrack.getType()} track - ${ isMuted ? 'muted' : 'unmuted'}`); diff --git a/react/features/feedback/components/FeedbackDialog.web.js b/react/features/feedback/components/FeedbackDialog.web.js index 47ff2ac4a..5d6341b3f 100644 --- a/react/features/feedback/components/FeedbackDialog.web.js +++ b/react/features/feedback/components/FeedbackDialog.web.js @@ -7,8 +7,8 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { - FEEDBACK_OPEN, - sendAnalyticsEvent + createFeedbackOpenEvent, + sendAnalytics } from '../../analytics'; import { Dialog } from '../../base/dialog'; import { translate } from '../../base/i18n'; @@ -148,7 +148,7 @@ class FeedbackDialog extends Component { * @inheritdoc */ componentDidMount() { - sendAnalyticsEvent(FEEDBACK_OPEN); + sendAnalytics(createFeedbackOpenEvent()); } /** diff --git a/react/features/filmstrip/middleware.js b/react/features/filmstrip/middleware.js index 820cf1113..df29c82c9 100644 --- a/react/features/filmstrip/middleware.js +++ b/react/features/filmstrip/middleware.js @@ -26,7 +26,7 @@ MiddlewareRegistry.register(({ getState }) => next => action => { // not need the middleware implemented here, Filmstrip.init, and // UI.start. || (Filmstrip.filmstrip - && Filmstrip.toggleFilmstrip(!newValue, false)); + && Filmstrip.toggleFilmstrip(!newValue)); return result; } diff --git a/react/features/invite/components/InviteDialog.web.js b/react/features/invite/components/InviteDialog.web.js index 56b0bbfb2..73a6be1da 100644 --- a/react/features/invite/components/InviteDialog.web.js +++ b/react/features/invite/components/InviteDialog.web.js @@ -3,8 +3,8 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { - TOOLBAR_INVITE_CLOSE, - sendAnalyticsEvent + createInviteDialogClosedEvent, + sendAnalytics } from '../../analytics'; import { getInviteURL } from '../../base/connection'; import { Dialog } from '../../base/dialog'; @@ -54,7 +54,7 @@ class InviteDialog extends Component { * @inheritdoc */ componentWillUnmount() { - sendAnalyticsEvent(TOOLBAR_INVITE_CLOSE); + sendAnalytics(createInviteDialogClosedEvent()); } /** diff --git a/react/features/mobile/background/actions.js b/react/features/mobile/background/actions.js index 06e49c037..ecaee774e 100644 --- a/react/features/mobile/background/actions.js +++ b/react/features/mobile/background/actions.js @@ -1,8 +1,8 @@ /* @flow */ import { - CALLKIT_BACKGROUND_VIDEO_MUTED, - sendAnalyticsEvent + createTrackMutedEvent, + sendAnalytics } from '../../analytics'; import { setLastN } from '../../base/conference'; import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../../base/media'; @@ -46,7 +46,9 @@ export function _setBackgroundVideoMuted(muted: boolean) { audioOnly || dispatch(setLastN(muted ? 0 : undefined)); - sendAnalyticsEvent(CALLKIT_BACKGROUND_VIDEO_MUTED); + sendAnalytics(createTrackMutedEvent( + 'video', + 'callkit.background.video')); dispatch(setVideoMuted(muted, VIDEO_MUTISM_AUTHORITY.BACKGROUND)); }; diff --git a/react/features/mobile/callkit/middleware.js b/react/features/mobile/callkit/middleware.js index d2754060a..d1f51c91a 100644 --- a/react/features/mobile/callkit/middleware.js +++ b/react/features/mobile/callkit/middleware.js @@ -4,8 +4,8 @@ import { NativeModules } from 'react-native'; import uuid from 'uuid'; import { - CALLKIT_AUDIO_, - sendAnalyticsEvent + createTrackMutedEvent, + sendAnalytics } from '../../analytics'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT, appNavigate } from '../../app'; import { @@ -279,8 +279,7 @@ function _onPerformSetMutedCallAction({ callUUID, muted: newValue }) { if (oldValue !== newValue) { const value = Boolean(newValue); - sendAnalyticsEvent(`${CALLKIT_AUDIO_}.${ - value ? 'muted' : 'unmuted'}`); + sendAnalytics(createTrackMutedEvent('audio', 'callkit', value)); dispatch(setAudioMuted(value)); } } diff --git a/react/features/overlay/components/AbstractPageReloadOverlay.js b/react/features/overlay/components/AbstractPageReloadOverlay.js index 32bc7a83e..f5c952608 100644 --- a/react/features/overlay/components/AbstractPageReloadOverlay.js +++ b/react/features/overlay/components/AbstractPageReloadOverlay.js @@ -3,7 +3,10 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { PAGE_RELOAD } from '../../analytics'; +import { + createPageReloadScheduledEvent, + sendAnalytics +} from '../../analytics'; import { isFatalJitsiConferenceError, isFatalJitsiConnectionError @@ -159,12 +162,18 @@ export default class AbstractPageReloadOverlay extends Component<*, *> { // sent to the backed. // FIXME: We should dispatch action for this. if (typeof APP !== 'undefined') { - APP.conference.logEvent( - PAGE_RELOAD, - /* value */ undefined, - /* label */ this.props.reason); + if (APP.conference && APP.conference.room) { + APP.conference.room.sendApplicationLog(JSON.stringify( + { + name: 'page.reload', + label: this.props.reason + })); + } } + sendAnalytics(createPageReloadScheduledEvent( + this.props.reason, this.state.timeoutSeconds)); + logger.info( `The conference will be reloaded after ${ this.state.timeoutSeconds} seconds.`); diff --git a/react/features/remote-video-menu/components/KickButton.js b/react/features/remote-video-menu/components/KickButton.js index 7ccbb8138..f9477ff81 100644 --- a/react/features/remote-video-menu/components/KickButton.js +++ b/react/features/remote-video-menu/components/KickButton.js @@ -3,8 +3,8 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { - REMOTE_VIDEO_MENU_KICK, - sendAnalyticsEvent + createRemoteVideoMenuButtonEvent, + sendAnalytics } from '../../analytics'; import { translate } from '../../base/i18n'; import { kickParticipant } from '../../base/participants'; @@ -86,13 +86,12 @@ class KickButton extends Component { _onClick() { const { dispatch, onClick, participantID } = this.props; - sendAnalyticsEvent( - REMOTE_VIDEO_MENU_KICK, + sendAnalytics(createRemoteVideoMenuButtonEvent( + 'kick.button', { - value: 1, - label: participantID - } - ); + 'participant_id': participantID + })); + dispatch(kickParticipant(participantID)); if (onClick) { diff --git a/react/features/remote-video-menu/components/MuteButton.js b/react/features/remote-video-menu/components/MuteButton.js index ba5d382a2..431ada860 100644 --- a/react/features/remote-video-menu/components/MuteButton.js +++ b/react/features/remote-video-menu/components/MuteButton.js @@ -3,8 +3,8 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { - REMOTE_VIDEO_MENU_MUTE_CLICKED, - sendAnalyticsEvent + createRemoteVideoMenuButtonEvent, + sendAnalytics } from '../../analytics'; import { translate } from '../../base/i18n'; import { openDialog } from '../../base/dialog'; @@ -101,13 +101,11 @@ class MuteButton extends Component { _onClick() { const { dispatch, onClick, participantID } = this.props; - sendAnalyticsEvent( - REMOTE_VIDEO_MENU_MUTE_CLICKED, + sendAnalytics(createRemoteVideoMenuButtonEvent( + 'mute.button', { - value: 1, - label: participantID - } - ); + 'participant_id': participantID + })); dispatch(openDialog(MuteRemoteParticipantDialog, { participantID })); diff --git a/react/features/remote-video-menu/components/MuteRemoteParticipantDialog.web.js b/react/features/remote-video-menu/components/MuteRemoteParticipantDialog.web.js index fad51ac25..a000bdcb3 100644 --- a/react/features/remote-video-menu/components/MuteRemoteParticipantDialog.web.js +++ b/react/features/remote-video-menu/components/MuteRemoteParticipantDialog.web.js @@ -6,8 +6,8 @@ import { Dialog } from '../../base/dialog'; import { translate } from '../../base/i18n'; import { - REMOTE_VIDEO_MENU_MUTE_CONFIRMED, - sendAnalyticsEvent + createRemoteMuteConfirmedEvent, + sendAnalytics } from '../../analytics'; import { muteRemoteParticipant } from '../../base/participants'; @@ -77,18 +77,12 @@ class MuteRemoteParticipantDialog extends Component { * Handles the submit button action. * * @private - * @returns {void} + * @returns {boolean} - True (to note that the modal should be closed). */ _onSubmit() { const { dispatch, participantID } = this.props; - sendAnalyticsEvent( - REMOTE_VIDEO_MENU_MUTE_CONFIRMED, - { - value: 1, - label: participantID - } - ); + sendAnalytics(createRemoteMuteConfirmedEvent(participantID)); dispatch(muteRemoteParticipant(participantID)); diff --git a/react/features/remote-video-menu/components/RemoteControlButton.js b/react/features/remote-video-menu/components/RemoteControlButton.js index f7eaede42..c4c834c8e 100644 --- a/react/features/remote-video-menu/components/RemoteControlButton.js +++ b/react/features/remote-video-menu/components/RemoteControlButton.js @@ -2,8 +2,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { - REMOTE_VIDEO_MENU_REMOTE_CONTROL_, - sendAnalyticsEvent + createRemoteVideoMenuButtonEvent, + sendAnalytics } from '../../analytics'; import { translate } from '../../base/i18n'; @@ -122,24 +122,19 @@ class RemoteControlButton extends Component { _onClick() { const { onClick, participantID, remoteControlState } = this.props; - let eventName; + // TODO: What do we do in case the state is e.g. "requesting"? + if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED + || remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) { - if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) { - eventName = 'stop'; - } + const enable + = remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED; - if (remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) { - eventName = 'start'; - } - - if (eventName) { - sendAnalyticsEvent( - `${REMOTE_VIDEO_MENU_REMOTE_CONTROL_}.${eventName}`, + sendAnalytics(createRemoteVideoMenuButtonEvent( + 'remote.control.button', { - value: 1, - label: participantID - } - ); + enable, + 'participant_id': participantID + })); } if (onClick) { diff --git a/react/features/toolbox/components/ProfileButton.web.js b/react/features/toolbox/components/ProfileButton.web.js index a02032860..3f1d6be2c 100644 --- a/react/features/toolbox/components/ProfileButton.web.js +++ b/react/features/toolbox/components/ProfileButton.web.js @@ -4,7 +4,10 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; -import { TOOLBAR_PROFILE_TOGGLED, sendAnalyticsEvent } from '../../analytics'; +import { + createToolbarEvent, + sendAnalytics +} from '../../analytics'; import { getAvatarURL, getLocalParticipant @@ -115,7 +118,9 @@ class ProfileButton extends Component<*> { */ _onClick() { if (!this.props._unclickable) { - sendAnalyticsEvent(TOOLBAR_PROFILE_TOGGLED); + // TODO: Include an 'enable' attribute, which specifies whether + // the profile panel was opened or closed. + sendAnalytics(createToolbarEvent('profile')); APP.UI.emitEvent(UIEvents.TOGGLE_PROFILE); } } diff --git a/react/features/toolbox/components/Toolbox.native.js b/react/features/toolbox/components/Toolbox.native.js index ccecec2d2..05fa23185 100644 --- a/react/features/toolbox/components/Toolbox.native.js +++ b/react/features/toolbox/components/Toolbox.native.js @@ -4,10 +4,10 @@ import { View } from 'react-native'; import { connect } from 'react-redux'; import { - TOOLBAR_AUDIO_MUTED, - TOOLBAR_AUDIO_UNMUTED, - TOOLBAR_VIDEO_, - sendAnalyticsEvent + AUDIO_MUTE, + VIDEO_MUTE, + createToolbarEvent, + sendAnalytics } from '../../analytics'; import { isNarrowAspectRatio, @@ -188,7 +188,11 @@ class Toolbox extends Component { _onToggleAudio() { const mute = !this.props._audioMuted; - sendAnalyticsEvent(mute ? TOOLBAR_AUDIO_MUTED : TOOLBAR_AUDIO_UNMUTED); + sendAnalytics(createToolbarEvent( + AUDIO_MUTE, + { + enable: mute + })); // The user sees the reality i.e. the state of base/tracks and intends // to change reality by tapping on the respective button i.e. the user @@ -211,7 +215,11 @@ class Toolbox extends Component { _onToggleVideo() { const mute = !this.props._videoMuted; - sendAnalyticsEvent(`${TOOLBAR_VIDEO_}.${mute ? 'muted' : 'unmuted'}`); + sendAnalytics(createToolbarEvent( + VIDEO_MUTE, + { + enable: mute + })); // The user sees the reality i.e. the state of base/tracks and intends // to change reality by tapping on the respective button i.e. the user diff --git a/react/features/toolbox/defaultToolbarButtons.web.js b/react/features/toolbox/defaultToolbarButtons.web.js index 4319f3f12..534f9790b 100644 --- a/react/features/toolbox/defaultToolbarButtons.web.js +++ b/react/features/toolbox/defaultToolbarButtons.web.js @@ -3,29 +3,12 @@ import React from 'react'; import { - SHORTCUT_AUDIO_MUTE_TOGGLED, - SHORTCUT_CHAT_TOGGLED, - SHORTCUT_RAISE_HAND_CLICKED, - SHORTCUT_SCREEN_TOGGLED, - SHORTCUT_VIDEO_MUTE_TOGGLED, - TOOLBAR_AUDIO_MUTED, - TOOLBAR_AUDIO_UNMUTED, - TOOLBAR_CHAT_TOGGLED, - TOOLBAR_CONTACTS_TOGGLED, - TOOLBAR_ETHERPACK_CLICKED, - TOOLBAR_FILMSTRIP_ONLY_DEVICE_SELECTION_TOGGLED, - TOOLBAR_FULLSCREEN_ENABLED, - TOOLBAR_HANGUP, - TOOLBAR_INVITE_CLICKED, - TOOLBAR_RAISE_HAND_CLICKED, - TOOLBAR_SCREEN_DISABLED, - TOOLBAR_SCREEN_ENABLED, - TOOLBAR_SETTINGS_TOGGLED, - TOOLBAR_SHARED_VIDEO_CLICKED, - TOOLBAR_SIP_DIALPAD_CLICKED, - TOOLBAR_VIDEO_DISABLED, - TOOLBAR_VIDEO_ENABLED, - sendAnalyticsEvent + ACTION_SHORTCUT_TRIGGERED as TRIGGERED, + AUDIO_MUTE, + VIDEO_MUTE, + createShortcutEvent, + createToolbarEvent, + sendAnalytics } from '../analytics'; import { ParticipantCounter } from '../contact-list'; import { openDeviceSelectionDialog } from '../device-selection'; @@ -63,13 +46,18 @@ export default function getDefaultButtons() { isDisplayed: () => true, id: 'toolbar_button_camera', onClick() { + // TODO: Why is this different from the code which handles + // a keyboard shortcut? const newVideoMutedState = !APP.conference.isLocalVideoMuted(); - if (newVideoMutedState) { - sendAnalyticsEvent(TOOLBAR_VIDEO_ENABLED); - } else { - sendAnalyticsEvent(TOOLBAR_VIDEO_DISABLED); - } + // The 'enable' attribute in the event is set to true if the + // button click triggered a mute action, and set to false if it + // triggered an unmute action. + sendAnalytics(createToolbarEvent( + VIDEO_MUTE, + { + enable: newVideoMutedState + })); APP.UI.emitEvent(UIEvents.VIDEO_MUTED, newVideoMutedState); }, popups: [ @@ -88,7 +76,13 @@ export default function getDefaultButtons() { return; } - sendAnalyticsEvent(SHORTCUT_VIDEO_MUTE_TOGGLED); + // The 'enable' attribute in the event is set to true if the + // shortcut triggered a mute action, and set to false if it + // triggered an unmute action. + sendAnalytics(createShortcutEvent( + VIDEO_MUTE, + TRIGGERED, + { enable: !APP.conference.isLocalVideoMuted() })); APP.conference.toggleVideoMuted(); }, shortcutDescription: 'keyboardShortcuts.videoMute', @@ -105,13 +99,26 @@ export default function getDefaultButtons() { , id: 'toolbar_button_chat', onClick() { - sendAnalyticsEvent(TOOLBAR_CHAT_TOGGLED); + // The 'enable' attribute is set to true if the click resulted + // in the chat panel being shown, and to false if it was hidden. + sendAnalytics(createToolbarEvent( + 'toggle.chat', + { + enable: !APP.UI.Chat.isVisible() + })); APP.UI.emitEvent(UIEvents.TOGGLE_CHAT); }, shortcut: 'C', shortcutAttr: 'toggleChatPopover', shortcutFunc() { - sendAnalyticsEvent(SHORTCUT_CHAT_TOGGLED); + // The 'enable' attribute is set to true if the shortcut + // resulted in the chat panel being shown, and to false if it + // was hidden. + sendAnalytics(createShortcutEvent( + 'toggle.chat', + { + enable: !APP.UI.Chat.isVisible() + })); APP.UI.toggleChat(); }, shortcutDescription: 'keyboardShortcuts.toggleChat', @@ -128,7 +135,9 @@ export default function getDefaultButtons() { enabled: true, id: 'toolbar_contact_list', onClick() { - sendAnalyticsEvent(TOOLBAR_CONTACTS_TOGGLED); + // TODO: Include an 'enable' attribute which specifies whether + // the contacts panel was shown or hidden. + sendAnalytics(createToolbarEvent('contacts')); APP.UI.emitEvent(UIEvents.TOGGLE_CONTACT_LIST); }, sideContainerId: 'contacts_container', @@ -143,11 +152,14 @@ export default function getDefaultButtons() { enabled: true, id: 'toolbar_button_desktopsharing', onClick() { - if (APP.conference.isSharingScreen) { - sendAnalyticsEvent(TOOLBAR_SCREEN_DISABLED); - } else { - sendAnalyticsEvent(TOOLBAR_SCREEN_ENABLED); - } + // TODO: Why is the button clicked handled differently that + // a keyboard shortcut press (firing a TOGGLE_SCREENSHARING + // event vs. directly calling toggleScreenSharing())? + sendAnalytics(createToolbarEvent( + 'screen.sharing', + { + enable: !APP.conference.isSharingScreen + })); APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING); }, popups: [ @@ -160,7 +172,13 @@ export default function getDefaultButtons() { shortcut: 'D', shortcutAttr: 'toggleDesktopSharingPopover', shortcutFunc() { - sendAnalyticsEvent(SHORTCUT_SCREEN_TOGGLED); + // The 'enable' attribute is set to true if pressing the + // shortcut resulted in screen sharing being enabled, and false + // if it resulted in screen sharing being disabled. + sendAnalytics(createShortcutEvent( + 'toggle.screen.sharing', + TRIGGERED, + { enable: !APP.conference.isSharingScreen })); // eslint-disable-next-line no-empty-function APP.conference.toggleScreenSharing().catch(() => {}); @@ -180,8 +198,8 @@ export default function getDefaultButtons() { }, id: 'toolbar_button_fodeviceselection', onClick(dispatch: Function) { - sendAnalyticsEvent( - TOOLBAR_FILMSTRIP_ONLY_DEVICE_SELECTION_TOGGLED); + sendAnalytics( + createToolbarEvent('filmstrip.only.device.selection')); dispatch(openDeviceSelectionDialog()); }, @@ -200,7 +218,7 @@ export default function getDefaultButtons() { hidden: true, id: 'toolbar_button_dialpad', onClick() { - sendAnalyticsEvent(TOOLBAR_SIP_DIALPAD_CLICKED); + sendAnalytics(createToolbarEvent('dialpad')); }, tooltipKey: 'toolbar.dialpad' }, @@ -214,7 +232,13 @@ export default function getDefaultButtons() { hidden: true, id: 'toolbar_button_etherpad', onClick() { - sendAnalyticsEvent(TOOLBAR_ETHERPACK_CLICKED); + // The 'enable' attribute is set to true if the click resulted + // in the etherpad panel being shown, or false it it was hidden. + sendAnalytics(createToolbarEvent( + 'toggle.etherpad', + { + enable: !APP.UI.isEtherpadVisible() + })); APP.UI.emitEvent(UIEvents.ETHERPAD_CLICKED); }, tooltipKey: 'toolbar.etherpad' @@ -228,7 +252,18 @@ export default function getDefaultButtons() { enabled: true, id: 'toolbar_button_fullScreen', onClick() { - sendAnalyticsEvent(TOOLBAR_FULLSCREEN_ENABLED); + // TODO: why is the fullscreen button handled differently than + // the fullscreen keyboard shortcut (one results in a direct + // call to toggleFullScreen, while the other fires an + // UIEvents.TOGGLE_FULLSCREEN event)? + + // The 'enable' attribute is set to true if the action resulted + // in fullscreen mode being enabled. + sendAnalytics(createToolbarEvent( + 'toggle.fullscreen', + { + enable: !APP.UI.isFullScreen() + })); APP.UI.emitEvent(UIEvents.TOGGLE_FULLSCREEN); }, @@ -236,7 +271,13 @@ export default function getDefaultButtons() { shortcutAttr: 'toggleFullscreenPopover', shortcutDescription: 'keyboardShortcuts.fullScreen', shortcutFunc() { - sendAnalyticsEvent('shortcut.fullscreen.toggled'); + // The 'enable' attribute is set to true if the action resulted + // in fullscreen mode being enabled. + sendAnalytics(createShortcutEvent( + 'toggle.fullscreen', + { + enable: !APP.UI.isFullScreen() + })); APP.UI.toggleFullScreen(); }, tooltipKey: 'toolbar.fullscreen' @@ -252,7 +293,7 @@ export default function getDefaultButtons() { isDisplayed: () => true, id: 'toolbar_button_hangup', onClick() { - sendAnalyticsEvent(TOOLBAR_HANGUP); + sendAnalytics(createToolbarEvent('hangup')); APP.UI.emitEvent(UIEvents.HANGUP); }, tooltipKey: 'toolbar.hangup' @@ -275,7 +316,7 @@ export default function getDefaultButtons() { enabled: true, id: 'toolbar_button_link', onClick(dispatch: Function) { - sendAnalyticsEvent(TOOLBAR_INVITE_CLICKED); + sendAnalytics(createToolbarEvent('invite')); dispatch(openInviteDialog()); }, @@ -293,6 +334,13 @@ export default function getDefaultButtons() { onClick() { const sharedVideoManager = APP.UI.getSharedVideoManager(); + // TODO: Clicking the mute button and pressing the mute shortcut + // could be handled in a uniform manner. The code below checks + // the mute status and fires the appropriate event (MUTED or + // UNMUTED), while the code which handles the keyboard shortcut + // calls toggleAudioMuted(). Also strangely the the user is + // only warned if they click the button (and not if they use + // the shortcut). if (APP.conference.isLocalAudioMuted()) { // If there's a shared video with the volume "on" and we // aren't the video owner, we warn the user @@ -303,11 +351,15 @@ export default function getDefaultButtons() { APP.UI.showCustomToolbarPopup( 'microphone', 'unableToUnmutePopup', true, 5000); } else { - sendAnalyticsEvent(TOOLBAR_AUDIO_UNMUTED); + sendAnalytics(createToolbarEvent( + AUDIO_MUTE, + { enable: false })); APP.UI.emitEvent(UIEvents.AUDIO_MUTED, false, true); } } else { - sendAnalyticsEvent(TOOLBAR_AUDIO_MUTED); + sendAnalytics(createToolbarEvent( + AUDIO_MUTE, + { enable: true })); APP.UI.emitEvent(UIEvents.AUDIO_MUTED, true, true); } }, @@ -328,7 +380,13 @@ export default function getDefaultButtons() { shortcut: 'M', shortcutAttr: 'mutePopover', shortcutFunc() { - sendAnalyticsEvent(SHORTCUT_AUDIO_MUTE_TOGGLED); + // The 'enable' attribute in the event is set to true if the + // shortcut triggered a mute action, and set to false if it + // triggered an unmute action. + sendAnalytics(createShortcutEvent( + AUDIO_MUTE, + TRIGGERED, + { enable: !APP.conference.isLocalAudioMuted() })); APP.conference.toggleAudioMuted(); }, shortcutDescription: 'keyboardShortcuts.mute', @@ -351,14 +409,27 @@ export default function getDefaultButtons() { enabled: true, id: 'toolbar_button_raisehand', onClick() { - sendAnalyticsEvent(TOOLBAR_RAISE_HAND_CLICKED); + // TODO: reduce duplication with shortcutFunc below. + + // The 'enable' attribute is set to true if the pressing of the + // shortcut resulted in the hand being raised, and to false + // if it resulted in the hand being 'lowered'. + sendAnalytics(createToolbarEvent( + 'raise.hand', + { enable: !APP.conference.isHandRaised })); APP.conference.maybeToggleRaisedHand(); }, shortcut: 'R', shortcutAttr: 'raiseHandPopover', shortcutDescription: 'keyboardShortcuts.raiseHand', shortcutFunc() { - sendAnalyticsEvent(SHORTCUT_RAISE_HAND_CLICKED); + // The 'enable' attribute is set to true if the pressing of the + // shortcut resulted in the hand being raised, and to false + // if it resulted in the hand being 'lowered'. + sendAnalytics(createShortcutEvent( + 'toggle.raise.hand', + TRIGGERED, + { enable: !APP.conference.isHandRaised })); APP.conference.maybeToggleRaisedHand(); }, tooltipKey: 'toolbar.raiseHand' @@ -386,7 +457,9 @@ export default function getDefaultButtons() { enabled: true, id: 'toolbar_button_settings', onClick() { - sendAnalyticsEvent(TOOLBAR_SETTINGS_TOGGLED); + // TODO: Include an 'enable' attribute which specifies whether + // the settings panel was shown or hidden. + sendAnalytics(createToolbarEvent('settings')); APP.UI.emitEvent(UIEvents.TOGGLE_SETTINGS); }, sideContainerId: 'settings_container', @@ -401,7 +474,15 @@ export default function getDefaultButtons() { enabled: true, id: 'toolbar_button_sharedvideo', onClick() { - sendAnalyticsEvent(TOOLBAR_SHARED_VIDEO_CLICKED); + // The 'enable' attribute is set to true if the click resulted + // in the "start sharing video" dialog being shown, and false + // if it resulted in the "stop sharing video" dialog being + // shown. + sendAnalytics(createToolbarEvent( + 'shared.video.toggled', + { + enable: !APP.UI.isSharedVideoShown() + })); APP.UI.emitEvent(UIEvents.SHARED_VIDEO_CLICKED); }, popups: [ diff --git a/react/features/video-quality/components/VideoQualityDialog.web.js b/react/features/video-quality/components/VideoQualityDialog.web.js index e486066d9..9383b4552 100644 --- a/react/features/video-quality/components/VideoQualityDialog.web.js +++ b/react/features/video-quality/components/VideoQualityDialog.web.js @@ -4,16 +4,13 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { - TOOLBAR_AUDIO_ONLY_ENABLED, - TOOLBAR_VIDEO_QUALITY_HIGH, - TOOLBAR_VIDEO_QUALITY_LOW, - TOOLBAR_VIDEO_QUALITY_STANDARD, - sendAnalyticsEvent + createToolbarEvent, + sendAnalytics } from '../../analytics'; import { + VIDEO_QUALITY_LEVELS, setAudioOnly, - setReceiveVideoQuality, - VIDEO_QUALITY_LEVELS + setReceiveVideoQuality } from '../../base/conference'; import { translate } from '../../base/i18n'; import JitsiMeetJS from '../../base/lib-jitsi-meet'; @@ -26,6 +23,22 @@ const { LOW } = VIDEO_QUALITY_LEVELS; +/** + * Creates an analytics event for a press of one of the buttons in the video + * quality dialog. + * + * @param {string} quality - The quality which was selected. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. + */ +const createEvent = function(quality) { + return createToolbarEvent( + 'video.quality', + { + quality + }); +}; + /** * Implements a React {@link Component} which displays a dialog with a slider * for selecting a new receive video quality. @@ -255,12 +268,13 @@ class VideoQualityDialog extends Component { * @returns {void} */ _enableAudioOnly() { - sendAnalyticsEvent(TOOLBAR_AUDIO_ONLY_ENABLED); + sendAnalytics(createEvent('audio.only')); logger.log('Video quality: audio only enabled'); this.props.dispatch(setAudioOnly(true)); } /** + * Handles the action of the high definition video being selected. * Dispatches an action to receive high quality video from remote * participants. * @@ -268,7 +282,7 @@ class VideoQualityDialog extends Component { * @returns {void} */ _enableHighDefinition() { - sendAnalyticsEvent(TOOLBAR_VIDEO_QUALITY_HIGH); + sendAnalytics(createEvent('high')); logger.log('Video quality: high enabled'); this.props.dispatch(setReceiveVideoQuality(HIGH)); } @@ -281,7 +295,7 @@ class VideoQualityDialog extends Component { * @returns {void} */ _enableLowDefinition() { - sendAnalyticsEvent(TOOLBAR_VIDEO_QUALITY_LOW); + sendAnalytics(createEvent('low')); logger.log('Video quality: low enabled'); this.props.dispatch(setReceiveVideoQuality(LOW)); } @@ -294,7 +308,7 @@ class VideoQualityDialog extends Component { * @returns {void} */ _enableStandardDefinition() { - sendAnalyticsEvent(TOOLBAR_VIDEO_QUALITY_STANDARD); + sendAnalytics(createEvent('standard')); logger.log('Video quality: standard enabled'); this.props.dispatch(setReceiveVideoQuality(STANDARD)); }