123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- import { getLogger } from '@jitsi/logger';
-
- import {
- TYPE_OPERATIONAL,
- TYPE_PAGE,
- TYPE_TRACK,
- TYPE_UI
- } from '../../service/statistics/AnalyticsEvents';
- import browser from '../browser';
-
- const MAX_CACHE_SIZE = 100;
-
- // eslist-disable-line no-undef
- const logger = getLogger(__filename);
-
- /**
- * This class provides an API to lib-jitsi-meet and its users for sending
- * analytics events. It serves as a bridge to different backend implementations
- * ("analytics handlers") and a cache for events attempted to be sent before
- * the analytics handlers were enabled.
- *
- * The API is designed to be an easy replacement for the previous version of
- * this adapter, and is meant to be extended with more convenience methods.
- *
- *
- * The API calls are translated to objects with the following structure, which
- * are then passed to the sendEvent(event) function of the underlying handlers:
- *
- * {
- * type,
- *
- * action,
- * actionSubject,
- * actionSubjectId,
- * attributes,
- * categories,
- * containerId,
- * containerType,
- * name,
- * objectId,
- * objectType,
- * source,
- * tags
- * }
- *
- * The 'type' is one of 'operational', 'page', 'track' or 'ui', and some of the
- * other properties are considered required according to the type.
- *
- * For events with type 'page', the required properties are: name.
- *
- * For events with type 'operational' and 'ui', the required properties are:
- * action, actionSubject, source
- *
- * For events with type 'page', the required properties are:
- * action, actionSubject, source, containerType, containerId, objectType,
- * objectId
- */
- class AnalyticsAdapter {
- /**
- * Creates new AnalyticsAdapter instance.
- */
- constructor() {
- this.reset();
- }
-
- /**
- * Reset the state to the initial one.
- *
- * @returns {void}
- */
- reset() {
- /**
- * Whether this AnalyticsAdapter has been disposed of or not. Once this
- * is set to true, the AnalyticsAdapter is disabled and does not accept
- * any more events, and it can not be re-enabled.
- * @type {boolean}
- */
- this.disposed = false;
-
- /**
- * The set of handlers to which events will be sent.
- * @type {Set<any>}
- */
- this.analyticsHandlers = new Set();
-
- /**
- * The cache of events which are not sent yet. The cache is enabled
- * while this field is truthy, and disabled otherwise.
- * @type {Array}
- */
- this.cache = [];
-
- /**
- * Map of properties that will be added to every event. Note that the
- * keys will be prefixed with "permanent.".
- */
- this.permanentProperties = {};
-
- /**
- * The name of the conference that this AnalyticsAdapter is associated
- * with.
- * @type {null}
- */
- this.conferenceName = '';
-
- this.addPermanentProperties({
- 'user_agent': navigator.userAgent,
- 'browser_name': browser.getName()
- });
- }
-
- /**
- * Dispose analytics. Clears all handlers.
- */
- dispose() {
- logger.warn('Disposing of analytics adapter.');
-
- if (this.analyticsHandlers && this.analyticsHandlers.size > 0) {
- this.analyticsHandlers.forEach(handler => {
- if (typeof handler.dispose === 'function') {
- handler.dispose();
- }
- });
- }
-
- this.setAnalyticsHandlers([]);
- this.disposed = true;
- }
-
- /**
- * Sets the handlers that are going to be used to send analytics. Sends any
- * cached events.
- * @param {Array} handlers the handlers
- */
- setAnalyticsHandlers(handlers) {
- if (this.disposed) {
- return;
- }
-
- this.analyticsHandlers = new Set(handlers);
-
- this._setUserProperties();
-
- // Note that we disable the cache even if the set of handlers is empty.
- const cache = this.cache;
-
- this.cache = null;
- if (cache) {
- cache.forEach(event => this._sendEvent(event));
- }
- }
-
- /**
- * Set the user properties to the analytics handlers.
- *
- * @returns {void}
- */
- _setUserProperties() {
- this.analyticsHandlers.forEach(handler => {
- try {
- handler.setUserProperties(this.permanentProperties);
- } catch (error) {
- logger.warn('Error in setUserProperties method of one of the '
- + `analytics handlers: ${error}`);
- }
- });
- }
-
- /**
- * Adds a set of permanent properties to this this AnalyticsAdapter.
- * Permanent properties will be added as "attributes" to events sent to
- * the underlying "analytics handlers", and their keys will be prefixed
- * by "permanent_", i.e. adding a permanent property {key: "value"} will
- * result in {"permanent_key": "value"} object to be added to the
- * "attributes" field of events.
- *
- * @param {Object} properties the properties to add
- */
- addPermanentProperties(properties) {
- this.permanentProperties = {
- ...this.permanentProperties,
- ...properties
- };
-
- this._setUserProperties();
- }
-
- /**
- * Sets the name of the conference that this AnalyticsAdapter is associated
- * with.
- * @param name the name to set.
- */
- setConferenceName(name) {
- this.conferenceName = name;
- this.addPermanentProperties({ 'conference_name': name });
- }
-
- /**
- * Sends an event with a given name and given properties. The first
- * parameter is either a string or an object. If it is a string, it is used
- * as the event name and the second parameter is used at the attributes to
- * attach to the event. If it is an object, it represents the whole event,
- * including any desired attributes, and the second parameter is ignored.
- *
- * @param {String|Object} eventName either a string to be used as the name
- * of the event, or an event object. If an event object is passed, the
- * properties parameters is ignored.
- * @param {Object} properties the properties/attributes to attach to the
- * event, if eventName is a string.
- */
- sendEvent(eventName, properties = {}) {
- if (this.disposed) {
- return;
- }
-
- let event = null;
-
- if (typeof eventName === 'string') {
- event = {
- type: TYPE_OPERATIONAL,
- action: eventName,
- actionSubject: eventName,
- source: eventName,
- attributes: properties
- };
- } else if (typeof eventName === 'object') {
- event = eventName;
- }
-
- if (!this._verifyRequiredFields(event)) {
- logger.error(
- `Dropping a mis-formatted event: ${JSON.stringify(event)}`);
-
- return;
- }
-
- this._sendEvent(event);
- }
-
- /**
- * Checks whether an event has all of the required fields set, and tries
- * to fill in some of the missing fields with reasonable default values.
- * Returns true if after this operation the event has all of the required
- * fields set, and false otherwise (if some of the required fields were not
- * set and the attempt to fill them in with a default failed).
- *
- * @param event the event object.
- * @return {boolean} true if the event (after the call to this function)
- * contains all of the required fields, and false otherwise.
- * @private
- */
- _verifyRequiredFields(event) {
- if (!event) {
- return false;
- }
-
- if (!event.type) {
- event.type = TYPE_OPERATIONAL;
- }
-
- const type = event.type;
-
- if (type !== TYPE_OPERATIONAL && type !== TYPE_PAGE
- && type !== TYPE_UI && type !== TYPE_TRACK) {
- logger.error(`Unknown event type: ${type}`);
-
- return false;
- }
-
- if (type === TYPE_PAGE) {
- return Boolean(event.name);
- }
-
- // Try to set some reasonable default values in case some of the
- // parameters required by the handler API are missing.
- event.action = event.action || event.name || event.actionSubject;
- event.actionSubject = event.actionSubject || event.name || event.action;
- event.source = event.source || event.name || event.action
- || event.actionSubject;
-
- if (!event.action || !event.actionSubject || !event.source) {
- logger.error(
- 'Required field missing (action, actionSubject or source)');
-
- return false;
- }
-
- // Track events have additional required fields.
- if (type === TYPE_TRACK) {
- event.objectType = event.objectType || 'generic-object-type';
- event.containerType = event.containerType || 'conference';
- if (event.containerType === 'conference' && !event.containerId) {
- event.containerId = this.conferenceName;
- }
-
-
- if (!event.objectType || !event.objectId
- || !event.containerType || !event.containerId) {
- logger.error(
- 'Required field missing (containerId, containerType, '
- + 'objectId or objectType)');
-
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Saves an event to the cache, if the cache is enabled.
- * @param event the event to save.
- * @returns {boolean} true if the event was saved, and false otherwise (i.e.
- * if the cache was disabled).
- * @private
- */
- _maybeCacheEvent(event) {
- if (this.cache) {
- this.cache.push(event);
-
- // We limit the size of the cache, in case the user fails to ever
- // set the analytics handlers.
- if (this.cache.length > MAX_CACHE_SIZE) {
- this.cache.splice(0, 1);
- }
-
- return true;
- }
-
- return false;
-
- }
-
- /**
- *
- * @param event
- * @private
- */
- _sendEvent(event) {
- if (this._maybeCacheEvent(event)) {
- // The event was consumed by the cache.
- } else {
- this.analyticsHandlers.forEach(handler => {
- try {
- handler.sendEvent(event);
- } catch (e) {
- logger.warn(`Error sending analytics event: ${e}`);
- }
- });
- }
- }
- }
-
- export default new AnalyticsAdapter();
|