You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

functions.js 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. // @flow
  2. import { API_ID } from '../../../modules/API/constants';
  3. import {
  4. checkChromeExtensionsInstalled,
  5. isMobileBrowser
  6. } from '../base/environment/utils';
  7. import JitsiMeetJS, {
  8. analytics,
  9. browser,
  10. isAnalyticsEnabled
  11. } from '../base/lib-jitsi-meet';
  12. import { getJitsiMeetGlobalNS, loadScript } from '../base/util';
  13. import { AmplitudeHandler, MatomoHandler } from './handlers';
  14. import logger from './logger';
  15. /**
  16. * Sends an event through the lib-jitsi-meet AnalyticsAdapter interface.
  17. *
  18. * @param {Object} event - The event to send. It should be formatted as
  19. * described in AnalyticsAdapter.js in lib-jitsi-meet.
  20. * @returns {void}
  21. */
  22. export function sendAnalytics(event: Object) {
  23. try {
  24. analytics.sendEvent(event);
  25. } catch (e) {
  26. logger.warn(`Error sending analytics event: ${e}`);
  27. }
  28. }
  29. /**
  30. * Return saved amplitude identity info such as session id, device id and user id. We assume these do not change for
  31. * the duration of the conference.
  32. *
  33. * @returns {Object}
  34. */
  35. export function getAmplitudeIdentity() {
  36. return analytics.amplitudeIdentityProps;
  37. }
  38. /**
  39. * Resets the analytics adapter to its initial state - removes handlers, cache,
  40. * disabled state, etc.
  41. *
  42. * @returns {void}
  43. */
  44. export function resetAnalytics() {
  45. analytics.reset();
  46. }
  47. /**
  48. * Creates the analytics handlers.
  49. *
  50. * @param {Store} store - The redux store in which the specified {@code action} is being dispatched.
  51. * @returns {Promise} Resolves with the handlers that have been successfully loaded.
  52. */
  53. export function createHandlers({ getState }: { getState: Function }) {
  54. getJitsiMeetGlobalNS().analyticsHandlers = [];
  55. window.analyticsHandlers = []; // Legacy support.
  56. if (!isAnalyticsEnabled(getState)) {
  57. return Promise.resolve([]);
  58. }
  59. const state = getState();
  60. const config = state['features/base/config'];
  61. const { locationURL } = state['features/base/connection'];
  62. const host = locationURL ? locationURL.host : '';
  63. const {
  64. analytics: analyticsConfig = {},
  65. deploymentInfo
  66. } = config;
  67. const {
  68. amplitudeAPPKey,
  69. blackListedEvents,
  70. scriptURLs,
  71. googleAnalyticsTrackingId,
  72. matomoEndpoint,
  73. matomoSiteID,
  74. whiteListedEvents
  75. } = analyticsConfig;
  76. const { group, user } = state['features/base/jwt'];
  77. const handlerConstructorOptions = {
  78. amplitudeAPPKey,
  79. blackListedEvents,
  80. envType: (deploymentInfo && deploymentInfo.envType) || 'dev',
  81. googleAnalyticsTrackingId,
  82. matomoEndpoint,
  83. matomoSiteID,
  84. group,
  85. host,
  86. product: deploymentInfo && deploymentInfo.product,
  87. subproduct: deploymentInfo && deploymentInfo.environment,
  88. user: user && user.id,
  89. version: JitsiMeetJS.version,
  90. whiteListedEvents
  91. };
  92. const handlers = [];
  93. try {
  94. const amplitude = new AmplitudeHandler(handlerConstructorOptions);
  95. analytics.amplitudeIdentityProps = amplitude.getIdentityProps();
  96. handlers.push(amplitude);
  97. // eslint-disable-next-line no-empty
  98. } catch (e) {}
  99. try {
  100. const matomo = new MatomoHandler(handlerConstructorOptions);
  101. handlers.push(matomo);
  102. // eslint-disable-next-line no-empty
  103. } catch (e) {}
  104. return (
  105. _loadHandlers(scriptURLs, handlerConstructorOptions)
  106. .then(externalHandlers => {
  107. handlers.push(...externalHandlers);
  108. if (handlers.length === 0) {
  109. // Throwing an error in order to dispose the analytics in the catch clause due to the lack of any
  110. // analytics handlers.
  111. throw new Error('No analytics handlers created!');
  112. }
  113. return handlers;
  114. })
  115. .catch(e => {
  116. analytics.dispose();
  117. if (handlers.length !== 0) {
  118. logger.error(e);
  119. }
  120. return [];
  121. }));
  122. }
  123. /**
  124. * Inits JitsiMeetJS.analytics by setting permanent properties and setting the handlers from the loaded scripts.
  125. * NOTE: Has to be used after JitsiMeetJS.init. Otherwise analytics will be null.
  126. *
  127. * @param {Store} store - The redux store in which the specified {@code action} is being dispatched.
  128. * @param {Array<Object>} handlers - The analytics handlers.
  129. * @returns {void}
  130. */
  131. export function initAnalytics({ getState }: { getState: Function }, handlers: Array<Object>) {
  132. if (!isAnalyticsEnabled(getState) || handlers.length === 0) {
  133. return;
  134. }
  135. const state = getState();
  136. const config = state['features/base/config'];
  137. const {
  138. deploymentInfo
  139. } = config;
  140. const { group, server } = state['features/base/jwt'];
  141. const roomName = state['features/base/conference'].room;
  142. const permanentProperties = {};
  143. if (server) {
  144. permanentProperties.server = server;
  145. }
  146. if (group) {
  147. permanentProperties.group = group;
  148. }
  149. // Report if user is using websocket
  150. permanentProperties.websocket = navigator.product !== 'ReactNative' && typeof config.websocket === 'string';
  151. // permanentProperties is external api
  152. permanentProperties.externalApi = typeof API_ID === 'number';
  153. // Report if we are loaded in iframe
  154. permanentProperties.inIframe = _inIframe();
  155. // Optionally, include local deployment information based on the
  156. // contents of window.config.deploymentInfo.
  157. if (deploymentInfo) {
  158. for (const key in deploymentInfo) {
  159. if (deploymentInfo.hasOwnProperty(key)) {
  160. permanentProperties[key] = deploymentInfo[key];
  161. }
  162. }
  163. }
  164. analytics.addPermanentProperties(permanentProperties);
  165. analytics.setConferenceName(roomName);
  166. // Set the handlers last, since this triggers emptying of the cache
  167. analytics.setAnalyticsHandlers(handlers);
  168. if (!isMobileBrowser() && browser.isChrome()) {
  169. const bannerCfg = state['features/base/config'].chromeExtensionBanner;
  170. checkChromeExtensionsInstalled(bannerCfg).then(extensionsInstalled => {
  171. if (extensionsInstalled?.length) {
  172. analytics.addPermanentProperties({
  173. hasChromeExtension: extensionsInstalled.some(ext => ext)
  174. });
  175. }
  176. });
  177. }
  178. }
  179. /**
  180. * Checks whether we are loaded in iframe.
  181. *
  182. * @returns {boolean} Returns {@code true} if loaded in iframe.
  183. * @private
  184. */
  185. function _inIframe() {
  186. if (navigator.product === 'ReactNative') {
  187. return false;
  188. }
  189. try {
  190. return window.self !== window.top;
  191. } catch (e) {
  192. return true;
  193. }
  194. }
  195. /**
  196. * Tries to load the scripts for the analytics handlers and creates them.
  197. *
  198. * @param {Array} scriptURLs - The array of script urls to load.
  199. * @param {Object} handlerConstructorOptions - The default options to pass when creating handlers.
  200. * @private
  201. * @returns {Promise} Resolves with the handlers that have been successfully loaded and rejects if there are no handlers
  202. * loaded or the analytics is disabled.
  203. */
  204. function _loadHandlers(scriptURLs = [], handlerConstructorOptions) {
  205. const promises = [];
  206. for (const url of scriptURLs) {
  207. promises.push(
  208. loadScript(url).then(
  209. () => {
  210. return { type: 'success' };
  211. },
  212. error => {
  213. return {
  214. type: 'error',
  215. error,
  216. url
  217. };
  218. }));
  219. }
  220. return Promise.all(promises).then(values => {
  221. for (const el of values) {
  222. if (el.type === 'error') {
  223. logger.warn(`Failed to load ${el.url}: ${el.error}`);
  224. }
  225. }
  226. // analyticsHandlers is the handlers we want to use
  227. // we search for them in the JitsiMeetGlobalNS, but also
  228. // check the old location to provide legacy support
  229. const analyticsHandlers = [
  230. ...getJitsiMeetGlobalNS().analyticsHandlers,
  231. ...window.analyticsHandlers
  232. ];
  233. const handlers = [];
  234. for (const Handler of analyticsHandlers) {
  235. // Catch any error while loading to avoid skipping analytics in case
  236. // of multiple scripts.
  237. try {
  238. handlers.push(new Handler(handlerConstructorOptions));
  239. } catch (error) {
  240. logger.warn(`Error creating analytics handler: ${error}`);
  241. }
  242. }
  243. logger.debug(`Loaded ${handlers.length} analytics handlers`);
  244. return handlers;
  245. });
  246. }