Parcourir la source

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.
j8
Дамян Минков il y a 7 ans
Parent
révision
6682543691

+ 9
- 3
analytics.js Voir le fichier

@@ -27,7 +27,13 @@
27 27
         action + '.' + data.browserName, label, value);
28 28
   };
29 29
 
30
-  if(typeof ctx.analyticsHandlers === "undefined")
31
-    ctx.analyticsHandlers = [];
32
-  ctx.analyticsHandlers.push(Analytics);
30
+  if (typeof ctx.JitsiMeetJS === "undefined")
31
+    ctx.JitsiMeetJS = {};
32
+
33
+  if (typeof ctx.JitsiMeetJS.app === "undefined")
34
+    ctx.JitsiMeetJS.app = {};
35
+
36
+  if (typeof ctx.JitsiMeetJS.app.analyticsHandlers === "undefined")
37
+    ctx.JitsiMeetJS.app.analyticsHandlers = [];
38
+  ctx.JitsiMeetJS.app.analyticsHandlers.push(Analytics);
33 39
 }(window));

+ 7
- 5
conference.js Voir le fichier

@@ -17,7 +17,7 @@ import UIEvents from './service/UI/UIEvents';
17 17
 import UIUtil from './modules/UI/util/UIUtil';
18 18
 import * as JitsiMeetConferenceEvents from './ConferenceEvents';
19 19
 
20
-import analytics from './modules/analytics/analytics';
20
+import { initAnalytics } from './react/features/analytics';
21 21
 
22 22
 import EventEmitter from "events";
23 23
 
@@ -35,6 +35,7 @@ import {
35 35
 } from './react/features/base/conference';
36 36
 import { updateDeviceList } from './react/features/base/devices';
37 37
 import {
38
+    isAnalyticsEnabled,
38 39
     isFatalJitsiConnectionError
39 40
 } from './react/features/base/lib-jitsi-meet';
40 41
 import {
@@ -662,12 +663,13 @@ export default {
662 663
                     oldOnUnhandledRejection(event);
663 664
             };
664 665
         }
665
-
666 666
         return JitsiMeetJS.init(
667
-            Object.assign(
668
-                {enableAnalyticsLogging: analytics.isEnabled()}, config)
667
+            Object.assign({
668
+                    enableAnalyticsLogging: isAnalyticsEnabled(APP.store)
669
+                },
670
+                config)
669 671
             ).then(() => {
670
-                analytics.init();
672
+                initAnalytics(APP.store);
671 673
                 return this.createInitialLocalTracksAndConnect(
672 674
                     options.roomName, {
673 675
                         startAudioOnly: config.startAudioOnly,

+ 0
- 144
modules/analytics/analytics.js Voir le fichier

@@ -1,144 +0,0 @@
1
-/* global JitsiMeetJS, config, APP */
2
-
3
-/**
4
- * Load the integration of a third-party analytics API such as Google
5
- * Analytics. Since we cannot guarantee the quality of the third-party service
6
- * (e.g. their server may take noticeably long time to respond), it is in our
7
- * best interest (in the sense that the integration of the analytics API is
8
- * important to us but not enough to allow it to prevent people from joining
9
- * a conference) to download the API asynchronously. Additionally, Google
10
- * Analytics will download its implementation asynchronously anyway so it makes
11
- * sense to append the loading on our side rather than prepend it.
12
- * @param {string} url the url to be loaded
13
- * @returns {Promise} resolved with no arguments when the script is loaded and
14
- * rejected with the error from JitsiMeetJS.ScriptUtil.loadScript method
15
- */
16
-function loadScript(url) {
17
-    return new Promise((resolve, reject) =>
18
-        JitsiMeetJS.util.ScriptUtil.loadScript(
19
-            url,
20
-            /* async */ true,
21
-            /* prepend */ false,
22
-            /* relativeURL */ false,
23
-            /* loadCallback */ () => resolve(),
24
-            /* errorCallback */ error => reject(error)));
25
-}
26
-
27
-/**
28
- * Handles the initialization of analytics.
29
- */
30
-class Analytics {
31
-    constructor() {
32
-        this._scriptURLs = Array.isArray(config.analyticsScriptUrls)
33
-            ? config.analyticsScriptUrls : [];
34
-        this._enabled = !!this._scriptURLs.length
35
-            && !config.disableThirdPartyRequests;
36
-        window.analyticsHandlers = [];
37
-        const machineId = JitsiMeetJS.getMachineId();
38
-        this._handlerConstructorOptions = {
39
-            product: "lib-jitsi-meet",
40
-            version: JitsiMeetJS.version,
41
-            session: machineId,
42
-            user:  "uid-" + machineId
43
-        };
44
-    }
45
-
46
-    /**
47
-     * Returns whether analytics is enabled or not.
48
-     * @returns {boolean} whether analytics is enabled or not.
49
-     */
50
-    isEnabled() {
51
-        return this._enabled;
52
-    }
53
-
54
-    /**
55
-     * Tries to load the scripts for the analytics handlers.
56
-     * @returns {Promise} resolves with the handlers that have been
57
-     * successfully loaded and rejects if there are no handlers loaded or the
58
-     * analytics is disabled.
59
-     */
60
-    _loadHandlers() {
61
-        if(!this.isEnabled()) {
62
-            return Promise.reject(new Error("Analytics is disabled!"));
63
-        }
64
-        let handlersPromises = [];
65
-        this._scriptURLs.forEach(url =>
66
-            handlersPromises.push(
67
-                loadScript(url).then(
68
-                    () => {
69
-                        return {type: "success"};
70
-                    },
71
-                    error => {
72
-                        return {type: "error", error, url};
73
-                    }))
74
-        );
75
-        return new Promise((resolve, reject) =>
76
-        {
77
-            Promise.all(handlersPromises).then(values => {
78
-                values.forEach(el => {
79
-                    if(el.type === "error") {
80
-                        console.log("Failed to load " + el.url);
81
-                        console.error(el.error);
82
-                    }
83
-                });
84
-
85
-                if(window.analyticsHandlers.length === 0) {
86
-                    reject(new Error("No analytics handlers available"));
87
-                } else {
88
-                    let handlerInstances = [];
89
-                    window.analyticsHandlers.forEach(
90
-                        Handler => handlerInstances.push(
91
-                            new Handler(this._handlerConstructorOptions)));
92
-                    resolve(handlerInstances);
93
-                }
94
-            });
95
-        });
96
-    }
97
-
98
-    /**
99
-     * Loads the analytics scripts and inits JitsiMeetJS.analytics by setting
100
-     * permanent properties and setting the handlers from the loaded scripts.
101
-     * NOTE: Has to be used after JitsiMeetJS.init. Otherwise analytics will be
102
-     * null.
103
-     */
104
-    init() {
105
-        const { analytics } = JitsiMeetJS;
106
-
107
-        if (!this.isEnabled() || !analytics)
108
-            return;
109
-
110
-        this._loadHandlers().then(
111
-            handlers => {
112
-                const permanentProperties = {
113
-                    roomName: APP.conference.roomName,
114
-                    userAgent: navigator.userAgent
115
-                };
116
-
117
-                const { group, server } = APP.store.getState()['features/jwt'];
118
-
119
-                if (server) {
120
-                    permanentProperties.server = server;
121
-                }
122
-                if (group) {
123
-                    permanentProperties.group = group;
124
-                }
125
-                 // optionally include local deployment information based on
126
-                 // the contents of window.config.deploymentInfo
127
-                if (config.deploymentInfo) {
128
-                    for (let key in config.deploymentInfo) {
129
-                        if (config.deploymentInfo.hasOwnProperty(key)) {
130
-                            permanentProperties[key]
131
-                                = config.deploymentInfo[key];
132
-                        }
133
-                    }
134
-                }
135
-
136
-                analytics.addPermanentProperties(permanentProperties);
137
-                analytics.setAnalyticsHandlers(handlers);
138
-            },
139
-            error => analytics.dispose() && console.error(error));
140
-
141
-    }
142
-}
143
-
144
-export default new Analytics();

+ 1
- 1
modules/transport/index.js Voir le fichier

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

+ 0
- 17
modules/util/helpers.js Voir le fichier

@@ -16,23 +16,6 @@ export function createDeferred() {
16 16
     return deferred;
17 17
 }
18 18
 
19
-/**
20
- * Returns the namespace for all global variables, functions, etc that we need.
21
- *
22
- * @returns {Object} the namespace.
23
- *
24
- * NOTE: After React-ifying everything this should be the only global.
25
- */
26
-export function getJitsiMeetGlobalNS() {
27
-    if (!window.JitsiMeetJS) {
28
-        window.JitsiMeetJS = {};
29
-    }
30
-    if (!window.JitsiMeetJS.app) {
31
-        window.JitsiMeetJS.app = {};
32
-    }
33
-    return window.JitsiMeetJS.app;
34
-}
35
-
36 19
 /**
37 20
  * Reload page.
38 21
  */

+ 140
- 0
react/features/analytics/functions.js Voir le fichier

@@ -0,0 +1,140 @@
1
+import JitsiMeetJS, { isAnalyticsEnabled } from '../base/lib-jitsi-meet';
2
+import { getJitsiMeetGlobalNS, loadScript } from '../base/util';
3
+
4
+const logger = require('jitsi-meet-logger').getLogger(__filename);
5
+
6
+/**
7
+ * Loads the analytics scripts and inits JitsiMeetJS.analytics by setting
8
+ * permanent properties and setting the handlers from the loaded scripts.
9
+ * NOTE: Has to be used after JitsiMeetJS.init. Otherwise analytics will be
10
+ * null.
11
+ *
12
+ * @param {Store} store - The redux store in which the specified {@code action}
13
+ * is being dispatched.
14
+ * @returns {void}
15
+ */
16
+export function initAnalytics({ getState }) {
17
+    getJitsiMeetGlobalNS().analyticsHandlers = [];
18
+
19
+    // legacy support for old analytics location
20
+    window.analyticsHandlers = [];
21
+
22
+    const { analytics } = JitsiMeetJS;
23
+
24
+    if (!isAnalyticsEnabled({ getState }) || !analytics) {
25
+        return;
26
+    }
27
+
28
+    const config = getState()['features/base/config'];
29
+    const { analyticsScriptUrls } = config;
30
+    const machineId = JitsiMeetJS.getMachineId();
31
+    const handlerConstructorOptions = {
32
+        product: 'lib-jitsi-meet',
33
+        version: JitsiMeetJS.version,
34
+        session: machineId,
35
+        user: `uid-${machineId}`,
36
+        server: getState()['features/base/connection'].locationURL.host
37
+    };
38
+
39
+    _loadHandlers(analyticsScriptUrls, handlerConstructorOptions)
40
+        .then(handlers => {
41
+            const permanentProperties = {
42
+                roomName: getState()['features/base/conference'].room,
43
+                userAgent: navigator.userAgent
44
+            };
45
+
46
+            const { group, server } = getState()['features/jwt'];
47
+
48
+            if (server) {
49
+                permanentProperties.server = server;
50
+            }
51
+            if (group) {
52
+                permanentProperties.group = group;
53
+            }
54
+
55
+            // optionally include local deployment information based on
56
+            // the contents of window.config.deploymentInfo
57
+            if (config.deploymentInfo) {
58
+                for (const key in config.deploymentInfo) {
59
+                    if (config.deploymentInfo.hasOwnProperty(key)) {
60
+                        permanentProperties[key]
61
+                            = config.deploymentInfo[key];
62
+                    }
63
+                }
64
+            }
65
+
66
+            analytics.addPermanentProperties(permanentProperties);
67
+            analytics.setAnalyticsHandlers(handlers);
68
+        },
69
+        error => analytics.dispose() && logger.error(error));
70
+}
71
+
72
+/**
73
+ * Tries to load the scripts for the analytics handlers.
74
+ *
75
+ * @param {Array} scriptURLs - The array of script urls to load.
76
+ * @param {Object} handlerConstructorOptions - The default options to pass when
77
+ * creating handlers.
78
+ * @private
79
+ * @returns {Promise} Resolves with the handlers that have been
80
+ * successfully loaded and rejects if there are no handlers loaded or the
81
+ * analytics is disabled.
82
+ */
83
+function _loadHandlers(scriptURLs, handlerConstructorOptions) {
84
+    const promises = [];
85
+
86
+    for (const url of scriptURLs) {
87
+        promises.push(
88
+            loadScript(url).then(
89
+                () => {
90
+                    return { type: 'success' };
91
+                },
92
+                error => {
93
+                    return {
94
+                        type: 'error',
95
+                        error,
96
+                        url
97
+                    };
98
+                }));
99
+    }
100
+
101
+    return Promise.all(promises).then(values => {
102
+        for (const el of values) {
103
+            if (el.type === 'error') {
104
+                logger.warn(`Failed to load ${el.url}: ${el.error}`);
105
+            }
106
+        }
107
+
108
+        // analyticsHandlers is the handlers we want to use
109
+        // we search for them in the JitsiMeetGlobalNS, but also
110
+        // check the old location to provide legacy support
111
+        let analyticsHandlers = [];
112
+
113
+        analyticsHandlers = analyticsHandlers.concat(
114
+            getJitsiMeetGlobalNS().analyticsHandlers);
115
+
116
+        // legacy support for old analytics location
117
+        analyticsHandlers = analyticsHandlers.concat(window.analyticsHandlers);
118
+
119
+        if (analyticsHandlers.length === 0) {
120
+            throw new Error('No analytics handlers available');
121
+        } else {
122
+            const handlers = [];
123
+
124
+            for (const Handler of analyticsHandlers) {
125
+                // catch any error while loading to avoid
126
+                // skipping analytics in case of multiple scripts
127
+                try {
128
+                    handlers.push(new Handler(handlerConstructorOptions));
129
+                } catch (error) {
130
+                    logger.error('error instantiating analytics impl', error);
131
+                }
132
+            }
133
+
134
+            logger.debug(`Loaded ${handlers.length} analytics handlers`);
135
+
136
+            return handlers;
137
+        }
138
+    });
139
+}
140
+

+ 3
- 0
react/features/analytics/index.js Voir le fichier

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

+ 22
- 0
react/features/analytics/middleware.js Voir le fichier

@@ -0,0 +1,22 @@
1
+import { MiddlewareRegistry } from '../base/redux';
2
+import { LIB_DID_INIT } from '../base/lib-jitsi-meet';
3
+
4
+import { initAnalytics } from './functions';
5
+
6
+/**
7
+ * Middleware which intercepts config actions to handle evaluating analytics
8
+ * config based on the config stored in the store.
9
+ *
10
+ * @param {Store} store - The redux store.
11
+ * @returns {Function}
12
+ */
13
+MiddlewareRegistry.register(store => next => action => {
14
+    switch (action.type) {
15
+    case LIB_DID_INIT: {
16
+        initAnalytics(store);
17
+        break;
18
+    }
19
+    }
20
+
21
+    return next(action);
22
+});

+ 1
- 0
react/features/app/components/App.native.js Voir le fichier

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

+ 6
- 1
react/features/base/lib-jitsi-meet/actions.js Voir le fichier

@@ -9,6 +9,7 @@ import {
9 9
     LIB_WILL_INIT,
10 10
     SET_WEBRTC_READY
11 11
 } from './actionTypes';
12
+import { isAnalyticsEnabled } from './functions';
12 13
 
13 14
 declare var APP: Object;
14 15
 
@@ -50,7 +51,11 @@ export function initLib() {
50 51
         dispatch({ type: LIB_WILL_INIT });
51 52
 
52 53
         return (
53
-            JitsiMeetJS.init(config)
54
+            JitsiMeetJS.init(
55
+                    Object.assign({
56
+                        enableAnalyticsLogging: isAnalyticsEnabled({ getState })
57
+                    },
58
+                    config))
54 59
                 .then(() => dispatch({ type: LIB_DID_INIT }))
55 60
                 .catch(error => {
56 61
                     dispatch(libInitError(error));

+ 20
- 0
react/features/base/lib-jitsi-meet/functions.js Voir le fichier

@@ -99,3 +99,23 @@ export function loadConfig(host: string, path: string = 'config.js') {
99 99
 
100 100
     return promise;
101 101
 }
102
+
103
+/**
104
+ * Evaluates whether analytics is enabled or not based on
105
+ * the redux {@code store}.
106
+ *
107
+ * @param {Store} store - The redux store in which the specified {@code action}
108
+ * is being dispatched.
109
+ * @returns {boolean} True if analytics is enabled, false otherwise.
110
+ */
111
+export function isAnalyticsEnabled({ getState }: { getState: Function }) {
112
+    const {
113
+        analyticsScriptUrls,
114
+        disableThirdPartyRequests
115
+    } = getState()['features/base/config'];
116
+
117
+    const scriptURLs = Array.isArray(analyticsScriptUrls)
118
+        ? analyticsScriptUrls : [];
119
+
120
+    return Boolean(scriptURLs.length) && !disableThirdPartyRequests;
121
+}

+ 23
- 6
react/features/base/lib-jitsi-meet/native/polyfills-browser.js Voir le fichier

@@ -132,6 +132,14 @@ function _visitNode(node, callback) {
132 132
             document.addEventListener = () => {};
133 133
         }
134 134
 
135
+        // document.cookie
136
+        //
137
+        // Required by:
138
+        // - herment
139
+        if (typeof document.cookie === 'undefined') {
140
+            document.cookie = '';
141
+        }
142
+
135 143
         // Document.querySelector
136 144
         //
137 145
         // Required by:
@@ -317,14 +325,23 @@ function _visitNode(node, callback) {
317 325
     //
318 326
     // Required by:
319 327
     // - Strophe
328
+    // - herment - requires a working sessionStorage, no empty impl. functions
320 329
     if (typeof global.sessionStorage === 'undefined') {
321
-        global.sessionStorage = {
322
-            /* eslint-disable no-empty-function */
323
-            getItem() {},
324
-            removeItem() {},
325
-            setItem() {}
330
+        let internalStorage = {};
326 331
 
327
-            /* eslint-enable no-empty-function */
332
+        global.sessionStorage = {
333
+            clear() {
334
+                internalStorage = {};
335
+            },
336
+            getItem(key) {
337
+                return internalStorage[key];
338
+            },
339
+            removeItem(key) {
340
+                delete internalStorage[key];
341
+            },
342
+            setItem(key, value) {
343
+                internalStorage[key] = value;
344
+            }
328 345
         };
329 346
     }
330 347
 

+ 18
- 0
react/features/base/util/helpers.js Voir le fichier

@@ -0,0 +1,18 @@
1
+/**
2
+ * Returns the namespace for all global variables, functions, etc that we need.
3
+ *
4
+ * @returns {Object} The namespace.
5
+ *
6
+ * NOTE: After React-ifying everything this should be the only global.
7
+ */
8
+export function getJitsiMeetGlobalNS() {
9
+    if (!window.JitsiMeetJS) {
10
+        window.JitsiMeetJS = {};
11
+    }
12
+
13
+    if (!window.JitsiMeetJS.app) {
14
+        window.JitsiMeetJS.app = {};
15
+    }
16
+
17
+    return window.JitsiMeetJS.app;
18
+}

+ 1
- 0
react/features/base/util/index.js Voir le fichier

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

+ 19
- 0
react/features/base/util/loadScript.web.js Voir le fichier

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

Chargement…
Annuler
Enregistrer