Restructures the analytics events (#2333)

* ref: Restructures the pinned/unpinned events.

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

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

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

* ref: Refactors the "initially muted" events.

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

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

* ref: Restructures the "shared video" events.

* ref: Restructures the "start muted" events.

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

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

* ref: Restructures the "callkit" events.

* ref: Restructures the "replace track".

* ref: Restructures keyboard shortcuts events.

* ref: Restructures most of the toolbar events.

* ref: Refactors the API events.

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

* ref: Refactors the "device changed" events.

* ref: Refactors the page reload event.

* ref: Removes an unused function.

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

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

* ref: Refactors the events from the profile pane.

* ref: Restructures the recording-related events.

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

* ref: Cleans up AnalyticsEvents.js.

* ref: Removes an unused function and adds documentation.

* feat: Adds events for all API calls.

* fix: Addresses feedback.

* fix: Brings back mistakenly removed code.

* fix: Simplifies code and fixes a bug in toggleFilmstrip

when the 'visible' parameter is defined.

* feat: Removes the resolution change application log.

* ref: Uses consistent naming for events' attributes.

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

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

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

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

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

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

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

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

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

* ref: Uses "value" instead of "delay" since this is friendlier to GA.
This commit is contained in:
bgrozev 2018-01-03 15:24:07 -06:00 committed by virtuacoplenny
parent d08bbae770
commit 090f2f9ccb
35 changed files with 988 additions and 966 deletions

View File

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

146
analytics-ga.js Normal file
View File

@ -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 */

View File

@ -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);

View File

@ -16,19 +16,13 @@ import UIUtil from './modules/UI/util/UIUtil';
import * as JitsiMeetConferenceEvents from './ConferenceEvents'; import * as JitsiMeetConferenceEvents from './ConferenceEvents';
import { import {
CONFERENCE_AUDIO_INITIALLY_MUTED, createDeviceChangedEvent,
CONFERENCE_SHARING_DESKTOP_START, createScreenSharingEvent,
CONFERENCE_SHARING_DESKTOP_STOP, createSelectParticipantFailedEvent,
CONFERENCE_VIDEO_INITIALLY_MUTED, createStreamSwitchDelayEvent,
DEVICE_LIST_CHANGED_AUDIO_MUTED, createTrackMutedEvent,
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,
initAnalytics, initAnalytics,
sendAnalyticsEvent sendAnalytics
} from './react/features/analytics'; } from './react/features/analytics';
import EventEmitter from 'events'; import EventEmitter from 'events';
@ -741,14 +735,13 @@ export default {
}) })
.then(([ tracks, con ]) => { .then(([ tracks, con ]) => {
tracks.forEach(track => { tracks.forEach(track => {
if (track.isAudioTrack() && this.isLocalAudioMuted()) { if ((track.isAudioTrack() && this.isLocalAudioMuted())
sendAnalyticsEvent(CONFERENCE_AUDIO_INITIALLY_MUTED); || (track.isVideoTrack() && this.isLocalVideoMuted())) {
logger.log('Audio mute: initially muted'); const mediaType = track.getType();
track.mute();
} else if (track.isVideoTrack() sendAnalytics(
&& this.isLocalVideoMuted()) { createTrackMutedEvent(mediaType, 'initial mute'));
sendAnalyticsEvent(CONFERENCE_VIDEO_INITIALLY_MUTED); logger.log(`${mediaType} mute: initially muted.`);
logger.log('Video mute: initially muted');
track.mute(); track.mute();
} }
}); });
@ -1453,8 +1446,9 @@ export default {
promise = createLocalTracksF({ devices: [ 'video' ] }) promise = createLocalTracksF({ devices: [ 'video' ] })
.then(([ stream ]) => this.useVideoStream(stream)) .then(([ stream ]) => this.useVideoStream(stream))
.then(() => { .then(() => {
sendAnalyticsEvent(CONFERENCE_SHARING_DESKTOP_STOP); sendAnalytics(createScreenSharingEvent('stopped'));
logger.log('switched back to local video'); logger.log('Screen sharing stopped, switching to video.');
if (!this.localVideo && wasVideoMuted) { if (!this.localVideo && wasVideoMuted) {
return Promise.reject('No local video to be muted!'); return Promise.reject('No local video to be muted!');
} else if (wasVideoMuted && this.localVideo) { } 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. * replacing it with a desktop one.
* *
* @param {Object} [options] - Screen sharing options that will be passed to * @param {Object} [options] - Screen sharing options that will be passed to
@ -1632,8 +1626,8 @@ export default {
.then(stream => this.useVideoStream(stream)) .then(stream => this.useVideoStream(stream))
.then(() => { .then(() => {
this.videoSwitchInProgress = false; this.videoSwitchInProgress = false;
sendAnalyticsEvent(CONFERENCE_SHARING_DESKTOP_START); sendAnalytics(createScreenSharingEvent('started'));
logger.log('sharing local desktop'); logger.log('Screen sharing started');
}) })
.catch(error => { .catch(error => {
this.videoSwitchInProgress = false; this.videoSwitchInProgress = false;
@ -1928,7 +1922,7 @@ export default {
room.selectParticipant(id); room.selectParticipant(id);
} catch (e) { } catch (e) {
sendAnalyticsEvent(SELECT_PARTICIPANT_FAILED); sendAnalytics(createSelectParticipantFailedEvent(e));
reportError(e); reportError(e);
} }
}); });
@ -2152,22 +2146,12 @@ export default {
APP.UI.addListener( APP.UI.addListener(
UIEvents.RESOLUTION_CHANGED, UIEvents.RESOLUTION_CHANGED,
(id, oldResolution, newResolution, delay) => { (id, oldResolution, newResolution, delay) => {
const logObject = { sendAnalytics(createStreamSwitchDelayEvent(
id: 'resolution_change', {
participant: id, 'old_resolution': oldResolution,
oldValue: oldResolution, 'new_resolution': newResolution,
newValue: newResolution, value: delay
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 });
}
}); });
/* eslint-enable max-params */ /* eslint-enable max-params */
@ -2193,7 +2177,7 @@ export default {
cameraDeviceId => { cameraDeviceId => {
const videoWasMuted = this.isLocalVideoMuted(); const videoWasMuted = this.isLocalVideoMuted();
sendAnalyticsEvent(SETTINGS_CHANGE_DEVICE_VIDEO); sendAnalytics(createDeviceChangedEvent('video', 'input'));
createLocalTracksF({ createLocalTracksF({
devices: [ 'video' ], devices: [ 'video' ],
cameraDeviceId, cameraDeviceId,
@ -2232,7 +2216,7 @@ export default {
micDeviceId => { micDeviceId => {
const audioWasMuted = this.isLocalAudioMuted(); const audioWasMuted = this.isLocalAudioMuted();
sendAnalyticsEvent(SETTINGS_CHANGE_DEVICE_AUDIO_IN); sendAnalytics(createDeviceChangedEvent('audio', 'input'));
createLocalTracksF({ createLocalTracksF({
devices: [ 'audio' ], devices: [ 'audio' ],
cameraDeviceId: null, cameraDeviceId: null,
@ -2262,7 +2246,7 @@ export default {
APP.UI.addListener( APP.UI.addListener(
UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED,
audioOutputDeviceId => { audioOutputDeviceId => {
sendAnalyticsEvent(SETTINGS_CHANGE_DEVICE_AUDIO_OUT); sendAnalytics(createDeviceChangedEvent('audio', 'output'));
APP.settings.setAudioOutputDeviceId(audioOutputDeviceId) APP.settings.setAudioOutputDeviceId(audioOutputDeviceId)
.then(() => logger.log('changed audio output device')) .then(() => logger.log('changed audio output device'))
.catch(err => { .catch(err => {
@ -2528,7 +2512,9 @@ export default {
// If audio was muted before, or we unplugged current device // If audio was muted before, or we unplugged current device
// and selected new one, then mute new audio track. // and selected new one, then mute new audio track.
if (audioWasMuted) { if (audioWasMuted) {
sendAnalyticsEvent(DEVICE_LIST_CHANGED_AUDIO_MUTED); sendAnalytics(createTrackMutedEvent(
'audio',
'device list changed'));
logger.log('Audio mute: device list changed'); logger.log('Audio mute: device list changed');
muteLocalAudio(true); muteLocalAudio(true);
} }
@ -2536,7 +2522,9 @@ export default {
// If video was muted before, or we unplugged current device // If video was muted before, or we unplugged current device
// and selected new one, then mute new video track. // and selected new one, then mute new video track.
if (!this.isSharingScreen && videoWasMuted) { if (!this.isSharingScreen && videoWasMuted) {
sendAnalyticsEvent(DEVICE_LIST_CHANGED_VIDEO_MUTED); sendAnalytics(createTrackMutedEvent(
'video',
'device list changed'));
logger.log('Video mute: device list changed'); logger.log('Video mute: device list changed');
muteLocalVideo(true); 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. * Disconnect from the conference and optionally request user feedback.
* @param {boolean} [requestFeedback=false] if user feedback should be * @param {boolean} [requestFeedback=false] if user feedback should be

View File

@ -307,21 +307,24 @@ var config = {
// backToP2PDelay: 5 // 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 // Information about the jitsi-meet instance we are connecting to, including
// the user region as seen by the server. // the user region as seen by the server.
//
deploymentInfo: { deploymentInfo: {
// shard: "shard1", // shard: "shard1",
// region: "europe", // region: "europe",
// userRegion: "asia" // userRegion: "asia"
} }
// List of undocumented settings used in jitsi-meet // List of undocumented settings used in jitsi-meet
/** /**
alwaysVisibleToolbar alwaysVisibleToolbar
analyticsScriptUrls
autoEnableDesktopSharing autoEnableDesktopSharing
autoRecord autoRecord
autoRecordToken autoRecordToken

View File

@ -3,9 +3,8 @@
import * as JitsiMeetConferenceEvents from '../../ConferenceEvents'; import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
import { parseJWTFromURLParams } from '../../react/features/base/jwt'; import { parseJWTFromURLParams } from '../../react/features/base/jwt';
import { import {
API_TOGGLE_AUDIO, createApiEvent,
API_TOGGLE_VIDEO, sendAnalytics
sendAnalyticsEvent
} from '../../react/features/analytics'; } from '../../react/features/analytics';
import { getJitsiMeetTransport } from '../transport'; import { getJitsiMeetTransport } from '../transport';
@ -56,25 +55,48 @@ let videoAvailable = true;
*/ */
function initCommands() { function initCommands() {
commands = { commands = {
'display-name': 'display-name': displayName => {
APP.conference.changeLocalDisplayName.bind(APP.conference), sendAnalytics(createApiEvent('display.name.changed'));
APP.conference.changeLocalDisplayName(displayName);
},
'toggle-audio': () => { 'toggle-audio': () => {
sendAnalyticsEvent(API_TOGGLE_AUDIO); sendAnalytics(createApiEvent('toggle-audio'));
logger.log('Audio toggle: API command received'); logger.log('Audio toggle: API command received');
APP.conference.toggleAudioMuted(false /* no UI */); APP.conference.toggleAudioMuted(false /* no UI */);
}, },
'toggle-video': () => { 'toggle-video': () => {
sendAnalyticsEvent(API_TOGGLE_VIDEO); sendAnalytics(createApiEvent('toggle-video'));
logger.log('Video toggle: API command received'); logger.log('Video toggle: API command received');
APP.conference.toggleVideoMuted(false /* no UI */); APP.conference.toggleVideoMuted(false /* no UI */);
}, },
'toggle-film-strip': APP.UI.toggleFilmstrip, 'toggle-film-strip': () => {
'toggle-chat': APP.UI.toggleChat, sendAnalytics(createApiEvent('film.strip.toggled'));
'toggle-contact-list': APP.UI.toggleContactList, APP.UI.toggleFilmstrip();
'toggle-share-screen': toggleScreenSharing, },
'video-hangup': () => APP.conference.hangup(true), 'toggle-chat': () => {
'email': APP.conference.changeLocalEmail, sendAnalytics(createApiEvent('chat.toggled'));
'avatar-url': APP.conference.changeLocalAvatarUrl 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 }) => { transport.on('event', ({ data, name }) => {
if (name && commands[name]) { if (name && commands[name]) {

View File

@ -142,6 +142,32 @@ UI.toggleFullScreen = function() {
UIUtil.isFullScreen() ? UIUtil.exitFullScreen() : UIUtil.enterFullScreen(); 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. * Notify user that server has shut down.
*/ */

View File

@ -24,11 +24,9 @@ import {
JitsiRecordingStatus JitsiRecordingStatus
} from '../../../react/features/base/lib-jitsi-meet'; } from '../../../react/features/base/lib-jitsi-meet';
import { import {
RECORDING_CANCELED, createToolbarEvent,
RECORDING_CLICKED, createRecordingDialogEvent,
RECORDING_STARTED, sendAnalytics
RECORDING_STOPPED,
sendAnalyticsEvent
} from '../../../react/features/analytics'; } from '../../../react/features/analytics';
import { setToolboxEnabled } from '../../../react/features/toolbox'; import { setToolboxEnabled } from '../../../react/features/toolbox';
import { setNotificationsEnabled } from '../../../react/features/notifications'; import { setNotificationsEnabled } from '../../../react/features/notifications';
@ -452,12 +450,13 @@ const Recording = {
}, },
// checks whether recording is enabled and whether we have params // 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() { checkAutoRecord() {
if (_isRecordingButtonEnabled && config.autoRecord) { if (_isRecordingButtonEnabled && config.autoRecord) {
this.predefinedToken = UIUtil.escapeHtml(config.autoRecordToken); this.predefinedToken = UIUtil.escapeHtml(config.autoRecordToken);
this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED, this.eventEmitter.emit(
this.predefinedToken); UIEvents.RECORDING_TOGGLED,
{ token: this.predefinedToken });
} }
}, },
@ -467,11 +466,16 @@ const Recording = {
* @returns {void} * @returns {void}
*/ */
_onToolbarButtonClick() { _onToolbarButtonClick() {
sendAnalytics(createToolbarEvent(
'recording.button',
{
'dialog_present': Boolean(dialog)
}));
if (dialog) { if (dialog) {
return; return;
} }
sendAnalyticsEvent(RECORDING_CLICKED);
switch (this.currentState) { switch (this.currentState) {
case JitsiRecordingStatus.ON: case JitsiRecordingStatus.ON:
case JitsiRecordingStatus.RETRYING: case JitsiRecordingStatus.RETRYING:
@ -479,7 +483,13 @@ const Recording = {
_showStopRecordingPrompt(this.recordingType).then( _showStopRecordingPrompt(this.recordingType).then(
() => { () => {
this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED); 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 () => {}); // eslint-disable-line no-empty-function
break; break;
@ -492,21 +502,32 @@ const Recording = {
this.eventEmitter.emit( this.eventEmitter.emit(
UIEvents.RECORDING_TOGGLED, UIEvents.RECORDING_TOGGLED,
{ streamId }); { streamId });
sendAnalyticsEvent(RECORDING_STARTED);
// The confirm button on the start recording dialog was
// clicked
sendAnalytics(
createRecordingDialogEvent(
'start',
'confirm.button'));
}) })
.catch(reason => { .catch(reason => {
if (reason === APP.UI.messageHandler.CANCEL) { 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 { } else {
logger.error(reason); logger.error(reason);
} }
}); });
} else { } else {
// Note that we only fire analytics events for Jibri.
if (this.predefinedToken) { if (this.predefinedToken) {
this.eventEmitter.emit( this.eventEmitter.emit(
UIEvents.RECORDING_TOGGLED, UIEvents.RECORDING_TOGGLED,
{ token: this.predefinedToken }); { token: this.predefinedToken });
sendAnalyticsEvent(RECORDING_STARTED);
return; return;
} }
@ -515,12 +536,9 @@ const Recording = {
this.eventEmitter.emit( this.eventEmitter.emit(
UIEvents.RECORDING_TOGGLED, UIEvents.RECORDING_TOGGLED,
{ token }); { token });
sendAnalyticsEvent(RECORDING_STARTED);
}) })
.catch(reason => { .catch(reason => {
if (reason === APP.UI.messageHandler.CANCEL) { if (reason !== APP.UI.messageHandler.CANCEL) {
sendAnalyticsEvent(RECORDING_CANCELED);
} else {
logger.error(reason); logger.error(reason);
} }
}); });

View File

@ -11,15 +11,8 @@ import LargeContainer from '../videolayout/LargeContainer';
import Filmstrip from '../videolayout/Filmstrip'; import Filmstrip from '../videolayout/Filmstrip';
import { import {
SHARED_VIDEO_ALREADY_SHARED, createSharedVideoEvent as createEvent,
SHARED_VIDEO_AUDIO_MUTED, sendAnalytics
SHARED_VIDEO_AUDIO_UNMUTED,
SHARED_VIDEO_CANCELED,
SHARED_VIDEO_PAUSED,
SHARED_VIDEO_STARTED,
SHARED_VIDEO_STOPPED,
SHARED_VIDEO_VOLUME_CHANGED,
sendAnalyticsEvent
} from '../../../react/features/analytics'; } from '../../../react/features/analytics';
import { import {
participantJoined, participantJoined,
@ -95,11 +88,11 @@ export default class SharedVideoManager {
url => { url => {
this.emitter.emit( this.emitter.emit(
UIEvents.UPDATE_SHARED_VIDEO, url, 'start'); UIEvents.UPDATE_SHARED_VIDEO, url, 'start');
sendAnalyticsEvent(SHARED_VIDEO_STARTED); sendAnalytics(createEvent('started'));
}, },
err => { err => {
logger.log('SHARED VIDEO CANCELED', 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( this.emitter.emit(
UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop'); UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop');
sendAnalyticsEvent(SHARED_VIDEO_STOPPED); sendAnalytics(createEvent('stopped'));
}, },
() => {}); // eslint-disable-line no-empty-function () => {}); // eslint-disable-line no-empty-function
} else { } else {
@ -127,7 +120,7 @@ export default class SharedVideoManager {
descriptionKey: 'dialog.alreadySharedVideoMsg', descriptionKey: 'dialog.alreadySharedVideoMsg',
titleKey: 'dialog.alreadySharedVideoTitle' 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 // eslint-disable-next-line eqeqeq
} else if (event.data == YT.PlayerState.PAUSED) { } else if (event.data == YT.PlayerState.PAUSED) {
self.smartAudioUnmute(); self.smartAudioUnmute();
sendAnalyticsEvent(SHARED_VIDEO_PAUSED); sendAnalytics(createEvent('paused'));
} }
// eslint-disable-next-line eqeqeq // eslint-disable-next-line eqeqeq
self.fireSharedVideoEvent(event.data == YT.PlayerState.PAUSED); self.fireSharedVideoEvent(event.data == YT.PlayerState.PAUSED);
@ -268,7 +261,12 @@ export default class SharedVideoManager {
} else if (event.data.volume <= 0 || event.data.muted) { } else if (event.data.volume <= 0 || event.data.muted) {
self.smartAudioUnmute(); self.smartAudioUnmute();
} }
sendAnalyticsEvent(SHARED_VIDEO_VOLUME_CHANGED); sendAnalytics(createEvent(
'volume.changed',
{
volume: event.data.volume,
muted: event.data.muted
}));
}; };
window.onPlayerReady = function(event) { window.onPlayerReady = function(event) {
@ -434,8 +432,8 @@ export default class SharedVideoManager {
} }
/** /**
* Updates video, if its not playing and needs starting or * Updates video, if it's not playing and needs starting or if it's playing
* if its playing and needs to be paysed * and needs to be paused.
* @param id the id of the sender of the command * @param id the id of the sender of the command
* @param url the video url * @param url the video url
* @param attributes * @param attributes
@ -574,7 +572,7 @@ export default class SharedVideoManager {
if (APP.conference.isLocalAudioMuted() if (APP.conference.isLocalAudioMuted()
&& !this.mutedWithUserInteraction && !this.mutedWithUserInteraction
&& !this.isSharedVideoVolumeOn()) { && !this.isSharedVideoVolumeOn()) {
sendAnalyticsEvent(SHARED_VIDEO_AUDIO_UNMUTED); sendAnalytics(createEvent('audio.unmuted'));
logger.log('Shared video: audio unmuted'); logger.log('Shared video: audio unmuted');
this.emitter.emit(UIEvents.AUDIO_MUTED, false, false); this.emitter.emit(UIEvents.AUDIO_MUTED, false, false);
this.showMicMutedPopup(false); this.showMicMutedPopup(false);
@ -588,7 +586,7 @@ export default class SharedVideoManager {
smartAudioMute() { smartAudioMute() {
if (!APP.conference.isLocalAudioMuted() if (!APP.conference.isLocalAudioMuted()
&& this.isSharedVideoVolumeOn()) { && this.isSharedVideoVolumeOn()) {
sendAnalyticsEvent(SHARED_VIDEO_AUDIO_MUTED); sendAnalytics(createEvent('audio.muted'));
logger.log('Shared video: audio muted'); logger.log('Shared video: audio muted');
this.emitter.emit(UIEvents.AUDIO_MUTED, true, false); this.emitter.emit(UIEvents.AUDIO_MUTED, true, false);
this.showMicMutedPopup(true); this.showMicMutedPopup(true);

View File

@ -4,9 +4,8 @@ import UIEvents from '../../../../service/UI/UIEvents';
import Settings from '../../../settings/Settings'; import Settings from '../../../settings/Settings';
import { import {
AUTHENTICATE_LOGIN_CLICKED, createProfilePanelButtonEvent,
AUTHENTICATE_LOGOUT_CLICKED, sendAnalytics
sendAnalyticsEvent
} from '../../../../react/features/analytics'; } from '../../../../react/features/analytics';
const sidePanelsContainerId = 'sideToolbarContainer'; const sidePanelsContainerId = 'sideToolbarContainer';
@ -95,7 +94,7 @@ export default {
* *
*/ */
function loginClicked() { function loginClicked() {
sendAnalyticsEvent(AUTHENTICATE_LOGIN_CLICKED); sendAnalytics(createProfilePanelButtonEvent('login.button'));
emitter.emit(UIEvents.AUTH_CLICKED); emitter.emit(UIEvents.AUTH_CLICKED);
} }
@ -108,7 +107,7 @@ export default {
const titleKey = 'dialog.logoutTitle'; const titleKey = 'dialog.logoutTitle';
const msgKey = 'dialog.logoutQuestion'; const msgKey = 'dialog.logoutQuestion';
sendAnalyticsEvent(AUTHENTICATE_LOGOUT_CLICKED); sendAnalytics(createProfilePanelButtonEvent('logout.button'));
// Ask for confirmation // Ask for confirmation
APP.UI.messageHandler.openTwoButtonDialog({ APP.UI.messageHandler.openTwoButtonDialog({

View File

@ -6,8 +6,9 @@ import UIEvents from '../../../service/UI/UIEvents';
import UIUtil from '../util/UIUtil'; import UIUtil from '../util/UIUtil';
import { import {
TOOLBAR_FILMSTRIP_TOGGLED, createShortcutEvent,
sendAnalyticsEvent createToolbarEvent,
sendAnalytics
} from '../../../react/features/analytics'; } from '../../../react/features/analytics';
const Filmstrip = { const Filmstrip = {
@ -75,8 +76,18 @@ const Filmstrip = {
// Firing the event instead of executing toggleFilmstrip method because // Firing the event instead of executing toggleFilmstrip method because
// it's important to hide the filmstrip by UI.toggleFilmstrip in order // it's important to hide the filmstrip by UI.toggleFilmstrip in order
// to correctly resize the video area. // to correctly resize the video area.
$('#toggleFilmstripButton').on('click', $('#toggleFilmstripButton').on(
() => this.eventEmitter.emit(UIEvents.TOGGLE_FILMSTRIP)); '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(); this._registerToggleFilmstripShortcut();
}, },
@ -94,7 +105,14 @@ const Filmstrip = {
// Firing the event instead of executing toggleFilmstrip method because // Firing the event instead of executing toggleFilmstrip method because
// it's important to hide the filmstrip by UI.toggleFilmstrip in order // it's important to hide the filmstrip by UI.toggleFilmstrip in order
// to correctly resize the video area. // 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( APP.keyboardshortcut.registerShortcut(
shortcut, 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 * @param visible optional {Boolean} which specifies the desired visibility
* of the filmstrip. If not specified, the visibility will be flipped * of the filmstrip. If not specified, the visibility will be flipped
* (i.e. toggled); otherwise, the visibility will be set to the specified * (i.e. toggled); otherwise, the visibility will be set to the specified
* value. * value.
* @param {Boolean} sendAnalytics - True to send an analytics event. The
* default value is true.
* *
* Note: * Note:
* This method shouldn't be executed directly to hide the filmstrip. * This method shouldn't be executed directly to hide the filmstrip.
* It's important to hide the filmstrip with UI.toggleFilmstrip in order * It's important to hide the filmstrip with UI.toggleFilmstrip in order
* to correctly resize the video area. * to correctly resize the video area.
*/ */
toggleFilmstrip(visible, sendAnalytics = true) { toggleFilmstrip(visible) {
const isVisibleDefined = typeof visible === 'boolean'; const wasFilmstripVisible = this.isFilmstripVisible();
if (!isVisibleDefined) { // If 'visible' is defined and matches the current state, we have
// eslint-disable-next-line no-param-reassign // nothing to do. Otherwise (regardless of whether 'visible' is defined)
visible = this.isFilmstripVisible(); // we need to toggle the state.
} else if (this.isFilmstripVisible() === visible) { if (visible === wasFilmstripVisible) {
return; return;
} }
if (sendAnalytics) {
sendAnalyticsEvent(TOOLBAR_FILMSTRIP_TOGGLED);
}
this.filmstrip.toggleClass('hidden'); this.filmstrip.toggleClass('hidden');
if (visible) { if (wasFilmstripVisible) {
this.showMenuUpIcon(); this.showMenuUpIcon();
} else { } else {
this.showMenuDownIcon(); this.showMenuDownIcon();
} }
// Emit/fire UIEvents.TOGGLED_FILMSTRIP. if (this.eventEmitter) {
const eventEmitter = this.eventEmitter; this.eventEmitter.emit(
const isFilmstripVisible = this.isFilmstripVisible();
if (eventEmitter) {
eventEmitter.emit(
UIEvents.TOGGLED_FILMSTRIP, UIEvents.TOGGLED_FILMSTRIP,
this.isFilmstripVisible()); !wasFilmstripVisible);
} }
APP.store.dispatch(setFilmstripVisibility(isFilmstripVisible)); APP.store.dispatch(setFilmstripVisibility(!wasFilmstripVisible));
}, },
/** /**

View File

@ -2,11 +2,10 @@
import { toggleDialog } from '../../react/features/base/dialog'; import { toggleDialog } from '../../react/features/base/dialog';
import { import {
SHORTCUT_HELP, ACTION_SHORTCUT_PRESSED as PRESSED,
SHORTCUT_SPEAKER_STATS_CLICKED, ACTION_SHORTCUT_RELEASED as RELEASED,
SHORTCUT_TALK_CLICKED, createShortcutEvent,
SHORTCUT_TALK_RELEASED, sendAnalytics
sendAnalyticsEvent
} from '../../react/features/analytics'; } from '../../react/features/analytics';
import { KeyboardShortcutsDialog } import { KeyboardShortcutsDialog }
from '../../react/features/keyboard-shortcuts'; from '../../react/features/keyboard-shortcuts';
@ -72,8 +71,10 @@ const KeyboardShortcut = {
|| $(':focus').is('textarea'))) { || $(':focus').is('textarea'))) {
if (this._getKeyboardKey(e).toUpperCase() === ' ') { if (this._getKeyboardKey(e).toUpperCase() === ' ') {
if (APP.conference.isLocalAudioMuted()) { if (APP.conference.isLocalAudioMuted()) {
sendAnalyticsEvent(SHORTCUT_TALK_RELEASED); sendAnalytics(createShortcutEvent(
logger.log('Talk shortcut released'); 'push.to.talk',
PRESSED));
logger.log('Talk shortcut pressed');
APP.conference.muteAudio(false); APP.conference.muteAudio(false);
} }
} }
@ -93,7 +94,7 @@ const KeyboardShortcut = {
* Registers a new shortcut. * Registers a new shortcut.
* *
* @param shortcutChar the shortcut character triggering the action * @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 to this shortcut and used to show the shortcut character on the
* element tooltip * element tooltip
* @param exec the function to be executed when the shortcut is pressed * @param exec the function to be executed when the shortcut is pressed
@ -175,7 +176,7 @@ const KeyboardShortcut = {
*/ */
_initGlobalShortcuts() { _initGlobalShortcuts() {
this.registerShortcut('?', null, () => { this.registerShortcut('?', null, () => {
sendAnalyticsEvent(SHORTCUT_HELP); sendAnalytics(createShortcutEvent('help'));
APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, { APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
shortcutDescriptions: _shortcutsHelp shortcutDescriptions: _shortcutsHelp
})); }));
@ -184,15 +185,15 @@ const KeyboardShortcut = {
// register SPACE shortcut in two steps to insure visibility of help // register SPACE shortcut in two steps to insure visibility of help
// message // message
this.registerShortcut(' ', null, () => { this.registerShortcut(' ', null, () => {
sendAnalyticsEvent(SHORTCUT_TALK_CLICKED); sendAnalytics(createShortcutEvent('push.to.talk', RELEASED));
logger.log('Talk shortcut pressed'); logger.log('Talk shortcut released');
APP.conference.muteAudio(true); APP.conference.muteAudio(true);
}); });
this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk'); this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
if (!interfaceConfig.filmStripOnly) { if (!interfaceConfig.filmStripOnly) {
this.registerShortcut('T', null, () => { this.registerShortcut('T', null, () => {
sendAnalyticsEvent(SHORTCUT_SPEAKER_STATS_CLICKED); sendAnalytics(createShortcutEvent('speaker.stats'));
APP.store.dispatch(toggleDialog(SpeakerStats, { APP.store.dispatch(toggleDialog(SpeakerStats, {
conference: APP.conference conference: APP.conference
})); }));

View File

@ -37,20 +37,20 @@ export default class JitsiMeetLogStorage {
return; return;
} }
let logJSON = `{"log${this.counter}":"\n`; let logMessage = `{"log${this.counter}":"\n`;
for (let i = 0, len = logEntries.length; i < len; i++) { for (let i = 0, len = logEntries.length; i < len; i++) {
const logEntry = logEntries[i]; const logEntry = logEntries[i];
if (typeof logEntry === 'object') { if (typeof logEntry === 'object') {
// Aggregated message // Aggregated message
logJSON += `(${logEntry.count}) ${logEntry.text}\n`; logMessage += `(${logEntry.count}) ${logEntry.text}\n`;
} else { } else {
// Regular message // Regular message
logJSON += `${logEntry}\n`; logMessage += `${logEntry}\n`;
} }
} }
logJSON += '"}'; logMessage += '"}';
this.counter += 1; this.counter += 1;
@ -58,11 +58,11 @@ export default class JitsiMeetLogStorage {
// on the way that could be uninitialized if the storeLogs // on the way that could be uninitialized if the storeLogs
// attempt would be made very early (which is unlikely) // attempt would be made very early (which is unlikely)
try { try {
APP.conference.logJSON(logJSON); APP.conference.room.sendApplicationLog(logMessage);
} catch (error) { } catch (error) {
// NOTE console is intentional here // NOTE console is intentional here
console.error( console.error(
'Failed to store the logs: ', logJSON, error); 'Failed to store the logs: ', logMessage, error);
} }
} }
} }

2
package-lock.json generated
View File

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

View File

@ -44,7 +44,7 @@
"jquery-i18next": "1.2.0", "jquery-i18next": "1.2.0",
"js-md5": "0.6.1", "js-md5": "0.6.1",
"jwt-decode": "2.2.0", "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", "lodash": "4.17.4",
"moment": "2.19.4", "moment": "2.19.4",
"nuclear-js": "1.4.0", "nuclear-js": "1.4.0",

File diff suppressed because it is too large Load Diff

View File

@ -9,12 +9,14 @@ import { getJitsiMeetGlobalNS, loadScript } from '../base/util';
const logger = require('jitsi-meet-logger').getLogger(__filename); 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<any>) { export function sendAnalytics(event: Object) {
analytics.sendEvent(...args); analytics.sendEvent(event);
} }
/** /**
@ -38,23 +40,17 @@ export function initAnalytics({ getState }: { getState: Function }) {
const state = getState(); const state = getState();
const config = state['features/base/config']; const config = state['features/base/config'];
const { analyticsScriptUrls } = config; const { analyticsScriptUrls } = config;
const machineId = JitsiMeetJS.getMachineId();
const { user } = state['features/base/jwt']; const { user } = state['features/base/jwt'];
const handlerConstructorOptions = { const handlerConstructorOptions = {
product: 'lib-jitsi-meet',
version: JitsiMeetJS.version, version: JitsiMeetJS.version,
session: machineId, user
user: user ? user.id : `uid-${machineId}`,
server: state['features/base/connection'].locationURL.host
}; };
_loadHandlers(analyticsScriptUrls, handlerConstructorOptions) _loadHandlers(analyticsScriptUrls, handlerConstructorOptions)
.then(handlers => { .then(handlers => {
const permanentProperties: Object = { const roomName = state['features/base/conference'].room;
roomName: state['features/base/conference'].room,
userAgent: navigator.userAgent
};
const { group, server } = state['features/base/jwt']; const { group, server } = state['features/base/jwt'];
const permanentProperties = {};
if (server) { if (server) {
permanentProperties.server = server; permanentProperties.server = server;
@ -76,6 +72,9 @@ export function initAnalytics({ getState }: { getState: Function }) {
} }
analytics.addPermanentProperties(permanentProperties); analytics.addPermanentProperties(permanentProperties);
analytics.setConferenceName(roomName);
// Set the handlers last, since this triggers emptying of the cache
analytics.setAnalyticsHandlers(handlers); analytics.setAnalyticsHandlers(handlers);
}, },
error => analytics.dispose() && logger.error(error)); error => analytics.dispose() && logger.error(error));

View File

@ -3,9 +3,8 @@
import UIEvents from '../../../../service/UI/UIEvents'; import UIEvents from '../../../../service/UI/UIEvents';
import { import {
START_MUTED_SERVER_AUDIO_, createStartMutedConfigurationEvent,
START_MUTED_SERVER_VIDEO_, sendAnalytics
sendAnalyticsEvent
} from '../../analytics'; } from '../../analytics';
import { getName } from '../../app'; import { getName } from '../../app';
import { JitsiConferenceEvents } from '../lib-jitsi-meet'; import { JitsiConferenceEvents } from '../lib-jitsi-meet';
@ -90,12 +89,8 @@ function _addConferenceListeners(conference, dispatch) {
const audioMuted = Boolean(conference.startAudioMuted); const audioMuted = Boolean(conference.startAudioMuted);
const videoMuted = Boolean(conference.startVideoMuted); const videoMuted = Boolean(conference.startVideoMuted);
sendAnalyticsEvent( sendAnalytics(createStartMutedConfigurationEvent(
`${START_MUTED_SERVER_AUDIO_}.${ 'remote', audioMuted, videoMuted));
audioMuted ? 'muted' : 'unmuted'}`);
sendAnalyticsEvent(
`${START_MUTED_SERVER_VIDEO_}.${
videoMuted ? 'muted' : 'unmuted'}`);
logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${ logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${
videoMuted ? 'video' : ''}`); videoMuted ? 'video' : ''}`);

View File

@ -3,12 +3,11 @@
import UIEvents from '../../../../service/UI/UIEvents'; import UIEvents from '../../../../service/UI/UIEvents';
import { import {
_LOCAL, ACTION_PINNED,
_REMOTE, ACTION_UNPINNED,
AUDIO_ONLY_DISABLED, createAudioOnlyDisableEvent,
PINNED_, createPinnedEvent,
UNPINNED_, sendAnalytics
sendAnalyticsEvent
} from '../../analytics'; } from '../../analytics';
import { CONNECTION_ESTABLISHED } from '../connection'; import { CONNECTION_ESTABLISHED } from '../connection';
import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../media'; import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../media';
@ -131,7 +130,7 @@ function _conferenceFailedOrLeft({ dispatch, getState }, next, action) {
const result = next(action); const result = next(action);
if (getState()['features/base/conference'].audioOnly) { if (getState()['features/base/conference'].audioOnly) {
sendAnalyticsEvent(AUDIO_ONLY_DISABLED); sendAnalytics(createAudioOnlyDisableEvent());
logger.log('Audio only disabled'); logger.log('Audio only disabled');
dispatch(setAudioOnly(false)); dispatch(setAudioOnly(false));
} }
@ -193,19 +192,19 @@ function _pinParticipant(store, next, action) {
if (typeof APP !== 'undefined') { if (typeof APP !== 'undefined') {
const pinnedParticipant = getPinnedParticipant(participants); const pinnedParticipant = getPinnedParticipant(participants);
const actionName = action.participant.id ? PINNED_ : UNPINNED_; const actionName
let videoType; = action.participant.id ? ACTION_PINNED : ACTION_UNPINNED;
const local = (participantById && participantById.local)
|| (!id && pinnedParticipant && pinnedParticipant.local);
if ((participantById && participantById.local) sendAnalytics(createPinnedEvent(
|| (!id && pinnedParticipant && pinnedParticipant.local)) { actionName,
videoType = _LOCAL; local ? 'local' : id,
} else { {
videoType = _REMOTE; 'participant_count': conference.getParticipantCount(),
} local
}));
sendAnalyticsEvent(
`${actionName}.${videoType}`,
{ value: conference.getParticipantCount() });
} }
// The following condition prevents signaling to pin local participant and // The following condition prevents signaling to pin local participant and

View File

@ -1,11 +1,10 @@
/* @flow */ /* @flow */
import { import {
START_AUDIO_ONLY_, createStartAudioOnlyEvent,
START_MUTED_CLIENT_AUDIO_, createStartMutedConfigurationEvent,
START_MUTED_CLIENT_VIDEO_, createSyncTrackStateEvent,
SYNC_TRACK_STATE_, sendAnalytics
sendAnalyticsEvent
} from '../../analytics'; } from '../../analytics';
import { SET_ROOM, setAudioOnly } from '../conference'; import { SET_ROOM, setAudioOnly } from '../conference';
import { parseURLParams } from '../config'; import { parseURLParams } from '../config';
@ -90,12 +89,8 @@ function _setRoom({ dispatch, getState }, next, action) {
audioMuted = Boolean(audioMuted); audioMuted = Boolean(audioMuted);
videoMuted = Boolean(videoMuted); videoMuted = Boolean(videoMuted);
// Apply the config. sendAnalytics(createStartMutedConfigurationEvent(
'local', audioMuted, videoMuted));
sendAnalyticsEvent(
`${START_MUTED_CLIENT_AUDIO_}.${audioMuted ? 'muted' : 'unmuted'}`);
sendAnalyticsEvent(
`${START_MUTED_CLIENT_VIDEO_}.${videoMuted ? 'muted' : 'unmuted'}`);
logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${ logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${
videoMuted ? 'video' : ''}`); videoMuted ? 'video' : ''}`);
@ -128,8 +123,7 @@ function _setRoom({ dispatch, getState }, next, action) {
audioOnly = true; audioOnly = true;
} }
sendAnalyticsEvent( sendAnalytics(createStartAudioOnlyEvent(audioOnly));
`${START_AUDIO_ONLY_}.${audioOnly ? 'enabled' : 'disabled'}`);
logger.log(`Start audio only set to ${audioOnly.toString()}`); logger.log(`Start audio only set to ${audioOnly.toString()}`);
dispatch(setAudioOnly(audioOnly)); dispatch(setAudioOnly(audioOnly));
} }
@ -155,9 +149,7 @@ function _syncTrackMutedState({ getState }, track) {
// not yet in redux state and JitsiTrackEvents.TRACK_MUTE_CHANGED may be // not yet in redux state and JitsiTrackEvents.TRACK_MUTE_CHANGED may be
// fired before track gets to state. // fired before track gets to state.
if (track.muted !== muted) { if (track.muted !== muted) {
sendAnalyticsEvent( sendAnalytics(createSyncTrackStateEvent(track.mediaType, muted));
`${SYNC_TRACK_STATE_}.${track.mediaType}.${
muted ? 'muted' : 'unmuted'}`);
logger.log(`Sync ${track.mediaType} track muted state to ${ logger.log(`Sync ${track.mediaType} track muted state to ${
muted ? 'muted' : 'unmuted'}`); muted ? 'muted' : 'unmuted'}`);
track.muted = muted; track.muted = muted;

View File

@ -1,6 +1,6 @@
import { import {
REPLACE_TRACK_, createTrackMutedEvent,
sendAnalyticsEvent sendAnalytics
} from '../../analytics'; } from '../../analytics';
import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet'; import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
import { import {
@ -220,9 +220,10 @@ export function replaceLocalTrack(oldTrack, newTrack, conference) {
: setAudioMuted; : setAudioMuted;
const isMuted = newTrack.isMuted(); const isMuted = newTrack.isMuted();
sendAnalyticsEvent(`${REPLACE_TRACK_}.${ sendAnalytics(createTrackMutedEvent(
newTrack.getType()}.${ newTrack.getType(),
isMuted ? 'muted' : 'unmuted'}`); 'track.replaced',
isMuted));
logger.log(`Replace ${newTrack.getType()} track - ${ logger.log(`Replace ${newTrack.getType()} track - ${
isMuted ? 'muted' : 'unmuted'}`); isMuted ? 'muted' : 'unmuted'}`);

View File

@ -7,8 +7,8 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
FEEDBACK_OPEN, createFeedbackOpenEvent,
sendAnalyticsEvent sendAnalytics
} from '../../analytics'; } from '../../analytics';
import { Dialog } from '../../base/dialog'; import { Dialog } from '../../base/dialog';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
@ -148,7 +148,7 @@ class FeedbackDialog extends Component {
* @inheritdoc * @inheritdoc
*/ */
componentDidMount() { componentDidMount() {
sendAnalyticsEvent(FEEDBACK_OPEN); sendAnalytics(createFeedbackOpenEvent());
} }
/** /**

View File

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

View File

@ -3,8 +3,8 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
TOOLBAR_INVITE_CLOSE, createInviteDialogClosedEvent,
sendAnalyticsEvent sendAnalytics
} from '../../analytics'; } from '../../analytics';
import { getInviteURL } from '../../base/connection'; import { getInviteURL } from '../../base/connection';
import { Dialog } from '../../base/dialog'; import { Dialog } from '../../base/dialog';
@ -54,7 +54,7 @@ class InviteDialog extends Component {
* @inheritdoc * @inheritdoc
*/ */
componentWillUnmount() { componentWillUnmount() {
sendAnalyticsEvent(TOOLBAR_INVITE_CLOSE); sendAnalytics(createInviteDialogClosedEvent());
} }
/** /**

View File

@ -1,8 +1,8 @@
/* @flow */ /* @flow */
import { import {
CALLKIT_BACKGROUND_VIDEO_MUTED, createTrackMutedEvent,
sendAnalyticsEvent sendAnalytics
} from '../../analytics'; } from '../../analytics';
import { setLastN } from '../../base/conference'; import { setLastN } from '../../base/conference';
import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../../base/media'; import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../../base/media';
@ -46,7 +46,9 @@ export function _setBackgroundVideoMuted(muted: boolean) {
audioOnly || dispatch(setLastN(muted ? 0 : undefined)); audioOnly || dispatch(setLastN(muted ? 0 : undefined));
sendAnalyticsEvent(CALLKIT_BACKGROUND_VIDEO_MUTED); sendAnalytics(createTrackMutedEvent(
'video',
'callkit.background.video'));
dispatch(setVideoMuted(muted, VIDEO_MUTISM_AUTHORITY.BACKGROUND)); dispatch(setVideoMuted(muted, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
}; };

View File

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

View File

@ -3,7 +3,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { PAGE_RELOAD } from '../../analytics'; import {
createPageReloadScheduledEvent,
sendAnalytics
} from '../../analytics';
import { import {
isFatalJitsiConferenceError, isFatalJitsiConferenceError,
isFatalJitsiConnectionError isFatalJitsiConnectionError
@ -159,12 +162,18 @@ export default class AbstractPageReloadOverlay extends Component<*, *> {
// sent to the backed. // sent to the backed.
// FIXME: We should dispatch action for this. // FIXME: We should dispatch action for this.
if (typeof APP !== 'undefined') { if (typeof APP !== 'undefined') {
APP.conference.logEvent( if (APP.conference && APP.conference.room) {
PAGE_RELOAD, APP.conference.room.sendApplicationLog(JSON.stringify(
/* value */ undefined, {
/* label */ this.props.reason); name: 'page.reload',
label: this.props.reason
}));
}
} }
sendAnalytics(createPageReloadScheduledEvent(
this.props.reason, this.state.timeoutSeconds));
logger.info( logger.info(
`The conference will be reloaded after ${ `The conference will be reloaded after ${
this.state.timeoutSeconds} seconds.`); this.state.timeoutSeconds} seconds.`);

View File

@ -3,8 +3,8 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
REMOTE_VIDEO_MENU_KICK, createRemoteVideoMenuButtonEvent,
sendAnalyticsEvent sendAnalytics
} from '../../analytics'; } from '../../analytics';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { kickParticipant } from '../../base/participants'; import { kickParticipant } from '../../base/participants';
@ -86,13 +86,12 @@ class KickButton extends Component {
_onClick() { _onClick() {
const { dispatch, onClick, participantID } = this.props; const { dispatch, onClick, participantID } = this.props;
sendAnalyticsEvent( sendAnalytics(createRemoteVideoMenuButtonEvent(
REMOTE_VIDEO_MENU_KICK, 'kick.button',
{ {
value: 1, 'participant_id': participantID
label: participantID }));
}
);
dispatch(kickParticipant(participantID)); dispatch(kickParticipant(participantID));
if (onClick) { if (onClick) {

View File

@ -3,8 +3,8 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
REMOTE_VIDEO_MENU_MUTE_CLICKED, createRemoteVideoMenuButtonEvent,
sendAnalyticsEvent sendAnalytics
} from '../../analytics'; } from '../../analytics';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { openDialog } from '../../base/dialog'; import { openDialog } from '../../base/dialog';
@ -101,13 +101,11 @@ class MuteButton extends Component {
_onClick() { _onClick() {
const { dispatch, onClick, participantID } = this.props; const { dispatch, onClick, participantID } = this.props;
sendAnalyticsEvent( sendAnalytics(createRemoteVideoMenuButtonEvent(
REMOTE_VIDEO_MENU_MUTE_CLICKED, 'mute.button',
{ {
value: 1, 'participant_id': participantID
label: participantID }));
}
);
dispatch(openDialog(MuteRemoteParticipantDialog, { participantID })); dispatch(openDialog(MuteRemoteParticipantDialog, { participantID }));

View File

@ -6,8 +6,8 @@ import { Dialog } from '../../base/dialog';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { import {
REMOTE_VIDEO_MENU_MUTE_CONFIRMED, createRemoteMuteConfirmedEvent,
sendAnalyticsEvent sendAnalytics
} from '../../analytics'; } from '../../analytics';
import { muteRemoteParticipant } from '../../base/participants'; import { muteRemoteParticipant } from '../../base/participants';
@ -77,18 +77,12 @@ class MuteRemoteParticipantDialog extends Component {
* Handles the submit button action. * Handles the submit button action.
* *
* @private * @private
* @returns {void} * @returns {boolean} - True (to note that the modal should be closed).
*/ */
_onSubmit() { _onSubmit() {
const { dispatch, participantID } = this.props; const { dispatch, participantID } = this.props;
sendAnalyticsEvent( sendAnalytics(createRemoteMuteConfirmedEvent(participantID));
REMOTE_VIDEO_MENU_MUTE_CONFIRMED,
{
value: 1,
label: participantID
}
);
dispatch(muteRemoteParticipant(participantID)); dispatch(muteRemoteParticipant(participantID));

View File

@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { import {
REMOTE_VIDEO_MENU_REMOTE_CONTROL_, createRemoteVideoMenuButtonEvent,
sendAnalyticsEvent sendAnalytics
} from '../../analytics'; } from '../../analytics';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
@ -122,24 +122,19 @@ class RemoteControlButton extends Component {
_onClick() { _onClick() {
const { onClick, participantID, remoteControlState } = this.props; 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) { const enable
eventName = 'stop'; = remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
}
if (remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) { sendAnalytics(createRemoteVideoMenuButtonEvent(
eventName = 'start'; 'remote.control.button',
}
if (eventName) {
sendAnalyticsEvent(
`${REMOTE_VIDEO_MENU_REMOTE_CONTROL_}.${eventName}`,
{ {
value: 1, enable,
label: participantID 'participant_id': participantID
} }));
);
} }
if (onClick) { if (onClick) {

View File

@ -4,7 +4,10 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { TOOLBAR_PROFILE_TOGGLED, sendAnalyticsEvent } from '../../analytics'; import {
createToolbarEvent,
sendAnalytics
} from '../../analytics';
import { import {
getAvatarURL, getAvatarURL,
getLocalParticipant getLocalParticipant
@ -115,7 +118,9 @@ class ProfileButton extends Component<*> {
*/ */
_onClick() { _onClick() {
if (!this.props._unclickable) { 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); APP.UI.emitEvent(UIEvents.TOGGLE_PROFILE);
} }
} }

View File

@ -4,10 +4,10 @@ import { View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
TOOLBAR_AUDIO_MUTED, AUDIO_MUTE,
TOOLBAR_AUDIO_UNMUTED, VIDEO_MUTE,
TOOLBAR_VIDEO_, createToolbarEvent,
sendAnalyticsEvent sendAnalytics
} from '../../analytics'; } from '../../analytics';
import { import {
isNarrowAspectRatio, isNarrowAspectRatio,
@ -188,7 +188,11 @@ class Toolbox extends Component {
_onToggleAudio() { _onToggleAudio() {
const mute = !this.props._audioMuted; 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 // 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 // to change reality by tapping on the respective button i.e. the user
@ -211,7 +215,11 @@ class Toolbox extends Component {
_onToggleVideo() { _onToggleVideo() {
const mute = !this.props._videoMuted; 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 // 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 // to change reality by tapping on the respective button i.e. the user

View File

@ -3,29 +3,12 @@
import React from 'react'; import React from 'react';
import { import {
SHORTCUT_AUDIO_MUTE_TOGGLED, ACTION_SHORTCUT_TRIGGERED as TRIGGERED,
SHORTCUT_CHAT_TOGGLED, AUDIO_MUTE,
SHORTCUT_RAISE_HAND_CLICKED, VIDEO_MUTE,
SHORTCUT_SCREEN_TOGGLED, createShortcutEvent,
SHORTCUT_VIDEO_MUTE_TOGGLED, createToolbarEvent,
TOOLBAR_AUDIO_MUTED, sendAnalytics
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
} from '../analytics'; } from '../analytics';
import { ParticipantCounter } from '../contact-list'; import { ParticipantCounter } from '../contact-list';
import { openDeviceSelectionDialog } from '../device-selection'; import { openDeviceSelectionDialog } from '../device-selection';
@ -63,13 +46,18 @@ export default function getDefaultButtons() {
isDisplayed: () => true, isDisplayed: () => true,
id: 'toolbar_button_camera', id: 'toolbar_button_camera',
onClick() { onClick() {
// TODO: Why is this different from the code which handles
// a keyboard shortcut?
const newVideoMutedState = !APP.conference.isLocalVideoMuted(); const newVideoMutedState = !APP.conference.isLocalVideoMuted();
if (newVideoMutedState) { // The 'enable' attribute in the event is set to true if the
sendAnalyticsEvent(TOOLBAR_VIDEO_ENABLED); // button click triggered a mute action, and set to false if it
} else { // triggered an unmute action.
sendAnalyticsEvent(TOOLBAR_VIDEO_DISABLED); sendAnalytics(createToolbarEvent(
} VIDEO_MUTE,
{
enable: newVideoMutedState
}));
APP.UI.emitEvent(UIEvents.VIDEO_MUTED, newVideoMutedState); APP.UI.emitEvent(UIEvents.VIDEO_MUTED, newVideoMutedState);
}, },
popups: [ popups: [
@ -88,7 +76,13 @@ export default function getDefaultButtons() {
return; 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(); APP.conference.toggleVideoMuted();
}, },
shortcutDescription: 'keyboardShortcuts.videoMute', shortcutDescription: 'keyboardShortcuts.videoMute',
@ -105,13 +99,26 @@ export default function getDefaultButtons() {
<span id = 'unreadMessages' /></span>, <span id = 'unreadMessages' /></span>,
id: 'toolbar_button_chat', id: 'toolbar_button_chat',
onClick() { 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); APP.UI.emitEvent(UIEvents.TOGGLE_CHAT);
}, },
shortcut: 'C', shortcut: 'C',
shortcutAttr: 'toggleChatPopover', shortcutAttr: 'toggleChatPopover',
shortcutFunc() { 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(); APP.UI.toggleChat();
}, },
shortcutDescription: 'keyboardShortcuts.toggleChat', shortcutDescription: 'keyboardShortcuts.toggleChat',
@ -128,7 +135,9 @@ export default function getDefaultButtons() {
enabled: true, enabled: true,
id: 'toolbar_contact_list', id: 'toolbar_contact_list',
onClick() { 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); APP.UI.emitEvent(UIEvents.TOGGLE_CONTACT_LIST);
}, },
sideContainerId: 'contacts_container', sideContainerId: 'contacts_container',
@ -143,11 +152,14 @@ export default function getDefaultButtons() {
enabled: true, enabled: true,
id: 'toolbar_button_desktopsharing', id: 'toolbar_button_desktopsharing',
onClick() { onClick() {
if (APP.conference.isSharingScreen) { // TODO: Why is the button clicked handled differently that
sendAnalyticsEvent(TOOLBAR_SCREEN_DISABLED); // a keyboard shortcut press (firing a TOGGLE_SCREENSHARING
} else { // event vs. directly calling toggleScreenSharing())?
sendAnalyticsEvent(TOOLBAR_SCREEN_ENABLED); sendAnalytics(createToolbarEvent(
} 'screen.sharing',
{
enable: !APP.conference.isSharingScreen
}));
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING); APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
}, },
popups: [ popups: [
@ -160,7 +172,13 @@ export default function getDefaultButtons() {
shortcut: 'D', shortcut: 'D',
shortcutAttr: 'toggleDesktopSharingPopover', shortcutAttr: 'toggleDesktopSharingPopover',
shortcutFunc() { 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 // eslint-disable-next-line no-empty-function
APP.conference.toggleScreenSharing().catch(() => {}); APP.conference.toggleScreenSharing().catch(() => {});
@ -180,8 +198,8 @@ export default function getDefaultButtons() {
}, },
id: 'toolbar_button_fodeviceselection', id: 'toolbar_button_fodeviceselection',
onClick(dispatch: Function) { onClick(dispatch: Function) {
sendAnalyticsEvent( sendAnalytics(
TOOLBAR_FILMSTRIP_ONLY_DEVICE_SELECTION_TOGGLED); createToolbarEvent('filmstrip.only.device.selection'));
dispatch(openDeviceSelectionDialog()); dispatch(openDeviceSelectionDialog());
}, },
@ -200,7 +218,7 @@ export default function getDefaultButtons() {
hidden: true, hidden: true,
id: 'toolbar_button_dialpad', id: 'toolbar_button_dialpad',
onClick() { onClick() {
sendAnalyticsEvent(TOOLBAR_SIP_DIALPAD_CLICKED); sendAnalytics(createToolbarEvent('dialpad'));
}, },
tooltipKey: 'toolbar.dialpad' tooltipKey: 'toolbar.dialpad'
}, },
@ -214,7 +232,13 @@ export default function getDefaultButtons() {
hidden: true, hidden: true,
id: 'toolbar_button_etherpad', id: 'toolbar_button_etherpad',
onClick() { 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); APP.UI.emitEvent(UIEvents.ETHERPAD_CLICKED);
}, },
tooltipKey: 'toolbar.etherpad' tooltipKey: 'toolbar.etherpad'
@ -228,7 +252,18 @@ export default function getDefaultButtons() {
enabled: true, enabled: true,
id: 'toolbar_button_fullScreen', id: 'toolbar_button_fullScreen',
onClick() { 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); APP.UI.emitEvent(UIEvents.TOGGLE_FULLSCREEN);
}, },
@ -236,7 +271,13 @@ export default function getDefaultButtons() {
shortcutAttr: 'toggleFullscreenPopover', shortcutAttr: 'toggleFullscreenPopover',
shortcutDescription: 'keyboardShortcuts.fullScreen', shortcutDescription: 'keyboardShortcuts.fullScreen',
shortcutFunc() { 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(); APP.UI.toggleFullScreen();
}, },
tooltipKey: 'toolbar.fullscreen' tooltipKey: 'toolbar.fullscreen'
@ -252,7 +293,7 @@ export default function getDefaultButtons() {
isDisplayed: () => true, isDisplayed: () => true,
id: 'toolbar_button_hangup', id: 'toolbar_button_hangup',
onClick() { onClick() {
sendAnalyticsEvent(TOOLBAR_HANGUP); sendAnalytics(createToolbarEvent('hangup'));
APP.UI.emitEvent(UIEvents.HANGUP); APP.UI.emitEvent(UIEvents.HANGUP);
}, },
tooltipKey: 'toolbar.hangup' tooltipKey: 'toolbar.hangup'
@ -275,7 +316,7 @@ export default function getDefaultButtons() {
enabled: true, enabled: true,
id: 'toolbar_button_link', id: 'toolbar_button_link',
onClick(dispatch: Function) { onClick(dispatch: Function) {
sendAnalyticsEvent(TOOLBAR_INVITE_CLICKED); sendAnalytics(createToolbarEvent('invite'));
dispatch(openInviteDialog()); dispatch(openInviteDialog());
}, },
@ -293,6 +334,13 @@ export default function getDefaultButtons() {
onClick() { onClick() {
const sharedVideoManager = APP.UI.getSharedVideoManager(); 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 (APP.conference.isLocalAudioMuted()) {
// If there's a shared video with the volume "on" and we // If there's a shared video with the volume "on" and we
// aren't the video owner, we warn the user // aren't the video owner, we warn the user
@ -303,11 +351,15 @@ export default function getDefaultButtons() {
APP.UI.showCustomToolbarPopup( APP.UI.showCustomToolbarPopup(
'microphone', 'unableToUnmutePopup', true, 5000); 'microphone', 'unableToUnmutePopup', true, 5000);
} else { } else {
sendAnalyticsEvent(TOOLBAR_AUDIO_UNMUTED); sendAnalytics(createToolbarEvent(
AUDIO_MUTE,
{ enable: false }));
APP.UI.emitEvent(UIEvents.AUDIO_MUTED, false, true); APP.UI.emitEvent(UIEvents.AUDIO_MUTED, false, true);
} }
} else { } else {
sendAnalyticsEvent(TOOLBAR_AUDIO_MUTED); sendAnalytics(createToolbarEvent(
AUDIO_MUTE,
{ enable: true }));
APP.UI.emitEvent(UIEvents.AUDIO_MUTED, true, true); APP.UI.emitEvent(UIEvents.AUDIO_MUTED, true, true);
} }
}, },
@ -328,7 +380,13 @@ export default function getDefaultButtons() {
shortcut: 'M', shortcut: 'M',
shortcutAttr: 'mutePopover', shortcutAttr: 'mutePopover',
shortcutFunc() { 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(); APP.conference.toggleAudioMuted();
}, },
shortcutDescription: 'keyboardShortcuts.mute', shortcutDescription: 'keyboardShortcuts.mute',
@ -351,14 +409,27 @@ export default function getDefaultButtons() {
enabled: true, enabled: true,
id: 'toolbar_button_raisehand', id: 'toolbar_button_raisehand',
onClick() { 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(); APP.conference.maybeToggleRaisedHand();
}, },
shortcut: 'R', shortcut: 'R',
shortcutAttr: 'raiseHandPopover', shortcutAttr: 'raiseHandPopover',
shortcutDescription: 'keyboardShortcuts.raiseHand', shortcutDescription: 'keyboardShortcuts.raiseHand',
shortcutFunc() { 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(); APP.conference.maybeToggleRaisedHand();
}, },
tooltipKey: 'toolbar.raiseHand' tooltipKey: 'toolbar.raiseHand'
@ -386,7 +457,9 @@ export default function getDefaultButtons() {
enabled: true, enabled: true,
id: 'toolbar_button_settings', id: 'toolbar_button_settings',
onClick() { 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); APP.UI.emitEvent(UIEvents.TOGGLE_SETTINGS);
}, },
sideContainerId: 'settings_container', sideContainerId: 'settings_container',
@ -401,7 +474,15 @@ export default function getDefaultButtons() {
enabled: true, enabled: true,
id: 'toolbar_button_sharedvideo', id: 'toolbar_button_sharedvideo',
onClick() { 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); APP.UI.emitEvent(UIEvents.SHARED_VIDEO_CLICKED);
}, },
popups: [ popups: [

View File

@ -4,16 +4,13 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
TOOLBAR_AUDIO_ONLY_ENABLED, createToolbarEvent,
TOOLBAR_VIDEO_QUALITY_HIGH, sendAnalytics
TOOLBAR_VIDEO_QUALITY_LOW,
TOOLBAR_VIDEO_QUALITY_STANDARD,
sendAnalyticsEvent
} from '../../analytics'; } from '../../analytics';
import { import {
VIDEO_QUALITY_LEVELS,
setAudioOnly, setAudioOnly,
setReceiveVideoQuality, setReceiveVideoQuality
VIDEO_QUALITY_LEVELS
} from '../../base/conference'; } from '../../base/conference';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import JitsiMeetJS from '../../base/lib-jitsi-meet'; import JitsiMeetJS from '../../base/lib-jitsi-meet';
@ -26,6 +23,22 @@ const {
LOW LOW
} = VIDEO_QUALITY_LEVELS; } = 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 * Implements a React {@link Component} which displays a dialog with a slider
* for selecting a new receive video quality. * for selecting a new receive video quality.
@ -255,12 +268,13 @@ class VideoQualityDialog extends Component {
* @returns {void} * @returns {void}
*/ */
_enableAudioOnly() { _enableAudioOnly() {
sendAnalyticsEvent(TOOLBAR_AUDIO_ONLY_ENABLED); sendAnalytics(createEvent('audio.only'));
logger.log('Video quality: audio only enabled'); logger.log('Video quality: audio only enabled');
this.props.dispatch(setAudioOnly(true)); 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 * Dispatches an action to receive high quality video from remote
* participants. * participants.
* *
@ -268,7 +282,7 @@ class VideoQualityDialog extends Component {
* @returns {void} * @returns {void}
*/ */
_enableHighDefinition() { _enableHighDefinition() {
sendAnalyticsEvent(TOOLBAR_VIDEO_QUALITY_HIGH); sendAnalytics(createEvent('high'));
logger.log('Video quality: high enabled'); logger.log('Video quality: high enabled');
this.props.dispatch(setReceiveVideoQuality(HIGH)); this.props.dispatch(setReceiveVideoQuality(HIGH));
} }
@ -281,7 +295,7 @@ class VideoQualityDialog extends Component {
* @returns {void} * @returns {void}
*/ */
_enableLowDefinition() { _enableLowDefinition() {
sendAnalyticsEvent(TOOLBAR_VIDEO_QUALITY_LOW); sendAnalytics(createEvent('low'));
logger.log('Video quality: low enabled'); logger.log('Video quality: low enabled');
this.props.dispatch(setReceiveVideoQuality(LOW)); this.props.dispatch(setReceiveVideoQuality(LOW));
} }
@ -294,7 +308,7 @@ class VideoQualityDialog extends Component {
* @returns {void} * @returns {void}
*/ */
_enableStandardDefinition() { _enableStandardDefinition() {
sendAnalyticsEvent(TOOLBAR_VIDEO_QUALITY_STANDARD); sendAnalytics(createEvent('standard'));
logger.log('Video quality: standard enabled'); logger.log('Video quality: standard enabled');
this.props.dispatch(setReceiveVideoQuality(STANDARD)); this.props.dispatch(setReceiveVideoQuality(STANDARD));
} }