diff --git a/Makefile b/Makefile index 0cb668e53..7b0733e2a 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,8 @@ deploy-appbundle: $(OUTPUT_DIR)/analytics-ga.js \ $(BUILD_DIR)/analytics-ga.min.js \ $(BUILD_DIR)/analytics-ga.min.map \ + $(BUILD_DIR)/analytics-matomo.min.js \ + $(BUILD_DIR)/analytics-matomo.min.map \ $(BUILD_DIR)/video-blur-effect.min.js \ $(BUILD_DIR)/video-blur-effect.min.map \ $(BUILD_DIR)/rnnoise-processor.min.js \ diff --git a/config.js b/config.js index 80998a54f..5e03cd3b1 100644 --- a/config.js +++ b/config.js @@ -371,13 +371,18 @@ var config = { // The Google Analytics Tracking ID: // googleAnalyticsTrackingId: 'your-tracking-id-UA-123456-1' + // Matomo configuration: + // matomoEndpoint: 'https://your-matomo-endpoint/', + // matomoSiteID: '42', + // The Amplitude APP Key: // amplitudeAPPKey: '' // Array of script URLs to load as lib-jitsi-meet "analytics handlers". // scriptURLs: [ - // "libs/analytics-ga.min.js", // google-analytics - // "https://example.com/my-custom-analytics.js" + // "libs/analytics-ga.min.js", // google-analytics + // "libs/analytics-matomo.min.js", // Matomo + // "https://example.com/my-custom-analytics.js" // ], }, diff --git a/react/features/analytics/functions.js b/react/features/analytics/functions.js index bb6fd8ca0..6dd66cd60 100644 --- a/react/features/analytics/functions.js +++ b/react/features/analytics/functions.js @@ -65,6 +65,8 @@ export function createHandlers({ getState }: { getState: Function }) { blackListedEvents, scriptURLs, googleAnalyticsTrackingId, + matomoEndpoint, + matomoSiteID, whiteListedEvents } = analyticsConfig; const { group, user } = state['features/base/jwt']; @@ -73,6 +75,8 @@ export function createHandlers({ getState }: { getState: Function }) { blackListedEvents, envType: (deploymentInfo && deploymentInfo.envType) || 'dev', googleAnalyticsTrackingId, + matomoEndpoint, + matomoSiteID, group, host, product: deploymentInfo && deploymentInfo.product, @@ -95,7 +99,7 @@ export function createHandlers({ getState }: { getState: Function }) { .then(externalHandlers => { handlers.push(...externalHandlers); if (handlers.length === 0) { - // Throwing an error in order to dispose the analytics in the catch clause due to the lack of any + // Throwing an error in order to dispose the analytics in the catch clause due to the lack of any // analytics handlers. throw new Error('No analytics handlers created!'); } diff --git a/react/features/analytics/handlers/MatomoHandler.js b/react/features/analytics/handlers/MatomoHandler.js new file mode 100644 index 000000000..5efba6102 --- /dev/null +++ b/react/features/analytics/handlers/MatomoHandler.js @@ -0,0 +1,163 @@ +/* global _paq */ + +import { getJitsiMeetGlobalNS } from '../../base/util'; + +import AbstractHandler from './AbstractHandler'; + +/** + * Analytics handler for Matomo. + */ +class MatomoHandler extends AbstractHandler { + + /** + * Creates new instance of the Matomo handler. + * + * @param {Object} options - + * @param {string} options.matomoEndpoint - The Matomo endpoint. + * @param {string} options.matomoSiteID - The site ID. + */ + constructor(options) { + 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 - + * @param {string} options.matomoEndpoint - The Matomo endpoint. + * @param {string} options.matomoSiteID - The site ID. + * @returns {void} + */ + _initMatomo(options) { + const _paq = window._paq || []; + + 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) { + const value = event && event.attributes && 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 = {}) { + 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) => { + _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) => { + _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) { + if (this._shouldIgnore(event)) { + return; + } + + const value = this._extractValue(event); + const matomoEvent = [ 'trackEvent', 'jitsi-meet', this._extractName(event) ]; + + if (!isNaN(value)) { + matomoEvent.push(value); + } + + _paq.push(matomoEvent); + } +} + +const globalNS = getJitsiMeetGlobalNS(); + +globalNS.analyticsHandlers = globalNS.analyticsHandlers || []; +globalNS.analyticsHandlers.push(MatomoHandler); diff --git a/webpack.config.js b/webpack.config.js index 72407cfe5..a8d577a81 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -217,6 +217,12 @@ module.exports = [ }, performance: getPerformanceHints(5 * 1024) }), + Object.assign({}, config, { + entry: { + 'analytics-matomo': './react/features/analytics/handlers/MatomoHandler.js' + }, + performance: getPerformanceHints(5 * 1024) + }), // Because both video-blur-effect and rnnoise-processor modules are loaded // in a lazy manner using the loadScript function with a hard coded name,