feat(Amplitude): Integration.
This commit is contained in:
parent
2d57d22a3f
commit
e5a8d95f1f
4
Makefile
4
Makefile
|
@ -43,6 +43,10 @@ deploy-appbundle:
|
||||||
$(BUILD_DIR)/alwaysontop.min.js \
|
$(BUILD_DIR)/alwaysontop.min.js \
|
||||||
$(BUILD_DIR)/alwaysontop.min.map \
|
$(BUILD_DIR)/alwaysontop.min.map \
|
||||||
$(OUTPUT_DIR)/analytics-ga.js \
|
$(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_DIR)
|
||||||
|
|
||||||
deploy-lib-jitsi-meet:
|
deploy-lib-jitsi-meet:
|
||||||
|
|
|
@ -2191,6 +2191,15 @@
|
||||||
"isomorphic-fetch": "^2.2.1"
|
"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": {
|
"@webassemblyjs/ast": {
|
||||||
"version": "1.7.11",
|
"version": "1.7.11",
|
||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz",
|
||||||
|
@ -2460,6 +2469,24 @@
|
||||||
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
|
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
|
||||||
"dev": true
|
"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": {
|
"ansi": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz",
|
||||||
|
@ -3295,6 +3322,11 @@
|
||||||
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==",
|
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==",
|
||||||
"dev": true
|
"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": {
|
"bn.js": {
|
||||||
"version": "4.11.8",
|
"version": "4.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||||
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs="
|
"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": {
|
"component-emitter": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
|
||||||
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
|
"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": {
|
"compressible": {
|
||||||
"version": "2.0.15",
|
"version": "2.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.15.tgz",
|
||||||
|
@ -8247,8 +8307,7 @@
|
||||||
"json3": {
|
"json3": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
|
||||||
"integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=",
|
"integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"json5": {
|
"json5": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
"@atlaskit/tooltip": "12.1.13",
|
"@atlaskit/tooltip": "12.1.13",
|
||||||
"@microsoft/microsoft-graph-client": "1.1.0",
|
"@microsoft/microsoft-graph-client": "1.1.0",
|
||||||
"@webcomponents/url": "0.7.1",
|
"@webcomponents/url": "0.7.1",
|
||||||
|
"amplitude-js": "4.5.2",
|
||||||
"dropbox": "4.0.9",
|
"dropbox": "4.0.9",
|
||||||
"i18next": "8.4.3",
|
"i18next": "8.4.3",
|
||||||
"i18next-browser-languagedetector": "2.0.0",
|
"i18next-browser-languagedetector": "2.0.0",
|
||||||
|
|
|
@ -43,10 +43,15 @@ export function initAnalytics({ getState }: { getState: Function }) {
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const config = state['features/base/config'];
|
const config = state['features/base/config'];
|
||||||
const { analyticsScriptUrls, deploymentInfo, googleAnalyticsTrackingId }
|
const {
|
||||||
= config;
|
amplitudeAPPKey,
|
||||||
|
analyticsScriptUrls,
|
||||||
|
deploymentInfo,
|
||||||
|
googleAnalyticsTrackingId
|
||||||
|
} = config;
|
||||||
const { group, server, user } = state['features/base/jwt'];
|
const { group, server, user } = state['features/base/jwt'];
|
||||||
const handlerConstructorOptions = {
|
const handlerConstructorOptions = {
|
||||||
|
amplitudeAPPKey,
|
||||||
envType: (deploymentInfo && deploymentInfo.envType) || 'dev',
|
envType: (deploymentInfo && deploymentInfo.envType) || 'dev',
|
||||||
googleAnalyticsTrackingId,
|
googleAnalyticsTrackingId,
|
||||||
group,
|
group,
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -133,7 +133,11 @@ module.exports = [
|
||||||
|
|
||||||
'flacEncodeWorker':
|
'flacEncodeWorker':
|
||||||
'./react/features/local-recording/'
|
'./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'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue