init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View 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;
}
}

View 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()
};
}
}

View 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);

View File

@@ -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);
}
}

View File

@@ -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);
}

View 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;
}

View 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;
}