您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

AnalyticsAdapter.js 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. import {
  2. TYPE_OPERATIONAL,
  3. TYPE_PAGE,
  4. TYPE_TRACK,
  5. TYPE_UI
  6. } from '../../service/statistics/AnalyticsEvents';
  7. import { getLogger } from 'jitsi-meet-logger';
  8. const MAX_CACHE_SIZE = 100;
  9. // eslist-disable-line no-undef
  10. const logger = getLogger(__filename);
  11. /**
  12. * This class provides an API to lib-jitsi-meet and its users for sending
  13. * analytics events. It serves as a bridge to different backend implementations
  14. * ("analytics handlers") and a cache for events attempted to be sent before
  15. * the analytics handlers were enabled.
  16. *
  17. * The API is designed to be an easy replacement for the previous version of
  18. * this adapter, and is meant to be extended with more convenience methods.
  19. *
  20. *
  21. * The API calls are translated to objects with the following structure, which
  22. * are then passed to the sendEvent(event) function of the underlying handlers:
  23. *
  24. * {
  25. * type,
  26. *
  27. * action,
  28. * actionSubject,
  29. * actionSubjectId,
  30. * attributes,
  31. * categories,
  32. * containerId,
  33. * containerType,
  34. * name,
  35. * objectId,
  36. * objectType,
  37. * source,
  38. * tags
  39. * }
  40. *
  41. * The 'type' is one of 'operational', 'page', 'track' or 'ui', and some of the
  42. * other properties are considered required according to the type.
  43. *
  44. * For events with type 'page', the required properties are: name.
  45. *
  46. * For events with type 'operational' and 'ui', the required properties are:
  47. * action, actionSubject, source
  48. *
  49. * For events with type 'page', the required properties are:
  50. * action, actionSubject, source, containerType, containerId, objectType,
  51. * objectId
  52. */
  53. class AnalyticsAdapter {
  54. /**
  55. * Creates new AnalyticsAdapter instance.
  56. */
  57. constructor(options) {
  58. this.options = options;
  59. this.reset();
  60. }
  61. /**
  62. * Reset the state to the initial one.
  63. *
  64. * @returns {void}
  65. */
  66. reset() {
  67. /**
  68. * Whether this AnalyticsAdapter has been disposed of or not. Once this
  69. * is set to true, the AnalyticsAdapter is disabled and does not accept
  70. * any more events, and it can not be re-enabled.
  71. * @type {boolean}
  72. */
  73. this.disposed = false;
  74. /**
  75. * The set of handlers to which events will be sent.
  76. * @type {Set<any>}
  77. */
  78. this.analyticsHandlers = new Set();
  79. /**
  80. * The cache of events which are not sent yet. The cache is enabled
  81. * while this field is truthy, and disabled otherwise.
  82. * @type {Array}
  83. */
  84. this.cache = [];
  85. /**
  86. * Map of properties that will be added to every event. Note that the
  87. * keys will be prefixed with "permanent.".
  88. */
  89. this.permanentProperties = {};
  90. /**
  91. * The name of the conference that this AnalyticsAdapter is associated
  92. * with.
  93. * @type {null}
  94. */
  95. this.conferenceName = '';
  96. }
  97. /**
  98. * Dispose analytics. Clears all handlers.
  99. */
  100. dispose() {
  101. logger.warn('Disposing of analytics adapter.');
  102. if (this.analyticsHandlers && this.analyticsHandlers.size > 0) {
  103. this.analyticsHandlers.forEach(handler => {
  104. if (typeof handler.dispose === 'function') {
  105. handler.dispose();
  106. }
  107. });
  108. }
  109. this.setAnalyticsHandlers([]);
  110. this.disposed = true;
  111. }
  112. /**
  113. * Sets the handlers that are going to be used to send analytics. Sends any
  114. * cached events.
  115. * @param {Array} handlers the handlers
  116. */
  117. setAnalyticsHandlers(handlers) {
  118. if (this.disposed) {
  119. return;
  120. }
  121. this.analyticsHandlers = new Set(handlers);
  122. this._setUserProperties();
  123. // Note that we disable the cache even if the set of handlers is empty.
  124. const cache = this.cache;
  125. this.cache = null;
  126. if (cache) {
  127. cache.forEach(event => this._sendEvent(event));
  128. }
  129. }
  130. /**
  131. * Set the user properties to the analytics handlers.
  132. *
  133. * @returns {void}
  134. */
  135. _setUserProperties() {
  136. this.analyticsHandlers.forEach(handler => {
  137. try {
  138. handler.setUserProperties(this.permanentProperties);
  139. } catch (error) {
  140. logger.warn('Error in setUserProperties method of one of the '
  141. + `analytics handlers: ${error}`);
  142. }
  143. });
  144. }
  145. /**
  146. * Adds a set of permanent properties to this this AnalyticsAdapter.
  147. * Permanent properties will be added as "attributes" to events sent to
  148. * the underlying "analytics handlers", and their keys will be prefixed
  149. * by "permanent_", i.e. adding a permanent property {key: "value"} will
  150. * result in {"permanent_key": "value"} object to be added to the
  151. * "attributes" field of events.
  152. *
  153. * @param {Object} properties the properties to add
  154. */
  155. addPermanentProperties(properties) {
  156. this.permanentProperties = {
  157. ...this.permanentProperties,
  158. ...properties
  159. };
  160. this._setUserProperties();
  161. }
  162. /**
  163. * Sets the name of the conference that this AnalyticsAdapter is associated
  164. * with.
  165. * @param name the name to set.
  166. */
  167. setConferenceName(name) {
  168. this.conferenceName = name;
  169. this.addPermanentProperties({ 'conference_name': name });
  170. }
  171. /**
  172. * Sends an event with a given name and given properties. The first
  173. * parameter is either a string or an object. If it is a string, it is used
  174. * as the event name and the second parameter is used at the attributes to
  175. * attach to the event. If it is an object, it represents the whole event,
  176. * including any desired attributes, and the second parameter is ignored.
  177. *
  178. * @param {String|Object} eventName either a string to be used as the name
  179. * of the event, or an event object. If an event object is passed, the
  180. * properties parameters is ignored.
  181. * @param {Object} properties the properties/attributes to attach to the
  182. * event, if eventName is a string.
  183. */
  184. sendEvent(eventName, properties = {}) {
  185. if (this.disposed) {
  186. return;
  187. }
  188. let event = null;
  189. if (typeof eventName === 'string') {
  190. event = {
  191. type: TYPE_OPERATIONAL,
  192. action: eventName,
  193. actionSubject: eventName,
  194. source: eventName,
  195. attributes: properties
  196. };
  197. } else if (typeof eventName === 'object') {
  198. event = eventName;
  199. }
  200. if (!this._verifyRequiredFields(event)) {
  201. logger.error(
  202. `Dropping a mis-formatted event: ${JSON.stringify(event)}`);
  203. return;
  204. }
  205. this._sendEvent(event);
  206. }
  207. /**
  208. * Checks whether an event has all of the required fields set, and tries
  209. * to fill in some of the missing fields with reasonable default values.
  210. * Returns true if after this operation the event has all of the required
  211. * fields set, and false otherwise (if some of the required fields were not
  212. * set and the attempt to fill them in with a default failed).
  213. *
  214. * @param event the event object.
  215. * @return {boolean} true if the event (after the call to this function)
  216. * contains all of the required fields, and false otherwise.
  217. * @private
  218. */
  219. _verifyRequiredFields(event) {
  220. if (!event) {
  221. return false;
  222. }
  223. if (!event.type) {
  224. event.type = TYPE_OPERATIONAL;
  225. }
  226. const type = event.type;
  227. if (type !== TYPE_OPERATIONAL && type !== TYPE_PAGE
  228. && type !== TYPE_UI && type !== TYPE_TRACK) {
  229. logger.error(`Unknown event type: ${type}`);
  230. return false;
  231. }
  232. if (type === TYPE_PAGE) {
  233. return Boolean(event.name);
  234. }
  235. // Try to set some reasonable default values in case some of the
  236. // parameters required by the handler API are missing.
  237. event.action = event.action || event.name || event.actionSubject;
  238. event.actionSubject = event.actionSubject || event.name || event.action;
  239. event.source = event.source || event.name || event.action
  240. || event.actionSubject;
  241. if (!event.action || !event.actionSubject || !event.source) {
  242. logger.error(
  243. 'Required field missing (action, actionSubject or source)');
  244. return false;
  245. }
  246. // Track events have additional required fields.
  247. if (type === TYPE_TRACK) {
  248. event.objectType = event.objectType || 'generic-object-type';
  249. event.containerType = event.containerType || 'conference';
  250. if (event.containerType === 'conference' && !event.containerId) {
  251. event.containerId = this.conferenceName;
  252. }
  253. if (!event.objectType || !event.objectId
  254. || !event.containerType || !event.containerId) {
  255. logger.error(
  256. 'Required field missing (containerId, containerType, '
  257. + 'objectId or objectType)');
  258. return false;
  259. }
  260. }
  261. return true;
  262. }
  263. /**
  264. * Saves an event to the cache, if the cache is enabled.
  265. * @param event the event to save.
  266. * @returns {boolean} true if the event was saved, and false otherwise (i.e.
  267. * if the cache was disabled).
  268. * @private
  269. */
  270. _maybeCacheEvent(event) {
  271. if (this.cache) {
  272. this.cache.push(event);
  273. // We limit the size of the cache, in case the user fails to ever
  274. // set the analytics handlers.
  275. if (this.cache.length > MAX_CACHE_SIZE) {
  276. this.cache.splice(0, 1);
  277. }
  278. return true;
  279. }
  280. return false;
  281. }
  282. /**
  283. *
  284. * @param event
  285. * @private
  286. */
  287. _sendEvent(event) {
  288. if (this._maybeCacheEvent(event)) {
  289. // The event was consumed by the cache.
  290. } else {
  291. this.analyticsHandlers.forEach(handler => {
  292. try {
  293. handler.sendEvent(event);
  294. } catch (e) {
  295. logger.warn(`Error sending analytics event: ${e}`);
  296. }
  297. });
  298. }
  299. }
  300. }
  301. export default new AnalyticsAdapter();