From 278373ed9b15aa2d198475b066c2a9a38d43ca5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 27 Jun 2018 11:32:20 +0200 Subject: [PATCH] Bootstrap analytics - Two components: and . we should try to ALWAYS use them. only case you might not use them is for imperative events (click, etc..) - also: introduce a "Use system locale" for the language because analytics need to diffferenciate if lang was set by user of fallbacked. it's also better when we'll introduce new lang to users to directly auto switch to their system's - started to track some pages and events. there will be more required and we'll have to add some in the next days --- scripts/download-analytics.sh | 10 ++ src/analytics/Track.js | 29 +++++ src/analytics/TrackPage.js | 14 +++ src/analytics/inject-in-window.js | 18 ++++ src/analytics/segment.js | 100 ++++++++++++++++++ src/components/AccountPage/index.js | 8 ++ src/components/DashboardPage/index.js | 8 ++ src/components/ExchangePage/ExchangeCard.js | 51 +++++---- src/components/ExchangePage/index.js | 44 ++++---- src/components/OperationsList/index.js | 14 +++ src/components/SettingsPage/sections/About.js | 2 + .../SettingsPage/sections/Currencies.js | 2 + .../SettingsPage/sections/Display.js | 38 ++++--- .../SettingsPage/sections/Profile.js | 2 + src/components/modals/AddAccounts/index.js | 2 + .../steps/01-step-choose-currency.js | 2 + .../steps/02-step-connect-device.js | 2 + .../AddAccounts/steps/03-step-import.js | 2 + .../AddAccounts/steps/04-step-finish.js | 2 + .../modals/Receive/01-step-account.js | 2 + .../modals/Receive/02-step-connect-device.js | 16 +++ .../modals/Receive/03-step-confirm-address.js | 2 + .../modals/Receive/04-step-receive-funds.js | 2 + src/components/modals/Receive/index.js | 4 +- src/components/modals/Send/01-step-amount.js | 3 + .../modals/Send/02-step-connect-device.js | 16 +++ .../modals/Send/03-step-verification.js | 2 + .../modals/Send/04-step-confirmation.js | 2 + src/components/modals/Send/index.js | 4 +- src/components/modals/StepConnectDevice.js | 1 + src/config/constants.js | 1 + src/helpers/staticPath.js | 8 +- src/helpers/systemLocale.js | 11 ++ src/middlewares/analytics.js | 18 ++++ src/reducers/settings.js | 47 ++++---- src/renderer/createStore.js | 1 + src/renderer/init.js | 4 +- static/analytics.min.js | 11 ++ static/i18n/en/language.yml | 1 + 39 files changed, 420 insertions(+), 86 deletions(-) create mode 100755 scripts/download-analytics.sh create mode 100644 src/analytics/Track.js create mode 100644 src/analytics/TrackPage.js create mode 100644 src/analytics/inject-in-window.js create mode 100644 src/analytics/segment.js create mode 100644 src/components/modals/Receive/02-step-connect-device.js create mode 100644 src/components/modals/Send/02-step-connect-device.js create mode 100644 src/helpers/systemLocale.js create mode 100644 src/middlewares/analytics.js create mode 100644 static/analytics.min.js diff --git a/scripts/download-analytics.sh b/scripts/download-analytics.sh new file mode 100755 index 00000000..daf8a3b1 --- /dev/null +++ b/scripts/download-analytics.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +if [ -z $ANALYTICS_KEY ]; then + echo 'ANALYTICS_KEY must be set' + exit 1 +fi + +cd `dirname $0`/.. + +wget https://cdn.segment.com/analytics.js/v1/$ANALYTICS_KEY/analytics.min.js -O static/analytics.min.js diff --git a/src/analytics/Track.js b/src/analytics/Track.js new file mode 100644 index 00000000..9c5d82e2 --- /dev/null +++ b/src/analytics/Track.js @@ -0,0 +1,29 @@ +import { PureComponent } from 'react' +import { track } from './segment' + +class Track extends PureComponent<{ + onMount?: boolean, + onUnmount?: boolean, + onUpdate?: boolean, + event: string, + properties?: Object, +}> { + componentDidMount() { + if (this.props.onMount) this.track() + } + componentDidUpdate() { + if (this.props.onUpdate) this.track() + } + componentWillUnmount() { + if (this.props.onUnmount) this.track() + } + track = () => { + const { event, properties } = this.props + track(event, properties) + } + render() { + return null + } +} + +export default Track diff --git a/src/analytics/TrackPage.js b/src/analytics/TrackPage.js new file mode 100644 index 00000000..2e18e868 --- /dev/null +++ b/src/analytics/TrackPage.js @@ -0,0 +1,14 @@ +import { PureComponent } from 'react' +import { page } from './segment' + +class TrackPage extends PureComponent<{ category: string, name?: string, properties?: Object }> { + componentDidMount() { + const { category, name, properties } = this.props + page(category, name, properties) + } + render() { + return null + } +} + +export default TrackPage diff --git a/src/analytics/inject-in-window.js b/src/analytics/inject-in-window.js new file mode 100644 index 00000000..859c2981 --- /dev/null +++ b/src/analytics/inject-in-window.js @@ -0,0 +1,18 @@ +/* eslint-disable */ +import { getPath } from 'helpers/staticPath' + +// prettier-ignore +!function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t { + if (loaded) return + loaded = true + var n = document.createElement('script') + n.type = 'text/javascript' + n.async = !0 + n.src = getPath('analytics.min.js') + var a = document.getElementsByTagName('script')[0] + a.parentNode.insertBefore(n, a) +} diff --git a/src/analytics/segment.js b/src/analytics/segment.js new file mode 100644 index 00000000..d239db10 --- /dev/null +++ b/src/analytics/segment.js @@ -0,0 +1,100 @@ +// @flow + +import uuid from 'uuid/v4' +import logger from 'logger' +import invariant from 'invariant' +import user from 'helpers/user' +import { DEBUG_ANALYTICS } from 'config/constants' +import { langAndRegionSelector } from 'reducers/settings' +import { getSystemLocale } from 'helpers/systemLocale' +import { load } from './inject-in-window' + +invariant(typeof window !== 'undefined', 'analytics/segment must be called on renderer thread') + +const sessionId = uuid() + +const getContext = store => { + const state = store.getState() + const { language, region } = langAndRegionSelector(state) + const systemLocale = getSystemLocale() + return { + ip: '0.0.0.0', + appVersion: __APP_VERSION__, + language, + region, + environment: __DEV__ ? 'development' : 'production', + systemLanguage: systemLocale.language, + systemRegion: systemLocale.region, + sessionId, + } +} + +let storeInstance // is the redux store. it's also used as a flag to know if analytics is on or off. + +export const start = (store: *) => { + storeInstance = store + const { analytics } = window + if (typeof analytics === 'undefined') { + logger.error('analytics is not available') + return + } + const { id } = user() + load() + analytics.identify( + id, + {}, + { + context: getContext(store), + }, + ) + if (DEBUG_ANALYTICS) { + logger.log(`analytics: start() with user id ${id}`) + } +} + +export const stop = () => { + storeInstance = null + const { analytics } = window + if (typeof analytics === 'undefined') { + logger.error('analytics is not available') + return + } + analytics.reset() + if (DEBUG_ANALYTICS) { + logger.log(`analytics: stop()`) + } +} + +export const track = (event: string, properties: ?Object) => { + if (!storeInstance) { + return + } + const { analytics } = window + if (typeof analytics === 'undefined') { + logger.error('analytics is not available') + return + } + analytics.track(event, properties, { + context: getContext(storeInstance), + }) + if (DEBUG_ANALYTICS) { + logger.log(`analytics: track(${event},`, properties) + } +} + +export const page = (category: string, name: ?string, properties: ?Object) => { + if (!storeInstance) { + return + } + const { analytics } = window + if (typeof analytics === 'undefined') { + logger.error('analytics is not available') + return + } + analytics.page(category, name, properties, { + context: getContext(storeInstance), + }) + if (DEBUG_ANALYTICS) { + logger.log(`analytics: page(${category}, ${name || ''},`, properties) + } +} diff --git a/src/components/AccountPage/index.js b/src/components/AccountPage/index.js index 29797f2c..2da8032f 100644 --- a/src/components/AccountPage/index.js +++ b/src/components/AccountPage/index.js @@ -9,6 +9,7 @@ import styled from 'styled-components' import type { Currency, Account } from '@ledgerhq/live-common/lib/types' import SyncOneAccountOnMount from 'components/SyncOneAccountOnMount' import Tooltip from 'components/base/Tooltip' +import TrackPage from 'analytics/TrackPage' import { MODAL_SEND, MODAL_RECEIVE, MODAL_SETTINGS_ACCOUNT } from 'config/constants' @@ -106,6 +107,13 @@ class AccountPage extends PureComponent { return ( // Force re-render account page, for avoid animation + diff --git a/src/components/DashboardPage/index.js b/src/components/DashboardPage/index.js index 9ffcd9a4..2582a017 100644 --- a/src/components/DashboardPage/index.js +++ b/src/components/DashboardPage/index.js @@ -1,6 +1,7 @@ // @flow import React, { PureComponent, Fragment } from 'react' +import uniq from 'lodash/uniq' import { compose } from 'redux' import { translate } from 'react-i18next' import { connect } from 'react-redux' @@ -26,6 +27,7 @@ import type { TimeRange } from 'reducers/settings' import { reorderAccounts } from 'actions/accounts' import { saveSettings } from 'actions/settings' +import TrackPage from 'analytics/TrackPage' import UpdateNotifier from 'components/UpdateNotifier' import BalanceInfos from 'components/BalanceSummary/BalanceInfos' import BalanceSummary from 'components/BalanceSummary' @@ -92,12 +94,18 @@ class DashboardPage extends PureComponent { const timeFrame = this.handleGreeting() const imagePath = i('empty-account-tile.svg') const totalAccounts = accounts.length + const totalCurrencies = uniq(accounts.map(a => a.currency.id)).length + const totalOperations = accounts.reduce((sum, a) => sum + a.operations.length, 0) const displayOperationsHelper = (account: Account) => account.operations.length > 0 const displayOperations = accounts.some(displayOperationsHelper) return ( + {totalAccounts > 0 ? ( diff --git a/src/components/ExchangePage/ExchangeCard.js b/src/components/ExchangePage/ExchangeCard.js index 7e6a00b8..0ae25b9d 100644 --- a/src/components/ExchangePage/ExchangeCard.js +++ b/src/components/ExchangePage/ExchangeCard.js @@ -1,7 +1,8 @@ // @flow -import React from 'react' +import React, { PureComponent } from 'react' import { shell } from 'electron' +import { track } from 'analytics/segment' import type { T } from 'types/common' @@ -10,31 +11,35 @@ import Box, { Card } from 'components/base/Box' import { FakeLink } from 'components/base/Link' type CardType = { + id: string, logo: any, - desc: string, url: string, } -export default function ExchangeCard({ t, card }: { t: T, card: CardType }) { - const { logo, desc } = card - return ( - shell.openExternal(card.url)} - > - - {logo} - - - {desc} - - {t('app:exchange.visitWebsite')} - +export default class ExchangeCard extends PureComponent<{ t: T, card: CardType }> { + onClick = () => { + const { card } = this.props + shell.openExternal(card.url) + track('VisitExchange', { id: card.id, url: card.url }) + } + render() { + const { + card: { logo, id }, + t, + } = this.props + return ( + + + {logo} - - - ) + + {t(`app:exchange.${id}`)} + + {t('app:exchange.visitWebsite')} + + + + + ) + } } diff --git a/src/components/ExchangePage/index.js b/src/components/ExchangePage/index.js index f5c7f129..d4e5a799 100644 --- a/src/components/ExchangePage/index.js +++ b/src/components/ExchangePage/index.js @@ -5,6 +5,7 @@ import { translate } from 'react-i18next' import type { T } from 'types/common' +import TrackPage from 'analytics/TrackPage' import Box from 'components/base/Box' import ExchangeCard from './ExchangeCard' @@ -16,32 +17,33 @@ type Props = { t: T, } +const cards = [ + { + key: 'coinhouse', + id: 'coinhouse', + url: 'https://www.coinhouse.com/r/157530', + logo: , + }, + { + key: 'changelly', + id: 'coinhouse', + url: 'https://changelly.com/?ref_id=aac789605a01', + logo: , + }, + { + key: 'coinmama', + id: 'coinhouse', + url: 'http://go.coinmama.com/visit/?bta=51801&nci=5343', + logo: , + }, +] + class ExchangePage extends PureComponent { render() { const { t } = this.props - const cards = [ - { - key: 'coinhouse', - url: 'https://www.coinhouse.com/r/157530', - logo: , - desc: t('app:exchange.coinhouse'), - }, - { - key: 'changelly', - url: 'https://changelly.com/?ref_id=aac789605a01', - logo: , - desc: t('app:exchange.changelly'), - }, - { - key: 'coinmama', - url: 'http://go.coinmama.com/visit/?bta=51801&nci=5343', - logo: , - desc: t('app:exchange.coinmama'), - }, - ] - return ( + {t('app:exchange.title')} diff --git a/src/components/OperationsList/index.js b/src/components/OperationsList/index.js index 6fdcece6..6a9d5bc4 100644 --- a/src/components/OperationsList/index.js +++ b/src/components/OperationsList/index.js @@ -25,6 +25,7 @@ import IconAngleDown from 'icons/AngleDown' import Box, { Card } from 'components/base/Box' import Text from 'components/base/Text' import Defer from 'components/base/Defer' +import Track from 'analytics/Track' import SectionTitle from './SectionTitle' import OperationC from './Operation' @@ -129,6 +130,19 @@ export class OperationsList extends PureComponent { ))} + {groupedOperations.completed ? ( + sum + s.data.length, + 0, + ), + }} + /> + ) : null} {!groupedOperations.completed ? ( {t('app:common.showMore')} diff --git a/src/components/SettingsPage/sections/About.js b/src/components/SettingsPage/sections/About.js index c2e2bf50..24ef9871 100644 --- a/src/components/SettingsPage/sections/About.js +++ b/src/components/SettingsPage/sections/About.js @@ -14,6 +14,7 @@ import { Tabbable } from 'components/base/Box' import { openModal } from 'reducers/modals' import { MODAL_RELEASES_NOTES } from 'config/constants' +import TrackPage from 'analytics/TrackPage' import { SettingsSection as Section, @@ -61,6 +62,7 @@ class SectionAbout extends PureComponent { return (
+
} title={t('app:settings.tabs.about')} diff --git a/src/components/SettingsPage/sections/Currencies.js b/src/components/SettingsPage/sections/Currencies.js index bef1773d..30055332 100644 --- a/src/components/SettingsPage/sections/Currencies.js +++ b/src/components/SettingsPage/sections/Currencies.js @@ -18,6 +18,7 @@ import type { SettingsState } from 'reducers/settings' import { currenciesSelector } from 'reducers/accounts' import { currencySettingsDefaults } from 'helpers/SettingsDefaults' +import TrackPage from 'analytics/TrackPage' import SelectCurrency from 'components/SelectCurrency' import StepperNumber from 'components/base/StepperNumber' import ExchangeSelect from 'components/SelectExchange' @@ -94,6 +95,7 @@ class TabCurrencies extends PureComponent { const defaults = currencySettingsDefaults(currency) return (
+
} title={t('app:settings.tabs.currencies')} diff --git a/src/components/SettingsPage/sections/Display.js b/src/components/SettingsPage/sections/Display.js index 4c17ba93..cc25139a 100644 --- a/src/components/SettingsPage/sections/Display.js +++ b/src/components/SettingsPage/sections/Display.js @@ -13,6 +13,7 @@ import { import type { SettingsState as Settings } from 'reducers/settings' import type { T } from 'types/common' +import TrackPage from 'analytics/TrackPage' import Box from 'components/base/Box' import SelectExchange from 'components/SelectExchange' import Select from 'components/base/Select' @@ -52,9 +53,9 @@ type Props = { type State = { cachedMarketIndicator: string, - cachedLanguageKey: string, + cachedLanguageKey: ?string, cachedCounterValue: ?Object, - cachedRegion: string, + cachedRegion: ?string, } class TabProfile extends PureComponent { @@ -131,9 +132,12 @@ class TabProfile extends PureComponent { const counterValueCurrency = counterValueCurrencyLocalSelector(settings) const counterValueExchange = counterValueExchangeLocalSelector(settings) - const languages = languageKeys.map(key => ({ value: key, label: t(`language:${key}`) })) - const currentLanguage = languages.find(l => l.value === cachedLanguageKey) + const languages = [{ value: null, label: t(`language:system`) }].concat( + languageKeys.map(key => ({ value: key, label: t(`language:${key}`) })), + ) const regionsFiltered = regions.filter(({ language }) => cachedLanguageKey === language) + + const currentLanguage = languages.find(l => l.value === cachedLanguageKey) const currentRegion = regionsFiltered.find(({ region }) => cachedRegion === region) || regionsFiltered[0] @@ -143,6 +147,7 @@ class TabProfile extends PureComponent { return (
+
} title={t('app:settings.tabs.display')} @@ -187,16 +192,21 @@ class TabProfile extends PureComponent { options={languages} /> - - item && item.name} + value={currentRegion} + options={regionsFiltered} + /> + + )} { const isPasswordEnabled = settings.password.isEnabled === true return (
+
} title={t('app:settings.tabs.profile')} diff --git a/src/components/modals/AddAccounts/index.js b/src/components/modals/AddAccounts/index.js index 3d56f246..31aed232 100644 --- a/src/components/modals/AddAccounts/index.js +++ b/src/components/modals/AddAccounts/index.js @@ -6,6 +6,7 @@ import { connect } from 'react-redux' import { translate } from 'react-i18next' import { createStructuredSelector } from 'reselect' +import Track from 'analytics/Track' import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority' import type { Currency, Account } from '@ledgerhq/live-common/lib/types' @@ -227,6 +228,7 @@ class AddAccounts extends PureComponent { steps={this.STEPS} {...addtionnalProps} > + )} diff --git a/src/components/modals/AddAccounts/steps/01-step-choose-currency.js b/src/components/modals/AddAccounts/steps/01-step-choose-currency.js index 86a1e2e0..214168f8 100644 --- a/src/components/modals/AddAccounts/steps/01-step-choose-currency.js +++ b/src/components/modals/AddAccounts/steps/01-step-choose-currency.js @@ -2,6 +2,7 @@ import React, { Fragment } from 'react' +import TrackPage from 'analytics/TrackPage' import SelectCurrency from 'components/SelectCurrency' import Button from 'components/base/Button' import CurrencyBadge from 'components/base/CurrencyBadge' @@ -15,6 +16,7 @@ function StepChooseCurrency({ currency, setCurrency }: StepProps) { export function StepChooseCurrencyFooter({ transitionTo, currency, t }: StepProps) { return ( + {currency && }