diff --git a/packages/yoroi-extension/app/App.js b/packages/yoroi-extension/app/App.js index e8ace513c7..1e9e2581a5 100644 --- a/packages/yoroi-extension/app/App.js +++ b/packages/yoroi-extension/app/App.js @@ -36,6 +36,7 @@ import { ThemeProvider } from '@mui/material/styles'; import { CssBaseline } from '@mui/material'; import { globalStyles } from './styles/globalStyles'; import Support from './components/widgets/Support'; +import { trackNavigation } from './api/analytics'; // https://github.com/yahoo/react-intl/wiki#loading-locale-data addLocaleData([ @@ -80,6 +81,9 @@ class App extends Component { this.mergedMessages = _mergedMessages; }); }); + this.props.history.listen(({ pathname }) => { + trackNavigation(pathname); + }); } state: State = { diff --git a/packages/yoroi-extension/app/api/analytics/index.js b/packages/yoroi-extension/app/api/analytics/index.js new file mode 100644 index 0000000000..73f063a9bd --- /dev/null +++ b/packages/yoroi-extension/app/api/analytics/index.js @@ -0,0 +1,171 @@ +// @flow +import cryptoRandomString from 'crypto-random-string'; +import querystring from 'querystring'; + +import LocalStorageApi, { + loadAnalyticsInstanceId, + saveAnalyticsInstanceId, +} from '../localStorage'; +import { environment } from '../../environment'; +import { TRACKED_ROUTES } from '../../routes-config'; +import type { StoresMap } from '../../stores'; +import { isTestnet as isTestnetFunc } from '../ada/lib/storage/database/prepackaged/networks'; + +const MATOMO_URL = 'https://analytics.emurgo-rnd.com/matomo.php'; +const SITE_ID = '4'; +let INSTANCE_ID; +let stores; + +export async function trackStartup(stores_: StoresMap): Promise { + stores = stores_; + + let event; + if (await (new LocalStorageApi()).getUserLocale() != null) { + INSTANCE_ID = await loadAnalyticsInstanceId(); + if (INSTANCE_ID) { + emitEvent(INSTANCE_ID, 'launch'); + return; + } + event = 'pre-existing-instance'; + } else { + event = 'new-instance'; + } + INSTANCE_ID = generateAnalyticsInstanceId(); + await saveAnalyticsInstanceId(INSTANCE_ID); + emitEvent(INSTANCE_ID, event); +} + +type NewWalletType = 'hardware' | 'created' | 'restored'; + +export function trackWalletCreation(newWalletType: NewWalletType): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'new-wallet/' + newWalletType); +} + +export function trackNavigation(path: string): void { + if (path.match(TRACKED_ROUTES)) { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'navigate' + path); + } +} + +export function trackSend(): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'new-transaction/send'); +} + +export function trackDelegation(): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'delegation'); +} + +export function trackWithdrawal(shouldDeregister: boolean): void { + if (INSTANCE_ID == null) { + return; + } + if (shouldDeregister) { + emitEvent(INSTANCE_ID, 'deregister'); + } else { + emitEvent(INSTANCE_ID, 'withdrawal'); + } +} + +export function trackCatalystRegistration(): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'new-transaction/catalyst'); +} + +export function trackSetLocale(locale: string): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'set-locale/' + locale); +} + +export function trackSetUnitOfAccount(unitOfAccount: string): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'unit-of-account/' + unitOfAccount); +} + +export function trackUpdateTheme(theme: string): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'update-theme/' + theme); +} + +export function trackUriPrompt(choice: 'skip' | 'allow'): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'uri-prompt/' + choice); +} + +export function trackBuySellDialog(): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'buy-sell-ada'); +} + +export function trackExportWallet(): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'export-wallet'); +} + +export function trackRemoveWallet(): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'remove-wallet'); +} + +export function trackResyncWallet(): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'resync-wallet'); +} + +function generateAnalyticsInstanceId(): string { + // Matomo requires 16 character hex string + return cryptoRandomString({ length: 16 }); +} + +function emitEvent(instanceId: string, event: string): void { + if (environment.isDev() || environment.isTest()) { + return; + } + + const isTestnet = stores.profile.selectedNetwork != null ? + isTestnetFunc(stores.profile.selectedNetwork) : + false; + + // https://developer.matomo.org/api-reference/tracking-api + const params = { + idsite: SITE_ID, + rec: '1', + action_name: (isTestnet ? 'testnet/' : '') + event, + url: `yoroi.extension/${isTestnet ? 'testnet/' : ''}${event}`, + _id: INSTANCE_ID, + rand: `${Date.now()}-${Math.random()}`, + apiv: '1' + }; + const url = `${MATOMO_URL}?${querystring.stringify(params)}`; + + fetch(url); +} diff --git a/packages/yoroi-extension/app/api/localStorage/index.js b/packages/yoroi-extension/app/api/localStorage/index.js index efdc33bab5..d183008186 100644 --- a/packages/yoroi-extension/app/api/localStorage/index.js +++ b/packages/yoroi-extension/app/api/localStorage/index.js @@ -32,6 +32,7 @@ const storageKeys = { TOGGLE_SIDEBAR: networkForLocalStorage + '-TOGGLE-SIDEBAR', WALLETS_NAVIGATION: networkForLocalStorage + '-WALLETS-NAVIGATION', SUBMITTED_TRANSACTIONS: 'submittedTransactions', + ANALYTICS_INSTANCE_ID: networkForLocalStorage + '-ANALYTICS', // ========== CONNECTOR ========== // ERGO_CONNECTOR_WHITELIST: 'connector_whitelist', }; @@ -360,3 +361,12 @@ export function loadSubmittedTransactions(): any { } return JSON.parse(dataStr); } + +export async function loadAnalyticsInstanceId(): Promise { + return getLocalItem(storageKeys.ANALYTICS_INSTANCE_ID); +} + +export async function saveAnalyticsInstanceId(id: string): Promise { + await setLocalItem(storageKeys.ANALYTICS_INSTANCE_ID, id); +} + diff --git a/packages/yoroi-extension/app/components/buySell/BuySellDialog.js b/packages/yoroi-extension/app/components/buySell/BuySellDialog.js index fe8a5749ff..a3497124d6 100644 --- a/packages/yoroi-extension/app/components/buySell/BuySellDialog.js +++ b/packages/yoroi-extension/app/components/buySell/BuySellDialog.js @@ -15,6 +15,7 @@ import { ReactComponent as VerifyIcon } from '../../assets/images/verify-icon.i import VerticalFlexContainer from '../layout/VerticalFlexContainer' import LoadingSpinner from '../widgets/LoadingSpinner' import globalMessages from '../../i18n/global-messages'; +import { trackBuySellDialog } from '../../api/analytics'; const messages = defineMessages({ dialogTitle: { @@ -77,6 +78,7 @@ export default class BuySellDialog extends Component { } ] this.setState({ walletList: wallets }) + trackBuySellDialog(); } createRows: ($npm$ReactIntl$IntlFormat, Array) => Node = (intl, wallets) => ( diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js index ec1691b59b..6fe88ad8ed 100644 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js +++ b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js @@ -20,6 +20,7 @@ import { } from '../../../../api/ada/lib/storage/models/ConceptualWallet'; import type { SendUsingLedgerParams } from '../../../../actions/ada/ledger-send-actions'; import type { SendUsingTrezorParams } from '../../../../actions/ada/trezor-send-actions'; +import { trackSend } from '../../../../api/analytics'; export type GeneratedData = typeof WalletSendPreviewStepContainer.prototype.generated; @@ -90,6 +91,7 @@ export default class WalletSendPreviewStepContainer extends Component { onSuccess: openTransactionSuccessDialog, }); } + trackSend() } render(): Node { diff --git a/packages/yoroi-extension/app/containers/profile/UriPromptPage.js b/packages/yoroi-extension/app/containers/profile/UriPromptPage.js index ee29484524..4af863dbed 100644 --- a/packages/yoroi-extension/app/containers/profile/UriPromptPage.js +++ b/packages/yoroi-extension/app/containers/profile/UriPromptPage.js @@ -21,6 +21,7 @@ import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; import type { ServerStatusErrorType } from '../../types/serverStatusErrorType'; import { PublicDeriver } from '../../api/ada/lib/storage/models/PublicDeriver/index'; import { isTestnet } from '../../api/ada/lib/storage/database/prepackaged/networks'; +import { trackUriPrompt } from '../../api/analytics'; type GeneratedData = typeof UriPromptPage.prototype.generated; @@ -39,10 +40,12 @@ export default class UriPromptPage extends Component { this.isAccepted = true; }); + trackUriPrompt('allow'); }; onSkip: void => void = () => { this.generated.actions.profile.acceptUriScheme.trigger() + trackUriPrompt('skip'); }; onBack: void => void = () => { diff --git a/packages/yoroi-extension/app/containers/settings/categories/BlockchainSettingsPage.js b/packages/yoroi-extension/app/containers/settings/categories/BlockchainSettingsPage.js index 5d1ae8d5e9..fc2cdeae2b 100644 --- a/packages/yoroi-extension/app/containers/settings/categories/BlockchainSettingsPage.js +++ b/packages/yoroi-extension/app/containers/settings/categories/BlockchainSettingsPage.js @@ -23,6 +23,7 @@ import { SelectedExplorer } from '../../../domain/SelectedExplorer'; import type { GetAllExplorersResponse, } from '../../../api/ada/lib/storage/bridge/explorers'; +import { trackUriPrompt } from '../../../api/analytics'; type GeneratedData = typeof BlockchainSettingsPage.prototype.generated; @@ -50,7 +51,12 @@ export default class BlockchainSettingsPage extends Component registerProtocols()} + registerUriScheme={ + () => { + registerProtocols(); + trackUriPrompt('allow'); + } + } isFirefox={environment.userAgentInfo.isFirefox()} /> ) diff --git a/packages/yoroi-extension/app/containers/settings/categories/GeneralSettingsPage.js b/packages/yoroi-extension/app/containers/settings/categories/GeneralSettingsPage.js index 5d2870cb2f..8f23f23343 100644 --- a/packages/yoroi-extension/app/containers/settings/categories/GeneralSettingsPage.js +++ b/packages/yoroi-extension/app/containers/settings/categories/GeneralSettingsPage.js @@ -18,6 +18,7 @@ import { ReactComponent as AdaCurrency } from '../../../assets/images/currencie import { unitOfAccountDisabledValue } from '../../../types/unitOfAccountType'; import type { UnitOfAccountSettingType } from '../../../types/unitOfAccountType'; import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; +import { trackSetUnitOfAccount, trackSetLocale } from '../../../api/analytics'; const currencyLabels = defineMessages({ USD: { @@ -84,6 +85,12 @@ export default class GeneralSettingsPage extends Component PossiblyAsync = ({ locale }) => { + this.generated.actions.profile.updateLocale.trigger({ locale }); + trackSetLocale(locale); }; render(): Node { @@ -120,7 +127,7 @@ export default class GeneralSettingsPage extends Component { quickAccess: walletsNavigation.quickAccess.filter(walletId => walletId !== selectedWalletId) } await this.generated.actions.profile.updateSortedWalletList.trigger(newWalletsNavigation); + trackRemoveWallet(); } this.props.publicDeriver && @@ -99,11 +101,14 @@ class RemoveWalletDialogContainer extends Component { onCancel={this.generated.actions.dialogs.closeActiveDialog.trigger} primaryButton={{ label: intl.formatMessage(globalMessages.remove), - onClick: () => - this.props.publicDeriver && - settingsActions.removeWallet.trigger({ - publicDeriver: this.props.publicDeriver, - }), + onClick: () => { + if (this.props.publicDeriver != null) { + settingsActions.removeWallet.trigger({ + publicDeriver: this.props.publicDeriver, + }); + trackRemoveWallet(); + } + } }} secondaryButton={{ onClick: this.generated.actions.dialogs.closeActiveDialog.trigger, diff --git a/packages/yoroi-extension/app/containers/settings/categories/ResyncWalletDialogContainer.js b/packages/yoroi-extension/app/containers/settings/categories/ResyncWalletDialogContainer.js index e41c482456..7a86838c4f 100644 --- a/packages/yoroi-extension/app/containers/settings/categories/ResyncWalletDialogContainer.js +++ b/packages/yoroi-extension/app/containers/settings/categories/ResyncWalletDialogContainer.js @@ -13,6 +13,7 @@ import type { InjectedOrGenerated } from '../../../types/injectedPropsType'; import DangerousActionDialog from '../../../components/widgets/DangerousActionDialog'; import LocalizableError from '../../../i18n/LocalizableError'; +import { trackResyncWallet } from '../../../api/analytics'; export type GeneratedData = typeof ResyncWalletDialogContainer.prototype.generated; @@ -65,6 +66,7 @@ export default class ResyncWalletDialogContainer extends Component { publicDeriver: this.props.publicDeriver, }); this.generated.actions.dialogs.closeActiveDialog.trigger(); + trackResyncWallet(); } }} onCancel={this.generated.actions.dialogs.closeActiveDialog.trigger} diff --git a/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js b/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js index 45a3395701..49f4d087e2 100644 --- a/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js +++ b/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js @@ -27,6 +27,7 @@ import type { SigningKeyCache } from '../../../stores/toplevel/WalletStore'; import LocalizableError from '../../../i18n/LocalizableError'; import type { RenameModelFunc } from '../../../api/common/index'; import type { IGetSigningKey } from '../../../api/ada/lib/storage/models/PublicDeriver/interfaces'; +import { trackExportWallet } from '../../../api/analytics'; type GeneratedData = typeof WalletSettingsPage.prototype.generated; @@ -108,9 +109,12 @@ export default class WalletSettingsPage extends Component actions.dialogs.open.trigger({ - dialog: ExportWalletDialogContainer, - })} + openDialog={() => { + actions.dialogs.open.trigger({ + dialog: ExportWalletDialogContainer, + }); + trackExportWallet(); + }} /> { render(): Node { const { intl } = this.context; - const { createWithdrawalTx } = this.generated.stores.substores.ada.delegationTransaction; + const { + createWithdrawalTx, + shouldDeregister, + } = this.generated.stores.substores.ada.delegationTransaction; if (this.generated.stores.profile.selectedNetwork == null) { throw new Error(`${nameof(WithdrawalTxDialogContainer)} no selected network`); @@ -51,7 +55,9 @@ export default class WithdrawalTxDialogContainer extends Component { label: intl.formatMessage(globalMessages.cancel), }} onSubmit={{ - trigger: () => {}, // nothing extra to do + trigger: () => { + trackWithdrawal(shouldDeregister); + }, label: intl.formatMessage(globalMessages.confirm), }} transactionRequest={createWithdrawalTx} @@ -97,6 +103,7 @@ export default class WithdrawalTxDialogContainer extends Component { error: ?LocalizableError, result: ?ISignRequest |}, + shouldDeregister: boolean, |}, |}, |}, @@ -128,6 +135,7 @@ export default class WithdrawalTxDialogContainer extends Component { result: stores.substores.ada.delegationTransaction.createWithdrawalTx.result, reset: stores.substores.ada.delegationTransaction.createWithdrawalTx.reset, }, + shouldDeregister: stores.substores.ada.delegationTransaction.shouldDeregister, }, }, }, diff --git a/packages/yoroi-extension/app/containers/transfer/YoroiTransferPage.stories.js b/packages/yoroi-extension/app/containers/transfer/YoroiTransferPage.stories.js index 66313bc1ff..f8682fccdf 100644 --- a/packages/yoroi-extension/app/containers/transfer/YoroiTransferPage.stories.js +++ b/packages/yoroi-extension/app/containers/transfer/YoroiTransferPage.stories.js @@ -559,6 +559,7 @@ export const WithdrawalTxPage = (): Node => { ), reset: action('createWithdrawalTx reset'), }, + shouldDeregister: false, }, }, }, diff --git a/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js b/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js index aa5e5935b6..4c6c33e8bc 100644 --- a/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js +++ b/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js @@ -49,6 +49,7 @@ import { withLayout } from '../../styles/context/layout'; import WalletSendPreviewStepContainer from '../../components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer'; import AddNFTDialog from '../../components/wallet/send/WalletSendFormSteps/AddNFTDialog'; import AddTokenDialog from '../../components/wallet/send/WalletSendFormSteps/AddTokenDialog'; +import { trackSend } from '../../api/analytics'; const messages = defineMessages({ txConfirmationLedgerNanoLine1: { @@ -397,11 +398,14 @@ class WalletSendPage extends Component { isSubmitting={ledgerSendStore.isActionProcessing} error={ledgerSendStore.error} onSubmit={ - () => ledgerSendAction.sendUsingLedgerWallet.trigger({ - params: { signRequest }, - publicDeriver, - onSuccess: this.openTransactionSuccessDialog, - }) + () => { + ledgerSendAction.sendUsingLedgerWallet.trigger({ + params: { signRequest }, + publicDeriver, + onSuccess: this.openTransactionSuccessDialog, + }); + trackSend(); + } } onCancel={ledgerSendAction.cancel.trigger} unitOfAccountSetting={this.generated.stores.profile.unitOfAccount} @@ -431,11 +435,14 @@ class WalletSendPage extends Component { isSubmitting={trezorSendStore.isActionProcessing} error={trezorSendStore.error} onSubmit={ - () => trezorSendAction.sendUsingTrezor.trigger({ - params: { signRequest }, - publicDeriver, - onSuccess: this.openTransactionSuccessDialog, - }) + () => { + trezorSendAction.sendUsingTrezor.trigger({ + params: { signRequest }, + publicDeriver, + onSuccess: this.openTransactionSuccessDialog, + }) + trackSend(); + } } onCancel={trezorSendAction.cancel.trigger} unitOfAccountSetting={this.generated.stores.profile.unitOfAccount} diff --git a/packages/yoroi-extension/app/containers/wallet/dialogs/WalletSendConfirmationDialogContainer.js b/packages/yoroi-extension/app/containers/wallet/dialogs/WalletSendConfirmationDialogContainer.js index bff0bf286b..e35eaa0534 100644 --- a/packages/yoroi-extension/app/containers/wallet/dialogs/WalletSendConfirmationDialogContainer.js +++ b/packages/yoroi-extension/app/containers/wallet/dialogs/WalletSendConfirmationDialogContainer.js @@ -13,6 +13,7 @@ import { addressToDisplayString } from '../../../api/ada/lib/storage/bridge/util import type { ISignRequest } from '../../../api/common/lib/transactions/ISignRequest'; import type { TokenInfoMap } from '../../../stores/toplevel/TokenInfoStore'; import { genLookupOrFail } from '../../../stores/stateless/tokenHelpers'; +import { trackSend } from '../../../api/analytics'; export type GeneratedData = typeof WalletSendConfirmationDialogContainer.prototype.generated; @@ -85,6 +86,7 @@ export default class WalletSendConfirmationDialogContainer extends Component { diff --git a/packages/yoroi-extension/app/containers/wallet/staking/StakingDashboardPage.stories.js b/packages/yoroi-extension/app/containers/wallet/staking/StakingDashboardPage.stories.js index 47d07bea8c..134b155f02 100644 --- a/packages/yoroi-extension/app/containers/wallet/staking/StakingDashboardPage.stories.js +++ b/packages/yoroi-extension/app/containers/wallet/staking/StakingDashboardPage.stories.js @@ -1014,6 +1014,7 @@ export const AdaWithdrawDialog = (): Node => { ), reset: action('createWithdrawalTx reset'), }, + shouldDeregister: false, }, }, }, diff --git a/packages/yoroi-extension/app/routes-config.js b/packages/yoroi-extension/app/routes-config.js index ff4536e54d..2cbcef7d3b 100644 --- a/packages/yoroi-extension/app/routes-config.js +++ b/packages/yoroi-extension/app/routes-config.js @@ -1,4 +1,27 @@ // @flow +// routes to by tracked by analytics +export const TRACKED_ROUTES: RegExp = new RegExp( + '^(' + + '(/my-wallets)|' + + '(/wallets/add)|' + + '(/wallets/transactions)|' + + '(/wallets/send)|' + + '(/wallets/assets)|' + + '(/wallets/receive/.+)|' + + '(/wallets/delegation-dashboard)|' + + '(/wallets/cardano-delegation)|' + + '(/wallets/voting)|' + + '(/settings/.+)|' + + '(/transfer(/.+)?)|' + + '(/send-from-uri)|' + + '(/notice-board)|' + + '(/staking)|' + + '(/assets/.*)|' + + '(/connector/connected-websites)|' + + '(/experimental/.*)' + + ')$' +); + export const ROUTES = { ROOT: '/', diff --git a/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js b/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js index 3341357905..e03d287da3 100644 --- a/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js +++ b/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js @@ -27,6 +27,7 @@ import { import { MultiToken } from '../../api/common/lib/MultiToken'; import type { ActionsMap } from '../../actions/index'; import type { StoresMap } from '../index'; +import { trackDelegation } from '../../api/analytics'; export default class AdaDelegationTransactionStore extends Store { @@ -214,9 +215,7 @@ export default class AdaDelegationTransactionStore extends Store this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), }); - return; - } - if (isTrezorTWallet(request.publicDeriver.getParent())) { + } else if (isTrezorTWallet(request.publicDeriver.getParent())) { await this.stores.substores.ada.wallets.adaSendAndRefresh({ broadcastRequest: { trezor: { @@ -226,23 +225,23 @@ export default class AdaDelegationTransactionStore extends Store this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), }); - return; - } - - // normal password-based wallet - if (request.password == null) { - throw new Error(`${nameof(this._signTransaction)} missing password for non-hardware signing`); - } - await this.stores.substores.ada.wallets.adaSendAndRefresh({ - broadcastRequest: { - normal: { - publicDeriver: request.publicDeriver, - password: request.password, - signRequest: result.signTxRequest, + } else { + // normal password-based wallet + if (request.password == null) { + throw new Error(`${nameof(this._signTransaction)} missing password for non-hardware signing`); + } + await this.stores.substores.ada.wallets.adaSendAndRefresh({ + broadcastRequest: { + normal: { + publicDeriver: request.publicDeriver, + password: request.password, + signRequest: result.signTxRequest, + }, }, - }, - refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), - }); + refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), + }); + } + trackDelegation(); } _complete: void => void = () => { diff --git a/packages/yoroi-extension/app/stores/ada/AdaWalletRestoreStore.js b/packages/yoroi-extension/app/stores/ada/AdaWalletRestoreStore.js index 017c230947..7737985e54 100644 --- a/packages/yoroi-extension/app/stores/ada/AdaWalletRestoreStore.js +++ b/packages/yoroi-extension/app/stores/ada/AdaWalletRestoreStore.js @@ -14,6 +14,7 @@ import type { } from '../../api/ada/lib/storage/models/PublicDeriver/interfaces'; import type { ActionsMap } from '../../actions/index'; import type { StoresMap } from '../index'; +import { trackWalletCreation } from '../../api/analytics'; export default class AdaWalletRestoreStore extends Store { @@ -129,6 +130,7 @@ export default class AdaWalletRestoreStore extends Store }); return wallet; }).promise; + trackWalletCreation('restored'); }; teardown(): void { diff --git a/packages/yoroi-extension/app/stores/ada/AdaWalletsStore.js b/packages/yoroi-extension/app/stores/ada/AdaWalletsStore.js index 821f8d4909..4dbc040ba4 100644 --- a/packages/yoroi-extension/app/stores/ada/AdaWalletsStore.js +++ b/packages/yoroi-extension/app/stores/ada/AdaWalletsStore.js @@ -12,6 +12,7 @@ import { buildCheckAndCall } from '../lib/check'; import { getApiForNetwork, ApiOptions } from '../../api/common/utils'; import type { ActionsMap } from '../../actions/index'; import type { StoresMap } from '../index'; +import { trackWalletCreation } from '../../api/analytics'; export default class AdaWalletsStore extends Store { @@ -129,5 +130,7 @@ export default class AdaWalletsStore extends Store { }); return wallet; }).promise; + + trackWalletCreation('created'); }; } diff --git a/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js b/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js index 96cdde4094..de52bce9f1 100644 --- a/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js +++ b/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js @@ -61,6 +61,7 @@ import type { StoresMap } from '../index'; import type { GetExtendedPublicKeyResponse, } from '@cardano-foundation/ledgerjs-hw-app-cardano'; +import { trackWalletCreation } from '../../api/analytics'; export default class LedgerConnectStore extends Store @@ -365,6 +366,7 @@ export default class LedgerConnectStore await this._saveHW( walletName, ); + trackWalletCreation('hardware'); }; /** creates new wallet and loads it */ diff --git a/packages/yoroi-extension/app/stores/ada/TrezorConnectStore.js b/packages/yoroi-extension/app/stores/ada/TrezorConnectStore.js index 23abfad637..1b6bb28333 100644 --- a/packages/yoroi-extension/app/stores/ada/TrezorConnectStore.js +++ b/packages/yoroi-extension/app/stores/ada/TrezorConnectStore.js @@ -51,6 +51,7 @@ import type { } from '../../actions/common/wallet-restore-actions'; import type { ActionsMap } from '../../actions/index'; import type { StoresMap } from '../index'; +import { trackWalletCreation } from '../../api/analytics'; type TrezorConnectionResponse = {| trezorResp: Success | Unsuccessful, @@ -333,6 +334,7 @@ export default class TrezorConnectStore await this._saveHW( walletName, ); + trackWalletCreation('hardware'); }; /** creates new wallet and loads it */ diff --git a/packages/yoroi-extension/app/stores/ada/VotingStore.js b/packages/yoroi-extension/app/stores/ada/VotingStore.js index 4a7dd6bccd..36a4f2282a 100644 --- a/packages/yoroi-extension/app/stores/ada/VotingStore.js +++ b/packages/yoroi-extension/app/stores/ada/VotingStore.js @@ -45,6 +45,7 @@ import { generateRegistration } from '../../api/ada/lib/cardanoCrypto/catalyst'; import { derivePublicByAddressing } from '../../api/ada/lib/cardanoCrypto/utils' import type { ConceptualWallet } from '../../api/ada/lib/storage/models/ConceptualWallet' import type { CatalystRoundInfoResponse } from '../../api/ada/lib/state-fetch/types' +import { trackCatalystRegistration } from '../../api/analytics'; export const ProgressStep = Object.freeze({ GENERATE: 0, @@ -392,9 +393,7 @@ export default class VotingStore extends Store { }, refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), }); - return; - } - if (isTrezorTWallet(request.publicDeriver.getParent())) { + } else if (isTrezorTWallet(request.publicDeriver.getParent())) { await this.stores.substores.ada.wallets.adaSendAndRefresh({ broadcastRequest: { trezor: { @@ -404,23 +403,23 @@ export default class VotingStore extends Store { }, refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), }); - return; - } - - // normal password-based wallet - if (request.password == null) { - throw new Error(`${nameof(this._signTransaction)} missing password for non-hardware signing`); - } - await this.stores.substores.ada.wallets.adaSendAndRefresh({ - broadcastRequest: { - normal: { - publicDeriver: request.publicDeriver, - password: request.password, - signRequest: result, + } else { + // normal password-based wallet + if (request.password == null) { + throw new Error(`${nameof(this._signTransaction)} missing password for non-hardware signing`); + } + await this.stores.substores.ada.wallets.adaSendAndRefresh({ + broadcastRequest: { + normal: { + publicDeriver: request.publicDeriver, + password: request.password, + signRequest: result, + }, }, - }, - refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), - }); + refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), + }); + } + trackCatalystRegistration(); }; @action _generateCatalystKey: void => Promise = async () => { diff --git a/packages/yoroi-extension/app/stores/base/BaseProfileStore.js b/packages/yoroi-extension/app/stores/base/BaseProfileStore.js index 87370108ff..7c31d6f988 100644 --- a/packages/yoroi-extension/app/stores/base/BaseProfileStore.js +++ b/packages/yoroi-extension/app/stores/base/BaseProfileStore.js @@ -15,6 +15,10 @@ import type { UnitOfAccountSettingType } from '../../types/unitOfAccountType'; import { SUPPORTED_CURRENCIES } from '../../config/unitOfAccount'; import type { ComplexityLevelType } from '../../types/complexityLevelType'; import BaseProfileActions from '../../actions/base/base-profile-actions'; +import { + trackSetLocale, + trackUpdateTheme +} from '../../api/analytics'; interface CoinPriceStore { refreshCurrentUnit: Request Promise> @@ -221,13 +225,15 @@ export default class BaseProfileStore _acceptLocale: void => Promise = async () => { // commit in-memory language to storage - await this.setProfileLocaleRequest.execute( - this.inMemoryLanguage != null ? this.inMemoryLanguage : BaseProfileStore.getDefaultLocale() - ); + const locale = this.inMemoryLanguage != null ? + this.inMemoryLanguage : + BaseProfileStore.getDefaultLocale(); + await this.setProfileLocaleRequest.execute(locale); await this.getProfileLocaleRequest.execute(); // eagerly cache runInAction(() => { this.inMemoryLanguage = null; }); + trackSetLocale(locale); }; _updateMomentJsLocaleAfterLocaleChange: void => void = () => { @@ -312,6 +318,7 @@ export default class BaseProfileStore await this.getCustomThemeRequest.execute(); // eagerly cache await this.setThemeRequest.execute(theme); await this.getThemeRequest.execute(); // eagerly cache + trackUpdateTheme(theme); }; diff --git a/packages/yoroi-extension/chrome/constants.js b/packages/yoroi-extension/chrome/constants.js index b4b57f36a5..49f80c2b52 100644 --- a/packages/yoroi-extension/chrome/constants.js +++ b/packages/yoroi-extension/chrome/constants.js @@ -54,6 +54,9 @@ export function genCSP(request: {| connectSrc.push('https://*.zdassets.com/') connectSrc.push('https://emurgohelpdesk.zendesk.com/') + // Analytics + connectSrc.push('https://analytics.emurgo-rnd.com/'); + // wasm-eval is needed to compile WebAssembly in the browser // note: wasm-eval is not standardized but empirically works in Firefox & Chrome https://github.com/w3c/webappsec-csp/pull/293 const evalSrc = "'wasm-eval'"; diff --git a/packages/yoroi-extension/chrome/extension/index.js b/packages/yoroi-extension/chrome/extension/index.js index 5d401703c3..02aa079632 100644 --- a/packages/yoroi-extension/chrome/extension/index.js +++ b/packages/yoroi-extension/chrome/extension/index.js @@ -15,6 +15,7 @@ import { addCloseListener, TabIdKeys } from '../../app/utils/tabManager'; import { Logger } from '../../app/utils/logging'; import { LazyLoadPromises } from '../../app/Routes'; import environment from '../../app/environment'; +import { trackStartup } from '../../app/api/analytics'; // run MobX in strict mode configure({ enforceActions: 'always' }); @@ -30,6 +31,7 @@ const initializeYoroi: void => Promise = async () => { const hashHistory = createHashHistory(); const history = syncHistoryWithStore(hashHistory, router); const stores = createStores(api, actions, router); + await trackStartup(stores); Logger.debug(`[yoroi] stores created`);