diff --git a/react/features/analytics/AnalyticsEvents.js b/react/features/analytics/AnalyticsEvents.js index ed8b821445..515226a3a3 100644 --- a/react/features/analytics/AnalyticsEvents.js +++ b/react/features/analytics/AnalyticsEvents.js @@ -184,6 +184,23 @@ export function createRecentClickedEvent(eventName, attributes = {}) { }; } +/** + * Creates an event which indicate an action occured in the chrome extension banner. + * + * @param {boolean} installPressed - Whether the user pressed install or `x` - cancel. + * @param {Object} attributes - Attributes to attach to the event. + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. + */ +export function createChromeExtensionBannerEvent(installPressed, attributes = {}) { + return { + action: installPressed ? 'install' : 'cancel', + attributes, + source: 'chrome.extension.banner', + type: TYPE_UI + }; +} + /** * Creates an event which indicates that the recent list container is shown and * selected. diff --git a/react/features/analytics/functions.js b/react/features/analytics/functions.js index 0e2a1ef938..bb6fd8ca0a 100644 --- a/react/features/analytics/functions.js +++ b/react/features/analytics/functions.js @@ -2,10 +2,14 @@ import JitsiMeetJS, { analytics, + browser, isAnalyticsEnabled } from '../base/lib-jitsi-meet'; import { getJitsiMeetGlobalNS, loadScript } from '../base/util'; - +import { + checkChromeExtensionsInstalled, + isMobileBrowser +} from '../base/environment/utils'; import { AmplitudeHandler } from './handlers'; import logger from './logger'; @@ -154,6 +158,18 @@ export function initAnalytics({ getState }: { getState: Function }, handlers: Ar // Set the handlers last, since this triggers emptying of the cache analytics.setAnalyticsHandlers(handlers); + + if (!isMobileBrowser() && browser.isChrome()) { + const bannerCfg = state['features/base/config'].chromeExtensionBanner; + + checkChromeExtensionsInstalled(bannerCfg).then(extensionsInstalled => { + if (extensionsInstalled?.length) { + analytics.addPermanentProperties({ + hasChromeExtension: extensionsInstalled.some(ext => ext) + }); + } + }); + } } /** diff --git a/react/features/base/environment/utils.js b/react/features/base/environment/utils.js index 3542217fa8..386c49b9eb 100644 --- a/react/features/base/environment/utils.js +++ b/react/features/base/environment/utils.js @@ -1,3 +1,5 @@ +// @flow + import Platform from '../react/Platform'; /** @@ -8,3 +10,29 @@ import Platform from '../react/Platform'; export function isMobileBrowser() { return Platform.OS === 'android' || Platform.OS === 'ios'; } + +/** + * Checks whether the chrome extensions defined in the config file are installed or not. + * + * @param {Object} config - Objects containing info about the configured extensions. + * + * @returns {Promise[]} + */ +export function checkChromeExtensionsInstalled(config: Object = {}) { + const isExtensionInstalled = info => new Promise(resolve => { + const img = new Image(); + + img.src = `chrome-extension://${info.id}/${info.path}`; + img.onload = function() { + resolve(true); + }; + img.onerror = function() { + resolve(false); + }; + }); + const extensionInstalledFunction = info => isExtensionInstalled(info); + + return Promise.all( + (config.chromeExtensionsInfo || []).map(info => extensionInstalledFunction(info)) + ); +} diff --git a/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.js b/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.js index 0cd2a002c7..ac35826756 100644 --- a/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.js +++ b/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.js @@ -5,8 +5,15 @@ import { Icon, IconClose } from '../../base/icons'; import { translate } from '../../base/i18n'; import { getCurrentConference } from '../../base/conference/functions'; import { browser } from '../../base/lib-jitsi-meet'; -import { isMobileBrowser } from '../../base/environment/utils'; +import { + checkChromeExtensionsInstalled, + isMobileBrowser +} from '../../base/environment/utils'; import logger from '../logger'; +import { + createChromeExtensionBannerEvent, + sendAnalytics +} from '../../analytics'; declare var interfaceConfig: Object; @@ -22,21 +29,16 @@ const DONT_SHOW_AGAIN_CHECKED = 'hide_chrome_extension_banner'; */ type Props = { + /** + * Contains info about installed/to be installed chrome extension(s). + */ + bannerCfg: Object, + /** * Conference data, if any */ conference: Object, - /** - * The url of the chrome extension - */ - chromeExtensionUrl: string, - - /** - * An array containing info for identifying a chrome extension - */ - chromeExtensionsInfo: Array, - /** * Whether I am the current recorder. */ @@ -91,7 +93,6 @@ class ChromeExtensionBanner extends PureComponent { this._onClosePressed = this._onClosePressed.bind(this); this._onInstallExtensionClick = this._onInstallExtensionClick.bind(this); - this._checkExtensionsInstalled = this._checkExtensionsInstalled.bind(this); this._shouldNotRender = this._shouldNotRender.bind(this); this._onDontShowAgainChange = this._onDontShowAgainChange.bind(this); } @@ -107,15 +108,18 @@ class ChromeExtensionBanner extends PureComponent { return; } - if (this.props.chromeExtensionUrl && !prevProps.chromeExtensionUrl) { + const { bannerCfg } = this.props; + const prevBannerCfg = prevProps.bannerCfg; + + if (bannerCfg.url && !prevBannerCfg.url) { logger.info('Chrome extension URL found.'); } - if (this.props.chromeExtensionsInfo.length && !prevProps.chromeExtensionsInfo.length) { + if ((bannerCfg.chromeExtensionsInfo || []).length && !(prevBannerCfg.chromeExtensionsInfo || []).length) { logger.info('Chrome extension(s) info found.'); } - const hasExtensions = await this._checkExtensionsInstalled(); + const hasExtensions = await checkChromeExtensionsInstalled(this.props.bannerCfg); if ( hasExtensions @@ -147,6 +151,7 @@ class ChromeExtensionBanner extends PureComponent { * @returns {void} */ _onClosePressed() { + sendAnalytics(createChromeExtensionBannerEvent(false)); this.setState({ closePressed: true }); } @@ -158,36 +163,11 @@ class ChromeExtensionBanner extends PureComponent { * @returns {void} */ _onInstallExtensionClick() { - window.open(this.props.chromeExtensionUrl); + sendAnalytics(createChromeExtensionBannerEvent(true)); + window.open(this.props.bannerCfg.url); this.setState({ closePressed: true }); } - _checkExtensionsInstalled: () => Promise<*>; - - /** - * Checks whether the chrome extensions defined in the config file are installed or not. - * - * @returns {Promise[]} - */ - _checkExtensionsInstalled() { - const isExtensionInstalled = info => new Promise(resolve => { - const img = new Image(); - - img.src = `chrome-extension://${info.id}/${info.path}`; - img.onload = function() { - resolve(true); - }; - img.onerror = function() { - resolve(false); - }; - }); - const extensionInstalledFunction = info => isExtensionInstalled(info); - - return Promise.all( - this.props.chromeExtensionsInfo.map(info => extensionInstalledFunction(info)) - ); - } - _shouldNotRender: () => boolean; /** @@ -202,7 +182,7 @@ class ChromeExtensionBanner extends PureComponent { const dontShowAgain = localStorage.getItem(DONT_SHOW_AGAIN_CHECKED) === 'true'; - return !this.props.chromeExtensionUrl + return !this.props.bannerCfg.url || dontShowAgain || this.state.closePressed || !this.state.shouldShow @@ -290,11 +270,8 @@ class ChromeExtensionBanner extends PureComponent { * @returns {Object} */ const _mapStateToProps = state => { - const bannerCfg = state['features/base/config'].chromeExtensionBanner || {}; - return { - chromeExtensionUrl: bannerCfg.url, - chromeExtensionsInfo: bannerCfg.chromeExtensionsInfo || [], + bannerCfg: state['features/base/config'].chromeExtensionBanner || {}, conference: getCurrentConference(state), iAmRecorder: state['features/base/config'].iAmRecorder };