feat(Amplitude): Integration.

This commit is contained in:
Hristo Terezov 2019-01-03 13:54:02 +00:00
parent 2d57d22a3f
commit e5a8d95f1f
8 changed files with 369 additions and 5 deletions

View File

@ -43,6 +43,10 @@ deploy-appbundle:
$(BUILD_DIR)/alwaysontop.min.js \
$(BUILD_DIR)/alwaysontop.min.map \
$(OUTPUT_DIR)/analytics-ga.js \
$(BUILD_DIR)/analytics-ga.min.js \
$(BUILD_DIR)/analytics-ga.min.map \
$(BUILD_DIR)/analytics-amplitude.min.js \
$(BUILD_DIR)/analytics-amplitude.min.map \
$(DEPLOY_DIR)
deploy-lib-jitsi-meet:

63
package-lock.json generated
View File

@ -2191,6 +2191,15 @@
"isomorphic-fetch": "^2.2.1"
}
},
"@segment/top-domain": {
"version": "3.0.0",
"resolved": "http://registry.npmjs.org/@segment/top-domain/-/top-domain-3.0.0.tgz",
"integrity": "sha1-AuWlpP1CqfbPiGsF6C8QQBKjw6c=",
"requires": {
"component-cookie": "^1.1.2",
"component-url": "^0.2.1"
}
},
"@webassemblyjs/ast": {
"version": "1.7.11",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz",
@ -2460,6 +2469,24 @@
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
"dev": true
},
"amplitude-js": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-4.5.2.tgz",
"integrity": "sha512-J075hRBuhCuBqwrhmuGIXg7zCLRO6TvONTJUESpTkM1LVL5bMcTx9BczW4Hh6p6kjBQjs2fD4rNbhPs48kYZwA==",
"requires": {
"@segment/top-domain": "^3.0.0",
"blueimp-md5": "^2.10.0",
"json3": "^3.3.2",
"lodash": "^4.17.4",
"ua-parser-js": "github:amplitude/ua-parser-js#ed538f16f5c6ecd8357da989b617d4f156dcf35d"
},
"dependencies": {
"ua-parser-js": {
"version": "github:amplitude/ua-parser-js#ed538f16f5c6ecd8357da989b617d4f156dcf35d",
"from": "github:amplitude/ua-parser-js#ed538f1"
}
}
},
"ansi": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz",
@ -3295,6 +3322,11 @@
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==",
"dev": true
},
"blueimp-md5": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.10.0.tgz",
"integrity": "sha512-EkNUOi7tpV68TqjpiUz9D9NcT8um2+qtgntmMbi5UKssVX2m/2PLqotcric0RE63pB3HPN/fjf3cKHN2ufGSUQ=="
},
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
@ -4070,11 +4102,39 @@
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs="
},
"component-cookie": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/component-cookie/-/component-cookie-1.1.4.tgz",
"integrity": "sha512-j6rzl+vHDTowvYz7Al3V0ud84O2l4YqGdA9qMj1W1nlZ5yWi7EhOd7ZSPzWFM25gZgv2OxWh6JlJYfsz2+XYow==",
"requires": {
"debug": "2.2.0"
},
"dependencies": {
"debug": {
"version": "2.2.0",
"resolved": "http://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
"integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
"requires": {
"ms": "0.7.1"
}
},
"ms": {
"version": "0.7.1",
"resolved": "http://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
"integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg="
}
}
},
"component-emitter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
},
"component-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/component-url/-/component-url-0.2.1.tgz",
"integrity": "sha1-Tk9HmcQ+rZ/TzpG1owXSICCP7kc="
},
"compressible": {
"version": "2.0.15",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.15.tgz",
@ -8247,8 +8307,7 @@
"json3": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
"integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=",
"dev": true
"integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE="
},
"json5": {
"version": "0.5.1",

View File

@ -35,6 +35,7 @@
"@atlaskit/tooltip": "12.1.13",
"@microsoft/microsoft-graph-client": "1.1.0",
"@webcomponents/url": "0.7.1",
"amplitude-js": "4.5.2",
"dropbox": "4.0.9",
"i18next": "8.4.3",
"i18next-browser-languagedetector": "2.0.0",

View File

@ -43,10 +43,15 @@ export function initAnalytics({ getState }: { getState: Function }) {
const state = getState();
const config = state['features/base/config'];
const { analyticsScriptUrls, deploymentInfo, googleAnalyticsTrackingId }
= config;
const {
amplitudeAPPKey,
analyticsScriptUrls,
deploymentInfo,
googleAnalyticsTrackingId
} = config;
const { group, server, user } = state['features/base/jwt'];
const handlerConstructorOptions = {
amplitudeAPPKey,
envType: (deploymentInfo && deploymentInfo.envType) || 'dev',
googleAnalyticsTrackingId,
group,

View File

@ -0,0 +1,67 @@
/**
* Abstract implementation of analytics handler
*/
export default class AbstractHandler {
/**
* Creates new instance.
*/
constructor() {
this._enabled = false;
}
/**
* Extracts a name for the event from the event properties.
*
* @param {Object} event - The analytics event.
* @returns {string} - The extracted name.
*/
_extractName(event) {
// Page events have a single 'name' field.
if (event.type === 'page') {
return event.name;
}
const {
action,
actionSubject,
source
} = event;
// All events have action, actionSubject, and source fields. All
// three fields are required, and 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 event's name harder
// to read.
let name = action;
if (actionSubject && actionSubject !== action) {
name = `${actionSubject}.${action}`;
}
if (source && source !== action) {
name = `${source}.${name}`;
}
return name;
}
/**
* Checks if an event should be ignored or not.
*
* @param {Object} event - The event.
* @returns {boolean}
*/
_shouldIgnore(event) {
if (!event || !this._enabled) {
return true;
}
const ignoredEvents
= [ 'e2e_rtt', 'rtp.stats', 'rtt.by.region', 'available.device',
'stream.switch.delay', 'ice.state.changed', 'ice.duration' ];
// Temporary removing some of the events that are too noisy.
return ignoredEvents.indexOf(event.action) !== -1;
}
}

View File

@ -0,0 +1,71 @@
import amplitude from 'amplitude-js';
import { getJitsiMeetGlobalNS } from '../../base/util';
import AbstractHandler from './AbstractHandler';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Analytics handler for Amplitude.
*/
class AmplitudeHandler extends AbstractHandler {
/**
* Creates new instance of the Amplitude analytics handler.
*
* @param {Object} options -
* @param {string} options.amplitudeAPPKey - The Amplitude app key required
* by the Amplitude API.
*/
constructor(options) {
super();
const { amplitudeAPPKey } = options;
if (!amplitudeAPPKey) {
logger.warn(
'Failed to initialize Amplitude handler, no tracking ID');
return;
}
this._enabled = true;
amplitude.getInstance().init(amplitudeAPPKey);
}
/**
* Sets the Amplitude user properties.
*
* @param {Object} props - The user portperties.
* @returns {void}
*/
setUserProperties(props) {
if (this._enabled) {
amplitude.getInstance().setUserProperties(props);
}
}
/**
* Sends an event to Amplitude. 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.
* @returns {void}
*/
sendEvent(event) {
if (this._shouldIgnore(event)) {
return;
}
amplitude.getInstance().logEvent(
this._extractName(event),
event);
}
}
const globalNS = getJitsiMeetGlobalNS();
globalNS.analyticsHandlers = globalNS.analyticsHandlers || [];
globalNS.analyticsHandlers.push(AmplitudeHandler);

View File

@ -0,0 +1,153 @@
/* global ga */
import { getJitsiMeetGlobalNS } from '../../base/util';
import AbstractHandler from './AbstractHandler';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Analytics handler for Google Analytics.
*/
class GoogleAnalyticsHandler extends AbstractHandler {
/**
* Creates new instance of the GA analytics handler.
*
* @param {Object} options -
* @param {string} options.googleAnalyticsTrackingId - The GA track id
* required by the GA API.
*/
constructor(options) {
super();
this._userProperties = {};
if (!options.googleAnalyticsTrackingId) {
logger.warn(
'Failed to initialize Google Analytics handler, no tracking ID'
);
return;
}
this._enabled = true;
this._initGoogleAnalytics(options);
}
/**
* Initializes the ga object.
*
* @param {Object} options -
* @param {string} options.googleAnalyticsTrackingId - The GA track id
* required by the GA API.
* @returns {void}
*/
_initGoogleAnalytics(options) {
/**
* TODO: Keep this local, there's no need to add it to window.
*/
/* eslint-disable */
(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');
/* eslint-enable */
ga('create', options.googleAnalyticsTrackingId, 'auto');
ga('send', 'pageview');
}
/**
* 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 {number} - 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
*/
_extractValue(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
*/
_extractLabel(event) {
const { attributes = {} } = event;
const labelsArray
= Object.keys(attributes).map(key => `${key}=${attributes[key]}`);
labelsArray.push(this._userPropertiesString);
return labelsArray.join('&');
}
/**
* Sets the permanent properties for the current session.
*
* @param {Object} props - The permanent portperties.
* @returns {void}
*/
setUserProperties(props = {}) {
if (!this._enabled) {
return;
}
// 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.
const filter = [ 'user_agent', 'callstats_name' ];
this._userPropertiesString
= Object.keys(props)
.filter(key => filter.indexOf(key) === -1)
.map(key => `permanent_${key}=${props[key]}`)
.join('&');
}
/**
* 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.
* @returns {void}
*/
sendEvent(event) {
if (this._shouldIgnore(event)) {
return;
}
const gaEvent = {
'eventCategory': 'jitsi-meet',
'eventAction': this._extractName(event),
'eventLabel': this._extractLabel(event)
};
const value = this._extractValue(event);
if (!isNaN(value)) {
gaEvent.eventValue = value;
}
ga('send', 'event', gaEvent);
}
}
const globalNS = getJitsiMeetGlobalNS();
globalNS.analyticsHandlers = globalNS.analyticsHandlers || [];
globalNS.analyticsHandlers.push(GoogleAnalyticsHandler);

View File

@ -133,7 +133,11 @@ module.exports = [
'flacEncodeWorker':
'./react/features/local-recording/'
+ 'recording/flac/flacEncodeWorker.js'
+ 'recording/flac/flacEncodeWorker.js',
'analytics-ga':
'./react/features/analytics/handlers/GoogleAnalyticsHandler.js',
'analytics-amplitude':
'./react/features/analytics/handlers/AmplitudeHandler.js'
}
}),