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:
parent
8d81f1d69f
commit
6682543691
12
analytics.js
12
analytics.js
|
@ -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));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
|
@ -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';
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './functions';
|
||||
|
||||
import './middleware';
|
|
@ -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);
|
||||
});
|
|
@ -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';
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export * from './helpers';
|
||||
export * from './loadScript';
|
||||
export * from './randomUtil';
|
||||
export * from './uri';
|
||||
|
|
|
@ -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)));
|
||||
}
|
Loading…
Reference in New Issue