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)/alwaysontop.min.js \
$(BUILD_DIR)/alwaysontop.min.map \
$(OUTPUT_DIR)/analytics.js \
$(OUTPUT_DIR)/analytics-ga.js \
$(DEPLOY_DIR)
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 {
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

View File

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

View File

@ -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]) {

View File

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

View File

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

View File

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

View File

@ -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({

View File

@ -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));
},
/**

View File

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

View File

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

2
package-lock.json generated
View File

@ -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",

View File

@ -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",

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);
/**
* 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>) {
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));

View File

@ -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' : ''}`);

View File

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

View File

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

View File

@ -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'}`);

View File

@ -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());
}
/**

View File

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

View File

@ -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());
}
/**

View File

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

View File

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

View File

@ -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,11 +162,17 @@ 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 ${

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {
<span id = 'unreadMessages' /></span>,
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: [

View File

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