This commit is contained in:
115
react/features/analytics/handlers/AbstractHandler.ts
Normal file
115
react/features/analytics/handlers/AbstractHandler.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
export interface IEvent {
|
||||
action?: string;
|
||||
actionSubject?: string;
|
||||
attributes?: {
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
name?: string;
|
||||
source?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface IOptions {
|
||||
amplitudeAPPKey?: string;
|
||||
blackListedEvents?: string[];
|
||||
envType?: string;
|
||||
group?: string;
|
||||
host?: string;
|
||||
matomoEndpoint?: string;
|
||||
matomoSiteID?: string;
|
||||
product?: string;
|
||||
subproduct?: string;
|
||||
user?: string;
|
||||
version?: string;
|
||||
whiteListedEvents?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract implementation of analytics handler.
|
||||
*/
|
||||
export default class AbstractHandler {
|
||||
_enabled: boolean;
|
||||
_whiteListedEvents: Array<string> | undefined;
|
||||
_blackListedEvents: Array<string> | undefined;
|
||||
|
||||
/**
|
||||
* Creates new instance.
|
||||
*
|
||||
* @param {Object} options - Optional parameters.
|
||||
*/
|
||||
constructor(options: IOptions = {}) {
|
||||
this._enabled = false;
|
||||
this._whiteListedEvents = options.whiteListedEvents;
|
||||
|
||||
// FIXME:
|
||||
// Keeping the list with the very noisy events so that we don't flood with events whoever hasn't configured
|
||||
// white/black lists yet. We need to solve this issue properly by either making these events not so noisy or
|
||||
// by removing them completely from the code.
|
||||
this._blackListedEvents = [
|
||||
...(options.blackListedEvents || []), // eslint-disable-line no-extra-parens
|
||||
'e2e_rtt', 'rtp.stats', 'rtt.by.region', 'available.device', 'stream.switch.delay', 'ice.state.changed',
|
||||
'ice.duration', 'peer.conn.status.duration'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a name for the event from the event properties.
|
||||
*
|
||||
* @param {Object} event - The analytics event.
|
||||
* @returns {string} - The extracted name.
|
||||
*/
|
||||
_extractName(event: IEvent) {
|
||||
// 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: IEvent) {
|
||||
if (!event || !this._enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const name = this._extractName(event) ?? '';
|
||||
|
||||
if (Array.isArray(this._whiteListedEvents)) {
|
||||
return this._whiteListedEvents.indexOf(name) === -1;
|
||||
}
|
||||
|
||||
if (Array.isArray(this._blackListedEvents)) {
|
||||
return this._blackListedEvents.indexOf(name) !== -1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
91
react/features/analytics/handlers/AmplitudeHandler.ts
Normal file
91
react/features/analytics/handlers/AmplitudeHandler.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Identify } from '@amplitude/analytics-core';
|
||||
|
||||
import logger from '../logger';
|
||||
|
||||
import AbstractHandler, { IEvent } from './AbstractHandler';
|
||||
import { fixDeviceID } from './amplitude/fixDeviceID';
|
||||
import amplitude, { initAmplitude } from './amplitude/lib';
|
||||
|
||||
/**
|
||||
* Analytics handler for Amplitude.
|
||||
*/
|
||||
export default class AmplitudeHandler extends AbstractHandler {
|
||||
|
||||
/**
|
||||
* Creates new instance of the Amplitude analytics handler.
|
||||
*
|
||||
* @param {Object} options - The amplitude options.
|
||||
* @param {string} options.amplitudeAPPKey - The Amplitude app key required by the Amplitude API
|
||||
* in the Amplitude events.
|
||||
*/
|
||||
constructor(options: any) {
|
||||
super(options);
|
||||
|
||||
const {
|
||||
amplitudeAPPKey,
|
||||
user
|
||||
} = options;
|
||||
|
||||
this._enabled = true;
|
||||
|
||||
initAmplitude(amplitudeAPPKey, user)
|
||||
.then(() => {
|
||||
logger.info('Amplitude initialized');
|
||||
fixDeviceID(amplitude);
|
||||
})
|
||||
.catch(e => {
|
||||
logger.error('Error initializing Amplitude', e);
|
||||
this._enabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Amplitude user properties.
|
||||
*
|
||||
* @param {Object} userProps - The user properties.
|
||||
* @returns {void}
|
||||
*/
|
||||
setUserProperties(userProps: any) {
|
||||
if (this._enabled) {
|
||||
const identify = new Identify();
|
||||
|
||||
// Set all properties
|
||||
Object.entries(userProps).forEach(([ key, value ]) => {
|
||||
identify.set(key, value as any);
|
||||
});
|
||||
|
||||
amplitude.identify(identify);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: IEvent) {
|
||||
if (this._shouldIgnore(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventName = this._extractName(event) ?? '';
|
||||
|
||||
amplitude.track(eventName, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return amplitude identity information.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
getIdentityProps() {
|
||||
return {
|
||||
sessionId: amplitude.getSessionId(),
|
||||
deviceId: amplitude.getDeviceId(),
|
||||
userId: amplitude.getUserId()
|
||||
};
|
||||
}
|
||||
}
|
||||
170
react/features/analytics/handlers/MatomoHandler.ts
Normal file
170
react/features/analytics/handlers/MatomoHandler.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/* global _paq */
|
||||
|
||||
import { getJitsiMeetGlobalNS } from '../../base/util/helpers';
|
||||
|
||||
import AbstractHandler, { IEvent } from './AbstractHandler';
|
||||
|
||||
/**
|
||||
* Analytics handler for Matomo.
|
||||
*/
|
||||
export default class MatomoHandler extends AbstractHandler {
|
||||
_userProperties: Object;
|
||||
|
||||
/**
|
||||
* Creates new instance of the Matomo handler.
|
||||
*
|
||||
* @param {Object} options - The matomo options.
|
||||
* @param {string} options.matomoEndpoint - The Matomo endpoint.
|
||||
* @param {string} options.matomoSiteID - The site ID.
|
||||
*/
|
||||
constructor(options: any) {
|
||||
super(options);
|
||||
this._userProperties = {};
|
||||
|
||||
if (!options.matomoEndpoint) {
|
||||
throw new Error(
|
||||
'Failed to initialize Matomo handler: no endpoint defined.'
|
||||
);
|
||||
}
|
||||
if (!options.matomoSiteID) {
|
||||
throw new Error(
|
||||
'Failed to initialize Matomo handler: no site ID defined.'
|
||||
);
|
||||
}
|
||||
|
||||
this._enabled = true;
|
||||
this._initMatomo(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the _paq object.
|
||||
*
|
||||
* @param {Object} options - The matomo options.
|
||||
* @param {string} options.matomoEndpoint - The Matomo endpoint.
|
||||
* @param {string} options.matomoSiteID - The site ID.
|
||||
* @returns {void}
|
||||
*/
|
||||
_initMatomo(options: any) {
|
||||
// @ts-ignore
|
||||
const _paq = window._paq || [];
|
||||
|
||||
// @ts-ignore
|
||||
window._paq = _paq;
|
||||
|
||||
_paq.push([ 'trackPageView' ]);
|
||||
_paq.push([ 'enableLinkTracking' ]);
|
||||
|
||||
(function() {
|
||||
// add trailing slash if needed
|
||||
const u = options.matomoEndpoint.endsWith('/')
|
||||
? options.matomoEndpoint
|
||||
: `${options.matomoEndpoint}/`;
|
||||
|
||||
// configure the tracker
|
||||
_paq.push([ 'setTrackerUrl', `${u}matomo.php` ]);
|
||||
_paq.push([ 'setSiteId', options.matomoSiteID ]);
|
||||
|
||||
// insert the matomo script
|
||||
const d = document,
|
||||
g = d.createElement('script'),
|
||||
s = d.getElementsByTagName('script')[0];
|
||||
|
||||
g.type = 'text/javascript';
|
||||
g.async = true;
|
||||
g.defer = true;
|
||||
g.src = `${u}matomo.js`;
|
||||
s.parentNode?.insertBefore(g, s);
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the integer to use for a Matomo 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 Matomo
|
||||
* event, or NaN if the lib-jitsi-meet event doesn't contain a
|
||||
* suitable value.
|
||||
* @private
|
||||
*/
|
||||
_extractValue(event: IEvent) {
|
||||
const value = event?.attributes?.value;
|
||||
|
||||
// Try to extract an integer from the 'value' attribute.
|
||||
return Math.round(parseFloat(value ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the permanent properties for the current session.
|
||||
*
|
||||
* @param {Object} userProps - The permanent properties.
|
||||
* @returns {void}
|
||||
*/
|
||||
setUserProperties(userProps: any = {}) {
|
||||
if (!this._enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visitScope = [ 'user_agent', 'callstats_name', 'browser_name' ];
|
||||
|
||||
// add variables in the 'page' scope
|
||||
Object.keys(userProps)
|
||||
.filter(key => visitScope.indexOf(key) === -1)
|
||||
.forEach((key, index) => {
|
||||
// @ts-ignore
|
||||
_paq.push([
|
||||
'setCustomVariable',
|
||||
1 + index,
|
||||
key,
|
||||
userProps[key],
|
||||
'page'
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
// add variables in the 'visit' scope
|
||||
Object.keys(userProps)
|
||||
.filter(key => visitScope.indexOf(key) !== -1)
|
||||
.forEach((key, index) => {
|
||||
// @ts-ignore
|
||||
_paq.push([
|
||||
'setCustomVariable',
|
||||
1 + index,
|
||||
key,
|
||||
userProps[key],
|
||||
'visit'
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the entry point of the API. The function sends an event to
|
||||
* the Matomo endpoint. 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: IEvent) {
|
||||
if (this._shouldIgnore(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = this._extractValue(event);
|
||||
const matomoEvent: Array<string | number | undefined> = [
|
||||
'trackEvent', 'jitsi-meet', this._extractName(event) ];
|
||||
|
||||
if (!isNaN(value)) {
|
||||
matomoEvent.push(value);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
_paq.push(matomoEvent);
|
||||
}
|
||||
}
|
||||
|
||||
const globalNS = getJitsiMeetGlobalNS();
|
||||
|
||||
globalNS.analyticsHandlers = globalNS.analyticsHandlers || [];
|
||||
globalNS.analyticsHandlers.push(MatomoHandler);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Types } from '@amplitude/analytics-react-native';
|
||||
import DefaultPreference from 'react-native-default-preference';
|
||||
import { getUniqueId } from 'react-native-device-info';
|
||||
|
||||
import logger from '../../logger';
|
||||
|
||||
|
||||
/**
|
||||
* Custom logic for setting the correct device id.
|
||||
*
|
||||
* @param {Types.ReactNativeClient} amplitude - The amplitude instance.
|
||||
* @returns {void}
|
||||
*/
|
||||
export async function fixDeviceID(amplitude: Types.ReactNativeClient) {
|
||||
await DefaultPreference.setName('jitsi-preferences');
|
||||
|
||||
const current = await DefaultPreference.get('amplitudeDeviceId');
|
||||
|
||||
if (current) {
|
||||
amplitude.setDeviceId(current);
|
||||
} else {
|
||||
const uid = await getUniqueId();
|
||||
|
||||
if (!uid) {
|
||||
logger.warn('Device ID is not set!');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
amplitude.setDeviceId(uid as string);
|
||||
await DefaultPreference.set('amplitudeDeviceId', uid as string);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Types } from '@amplitude/analytics-browser';
|
||||
// @ts-ignore
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
|
||||
import logger from '../../logger';
|
||||
|
||||
/**
|
||||
* Key used to store the device id in local storage.
|
||||
*/
|
||||
const DEVICE_ID_KEY = '__AMDID';
|
||||
|
||||
/**
|
||||
* Custom logic for setting the correct device id.
|
||||
*
|
||||
* @param {Types.BrowserClient} amplitude - The amplitude instance.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function fixDeviceID(amplitude: Types.BrowserClient) {
|
||||
const deviceId = jitsiLocalStorage.getItem(DEVICE_ID_KEY);
|
||||
|
||||
if (deviceId) {
|
||||
// Set the device id in Amplitude.
|
||||
try {
|
||||
amplitude.setDeviceId(JSON.parse(deviceId));
|
||||
} catch (error) {
|
||||
logger.error('Failed to set device ID in Amplitude', error);
|
||||
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
} else {
|
||||
const newDeviceId = amplitude.getDeviceId();
|
||||
|
||||
if (newDeviceId) {
|
||||
jitsiLocalStorage.setItem(DEVICE_ID_KEY, JSON.stringify(newDeviceId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amplitude shared deviceId.
|
||||
*
|
||||
* @returns {string} - The amplitude deviceId.
|
||||
*/
|
||||
export function getDeviceID() {
|
||||
return jitsiLocalStorage.getItem(DEVICE_ID_KEY);
|
||||
}
|
||||
15
react/features/analytics/handlers/amplitude/lib.native.ts
Normal file
15
react/features/analytics/handlers/amplitude/lib.native.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import amplitude from '@amplitude/analytics-react-native';
|
||||
|
||||
export default amplitude;
|
||||
|
||||
/**
|
||||
* Initializes the Amplitude instance.
|
||||
*
|
||||
* @param {string} amplitudeAPPKey - The Amplitude app key.
|
||||
* @param {string | undefined} user - The user ID.
|
||||
* @returns {Promise} The initialized Amplitude instance.
|
||||
*/
|
||||
export function initAmplitude(
|
||||
amplitudeAPPKey: string, user: string | undefined): Promise<unknown> {
|
||||
return amplitude.init(amplitudeAPPKey, user, {}).promise;
|
||||
}
|
||||
38
react/features/analytics/handlers/amplitude/lib.web.ts
Normal file
38
react/features/analytics/handlers/amplitude/lib.web.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createInstance } from '@amplitude/analytics-browser';
|
||||
|
||||
const amplitude = createInstance();
|
||||
|
||||
export default amplitude;
|
||||
|
||||
/**
|
||||
* Initializes the Amplitude instance.
|
||||
*
|
||||
* @param {string} amplitudeAPPKey - The Amplitude app key.
|
||||
* @param {string | undefined} user - The user ID.
|
||||
* @returns {Promise} The initialized Amplitude instance.
|
||||
*/
|
||||
export function initAmplitude(
|
||||
amplitudeAPPKey: string, user: string | undefined): Promise<unknown> {
|
||||
|
||||
// Forces sending all events on exit (flushing) via sendBeacon.
|
||||
window.addEventListener('pagehide', () => {
|
||||
// Set https transport to use sendBeacon API.
|
||||
amplitude.setTransport('beacon');
|
||||
// Send all pending events to server.
|
||||
amplitude.flush();
|
||||
});
|
||||
|
||||
const options = {
|
||||
autocapture: {
|
||||
attribution: true,
|
||||
pageViews: true,
|
||||
sessions: false,
|
||||
fileDownloads: false,
|
||||
formInteractions: false,
|
||||
elementInteractions: false
|
||||
},
|
||||
defaultTracking: false
|
||||
};
|
||||
|
||||
return amplitude.init(amplitudeAPPKey, user, options).promise;
|
||||
}
|
||||
Reference in New Issue
Block a user