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 && }