Moves analytics loading to react. (#1945)

* feat(analytics): move to React

The analytics handlers have been moved to JitsiMeetGlobalNS, so now they are
stored in `window.JitsiMeetJS.app.analyticsHandlers`.

The analytics handlers are re-downloaded and re-initialized on every
lib-jitsi-meet initialization, which happens every time the config is changed
(moving between deployments in the mobile app).

* Adds legacy support for old analytics location.
This commit is contained in:
Дамян Минков 2017-09-01 14:14:03 -05:00 committed by GitHub
parent 8d81f1d69f
commit 6682543691
15 changed files with 270 additions and 177 deletions

View File

@ -27,7 +27,13 @@
action + '.' + data.browserName, label, value);
};
if(typeof ctx.analyticsHandlers === "undefined")
ctx.analyticsHandlers = [];
ctx.analyticsHandlers.push(Analytics);
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

@ -17,7 +17,7 @@ import UIEvents from './service/UI/UIEvents';
import UIUtil from './modules/UI/util/UIUtil';
import * as JitsiMeetConferenceEvents from './ConferenceEvents';
import analytics from './modules/analytics/analytics';
import { initAnalytics } from './react/features/analytics';
import EventEmitter from "events";
@ -35,6 +35,7 @@ import {
} from './react/features/base/conference';
import { updateDeviceList } from './react/features/base/devices';
import {
isAnalyticsEnabled,
isFatalJitsiConnectionError
} from './react/features/base/lib-jitsi-meet';
import {
@ -662,12 +663,13 @@ export default {
oldOnUnhandledRejection(event);
};
}
return JitsiMeetJS.init(
Object.assign(
{enableAnalyticsLogging: analytics.isEnabled()}, config)
Object.assign({
enableAnalyticsLogging: isAnalyticsEnabled(APP.store)
},
config)
).then(() => {
analytics.init();
initAnalytics(APP.store);
return this.createInitialLocalTracksAndConnect(
options.roomName, {
startAudioOnly: config.startAudioOnly,

View File

@ -1,144 +0,0 @@
/* global JitsiMeetJS, config, APP */
/**
* Load the integration of a third-party analytics API such as Google
* Analytics. Since we cannot guarantee the quality of the third-party service
* (e.g. their server may take noticeably long time to respond), it is in our
* best interest (in the sense that the integration of the analytics API is
* important to us but not enough to allow it to prevent people from joining
* a conference) to download the API asynchronously. Additionally, Google
* Analytics will download its implementation asynchronously anyway so it makes
* sense to append the loading on our side rather than prepend it.
* @param {string} url the url to be loaded
* @returns {Promise} resolved with no arguments when the script is loaded and
* rejected with the error from JitsiMeetJS.ScriptUtil.loadScript method
*/
function loadScript(url) {
return new Promise((resolve, reject) =>
JitsiMeetJS.util.ScriptUtil.loadScript(
url,
/* async */ true,
/* prepend */ false,
/* relativeURL */ false,
/* loadCallback */ () => resolve(),
/* errorCallback */ error => reject(error)));
}
/**
* Handles the initialization of analytics.
*/
class Analytics {
constructor() {
this._scriptURLs = Array.isArray(config.analyticsScriptUrls)
? config.analyticsScriptUrls : [];
this._enabled = !!this._scriptURLs.length
&& !config.disableThirdPartyRequests;
window.analyticsHandlers = [];
const machineId = JitsiMeetJS.getMachineId();
this._handlerConstructorOptions = {
product: "lib-jitsi-meet",
version: JitsiMeetJS.version,
session: machineId,
user: "uid-" + machineId
};
}
/**
* Returns whether analytics is enabled or not.
* @returns {boolean} whether analytics is enabled or not.
*/
isEnabled() {
return this._enabled;
}
/**
* Tries to load the scripts for the analytics handlers.
* @returns {Promise} resolves with the handlers that have been
* successfully loaded and rejects if there are no handlers loaded or the
* analytics is disabled.
*/
_loadHandlers() {
if(!this.isEnabled()) {
return Promise.reject(new Error("Analytics is disabled!"));
}
let handlersPromises = [];
this._scriptURLs.forEach(url =>
handlersPromises.push(
loadScript(url).then(
() => {
return {type: "success"};
},
error => {
return {type: "error", error, url};
}))
);
return new Promise((resolve, reject) =>
{
Promise.all(handlersPromises).then(values => {
values.forEach(el => {
if(el.type === "error") {
console.log("Failed to load " + el.url);
console.error(el.error);
}
});
if(window.analyticsHandlers.length === 0) {
reject(new Error("No analytics handlers available"));
} else {
let handlerInstances = [];
window.analyticsHandlers.forEach(
Handler => handlerInstances.push(
new Handler(this._handlerConstructorOptions)));
resolve(handlerInstances);
}
});
});
}
/**
* Loads the analytics scripts and inits JitsiMeetJS.analytics by setting
* permanent properties and setting the handlers from the loaded scripts.
* NOTE: Has to be used after JitsiMeetJS.init. Otherwise analytics will be
* null.
*/
init() {
const { analytics } = JitsiMeetJS;
if (!this.isEnabled() || !analytics)
return;
this._loadHandlers().then(
handlers => {
const permanentProperties = {
roomName: APP.conference.roomName,
userAgent: navigator.userAgent
};
const { group, server } = APP.store.getState()['features/jwt'];
if (server) {
permanentProperties.server = server;
}
if (group) {
permanentProperties.group = group;
}
// optionally include local deployment information based on
// the contents of window.config.deploymentInfo
if (config.deploymentInfo) {
for (let key in config.deploymentInfo) {
if (config.deploymentInfo.hasOwnProperty(key)) {
permanentProperties[key]
= config.deploymentInfo[key];
}
}
}
analytics.addPermanentProperties(permanentProperties);
analytics.setAnalyticsHandlers(handlers);
},
error => analytics.dispose() && console.error(error));
}
}
export default new Analytics();

View File

@ -1,7 +1,7 @@
// FIXME: change to '../API' when we update to webpack2. If we do this now all
// files from API modules will be included in external_api.js.
import { API_ID } from '../API/constants';
import { getJitsiMeetGlobalNS } from '../util/helpers';
import { getJitsiMeetGlobalNS } from '../../react/features/base/util';
import PostMessageTransportBackend from './PostMessageTransportBackend';
import Transport from './Transport';

View File

@ -16,23 +16,6 @@ export function createDeferred() {
return deferred;
}
/**
* Returns the namespace for all global variables, functions, etc that we need.
*
* @returns {Object} the namespace.
*
* NOTE: After React-ifying everything this should be the only global.
*/
export function getJitsiMeetGlobalNS() {
if (!window.JitsiMeetJS) {
window.JitsiMeetJS = {};
}
if (!window.JitsiMeetJS.app) {
window.JitsiMeetJS.app = {};
}
return window.JitsiMeetJS.app;
}
/**
* Reload page.
*/

View File

@ -0,0 +1,140 @@
import JitsiMeetJS, { isAnalyticsEnabled } from '../base/lib-jitsi-meet';
import { getJitsiMeetGlobalNS, loadScript } from '../base/util';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Loads the analytics scripts and inits JitsiMeetJS.analytics by setting
* permanent properties and setting the handlers from the loaded scripts.
* NOTE: Has to be used after JitsiMeetJS.init. Otherwise analytics will be
* null.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @returns {void}
*/
export function initAnalytics({ getState }) {
getJitsiMeetGlobalNS().analyticsHandlers = [];
// legacy support for old analytics location
window.analyticsHandlers = [];
const { analytics } = JitsiMeetJS;
if (!isAnalyticsEnabled({ getState }) || !analytics) {
return;
}
const config = getState()['features/base/config'];
const { analyticsScriptUrls } = config;
const machineId = JitsiMeetJS.getMachineId();
const handlerConstructorOptions = {
product: 'lib-jitsi-meet',
version: JitsiMeetJS.version,
session: machineId,
user: `uid-${machineId}`,
server: getState()['features/base/connection'].locationURL.host
};
_loadHandlers(analyticsScriptUrls, handlerConstructorOptions)
.then(handlers => {
const permanentProperties = {
roomName: getState()['features/base/conference'].room,
userAgent: navigator.userAgent
};
const { group, server } = getState()['features/jwt'];
if (server) {
permanentProperties.server = server;
}
if (group) {
permanentProperties.group = group;
}
// optionally include local deployment information based on
// the contents of window.config.deploymentInfo
if (config.deploymentInfo) {
for (const key in config.deploymentInfo) {
if (config.deploymentInfo.hasOwnProperty(key)) {
permanentProperties[key]
= config.deploymentInfo[key];
}
}
}
analytics.addPermanentProperties(permanentProperties);
analytics.setAnalyticsHandlers(handlers);
},
error => analytics.dispose() && logger.error(error));
}
/**
* Tries to load the scripts for the analytics handlers.
*
* @param {Array} scriptURLs - The array of script urls to load.
* @param {Object} handlerConstructorOptions - The default options to pass when
* creating handlers.
* @private
* @returns {Promise} Resolves with the handlers that have been
* successfully loaded and rejects if there are no handlers loaded or the
* analytics is disabled.
*/
function _loadHandlers(scriptURLs, handlerConstructorOptions) {
const promises = [];
for (const url of scriptURLs) {
promises.push(
loadScript(url).then(
() => {
return { type: 'success' };
},
error => {
return {
type: 'error',
error,
url
};
}));
}
return Promise.all(promises).then(values => {
for (const el of values) {
if (el.type === 'error') {
logger.warn(`Failed to load ${el.url}: ${el.error}`);
}
}
// analyticsHandlers is the handlers we want to use
// we search for them in the JitsiMeetGlobalNS, but also
// check the old location to provide legacy support
let analyticsHandlers = [];
analyticsHandlers = analyticsHandlers.concat(
getJitsiMeetGlobalNS().analyticsHandlers);
// legacy support for old analytics location
analyticsHandlers = analyticsHandlers.concat(window.analyticsHandlers);
if (analyticsHandlers.length === 0) {
throw new Error('No analytics handlers available');
} else {
const handlers = [];
for (const Handler of analyticsHandlers) {
// catch any error while loading to avoid
// skipping analytics in case of multiple scripts
try {
handlers.push(new Handler(handlerConstructorOptions));
} catch (error) {
logger.error('error instantiating analytics impl', error);
}
}
logger.debug(`Loaded ${handlers.length} analytics handlers`);
return handlers;
}
});
}

View File

@ -0,0 +1,3 @@
export * from './functions';
import './middleware';

View File

@ -0,0 +1,22 @@
import { MiddlewareRegistry } from '../base/redux';
import { LIB_DID_INIT } from '../base/lib-jitsi-meet';
import { initAnalytics } from './functions';
/**
* Middleware which intercepts config actions to handle evaluating analytics
* config based on the config stored in the store.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case LIB_DID_INIT: {
initAnalytics(store);
break;
}
}
return next(action);
});

View File

@ -3,6 +3,7 @@
import React from 'react';
import { Linking } from 'react-native';
import '../../analytics';
import { Platform } from '../../base/react';
import '../../mobile/audio-mode';
import '../../mobile/background';

View File

@ -9,6 +9,7 @@ import {
LIB_WILL_INIT,
SET_WEBRTC_READY
} from './actionTypes';
import { isAnalyticsEnabled } from './functions';
declare var APP: Object;
@ -50,7 +51,11 @@ export function initLib() {
dispatch({ type: LIB_WILL_INIT });
return (
JitsiMeetJS.init(config)
JitsiMeetJS.init(
Object.assign({
enableAnalyticsLogging: isAnalyticsEnabled({ getState })
},
config))
.then(() => dispatch({ type: LIB_DID_INIT }))
.catch(error => {
dispatch(libInitError(error));

View File

@ -99,3 +99,23 @@ export function loadConfig(host: string, path: string = 'config.js') {
return promise;
}
/**
* Evaluates whether analytics is enabled or not based on
* the redux {@code store}.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @returns {boolean} True if analytics is enabled, false otherwise.
*/
export function isAnalyticsEnabled({ getState }: { getState: Function }) {
const {
analyticsScriptUrls,
disableThirdPartyRequests
} = getState()['features/base/config'];
const scriptURLs = Array.isArray(analyticsScriptUrls)
? analyticsScriptUrls : [];
return Boolean(scriptURLs.length) && !disableThirdPartyRequests;
}

View File

@ -132,6 +132,14 @@ function _visitNode(node, callback) {
document.addEventListener = () => {};
}
// document.cookie
//
// Required by:
// - herment
if (typeof document.cookie === 'undefined') {
document.cookie = '';
}
// Document.querySelector
//
// Required by:
@ -317,14 +325,23 @@ function _visitNode(node, callback) {
//
// Required by:
// - Strophe
// - herment - requires a working sessionStorage, no empty impl. functions
if (typeof global.sessionStorage === 'undefined') {
global.sessionStorage = {
/* eslint-disable no-empty-function */
getItem() {},
removeItem() {},
setItem() {}
let internalStorage = {};
/* eslint-enable no-empty-function */
global.sessionStorage = {
clear() {
internalStorage = {};
},
getItem(key) {
return internalStorage[key];
},
removeItem(key) {
delete internalStorage[key];
},
setItem(key, value) {
internalStorage[key] = value;
}
};
}

View File

@ -0,0 +1,18 @@
/**
* Returns the namespace for all global variables, functions, etc that we need.
*
* @returns {Object} The namespace.
*
* NOTE: After React-ifying everything this should be the only global.
*/
export function getJitsiMeetGlobalNS() {
if (!window.JitsiMeetJS) {
window.JitsiMeetJS = {};
}
if (!window.JitsiMeetJS.app) {
window.JitsiMeetJS.app = {};
}
return window.JitsiMeetJS.app;
}

View File

@ -1,3 +1,4 @@
export * from './helpers';
export * from './loadScript';
export * from './randomUtil';
export * from './uri';

View File

@ -0,0 +1,19 @@
import JitsiMeetJS from '../lib-jitsi-meet';
/**
* Loads a script from a specific URL. The script will be interpreted upon load.
*
* @param {string} url - The url to be loaded.
* @returns {Promise} Resolved with no arguments when the script is loaded and
* rejected with the error from JitsiMeetJS.ScriptUtil.loadScript method.
*/
export function loadScript(url) {
return new Promise((resolve, reject) =>
JitsiMeetJS.util.ScriptUtil.loadScript(
url,
/* async */ true,
/* prepend */ false,
/* relativeURL */ false,
/* loadCallback */ () => resolve(),
/* errorCallback */ error => reject(error)));
}