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

AnalyticsAdapter.js 10KB

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