mirror of https://github.com/jitsi/jitsi-meet
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.pull/1958/head jitsi-meet_2417
parent
8d81f1d69f
commit
6682543691
@ -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(); |
@ -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); |
||||
}); |
@ -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