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

AnalyticsAdapter.js 11KB

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