Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support analytics #2916

Merged
merged 12 commits into from
Aug 8, 2022
4 changes: 4 additions & 0 deletions packages/yoroi-extension/app/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -80,6 +81,9 @@ class App extends Component<Props, State> {
this.mergedMessages = _mergedMessages;
});
});
this.props.history.listen(({ pathname }) => {
trackNavigation(pathname);
});
}

state: State = {
Expand Down
171 changes: 171 additions & 0 deletions packages/yoroi-extension/app/api/analytics/index.js
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
}
10 changes: 10 additions & 0 deletions packages/yoroi-extension/app/api/localStorage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
Expand Down Expand Up @@ -360,3 +361,12 @@ export function loadSubmittedTransactions(): any {
}
return JSON.parse(dataStr);
}

export async function loadAnalyticsInstanceId(): Promise<?string> {
return getLocalItem(storageKeys.ANALYTICS_INSTANCE_ID);
}

export async function saveAnalyticsInstanceId(id: string): Promise<void> {
await setLocalItem(storageKeys.ANALYTICS_INSTANCE_ID, id);
}

Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -77,6 +78,7 @@ export default class BuySellDialog extends Component<Props, State> {
}
]
this.setState({ walletList: wallets })
trackBuySellDialog();
}

createRows: ($npm$ReactIntl$IntlFormat, Array<WalletInfo>) => Node = (intl, wallets) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -90,6 +91,7 @@ export default class WalletSendPreviewStepContainer extends Component<Props> {
onSuccess: openTransactionSuccessDialog,
});
}
trackSend()
}

render(): Node {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -39,10 +40,12 @@ export default class UriPromptPage extends Component<InjectedOrGenerated<Generat
runInAction(() => {
this.isAccepted = true;
});
trackUriPrompt('allow');
};

onSkip: void => void = () => {
this.generated.actions.profile.acceptUriScheme.trigger()
trackUriPrompt('skip');
};

onBack: void => void = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -50,7 +51,12 @@ export default class BlockchainSettingsPage extends Component<InjectedOrGenerate
)
? (
<UriSettingsBlock
registerUriScheme={() => registerProtocols()}
registerUriScheme={
() => {
registerProtocols();
trackUriPrompt('allow');
}
}
isFirefox={environment.userAgentInfo.isFirefox()}
/>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -84,6 +85,12 @@ export default class GeneralSettingsPage extends Component<InjectedOrGenerated<G
? unitOfAccountDisabledValue
: { enabled: true, currency: value };
await this.generated.actions.profile.updateUnitOfAccount.trigger(unitOfAccount);
trackSetUnitOfAccount(value);
};

onSelectLanguage: {| locale: string |} => PossiblyAsync<void> = ({ locale }) => {
this.generated.actions.profile.updateLocale.trigger({ locale });
trackSetLocale(locale);
};

render(): Node {
Expand Down Expand Up @@ -120,7 +127,7 @@ export default class GeneralSettingsPage extends Component<InjectedOrGenerated<G
return (
<>
<GeneralSettings
onSelectLanguage={this.generated.actions.profile.updateLocale.trigger}
onSelectLanguage={this.onSelectLanguage}
isSubmitting={isSubmittingLocale}
languages={profileStore.LANGUAGE_OPTIONS}
currentLocale={profileStore.currentLocale}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { withLayout } from '../../../styles/context/layout';
import type { LayoutComponentMap } from '../../../styles/context/layout';
import { getWalletType } from '../../../stores/toplevel/WalletSettingsStore';
import type { WalletsNavigation } from '../../../api/localStorage'
import { trackRemoveWallet } from '../../../api/analytics';

export type GeneratedData = typeof RemoveWalletDialogContainer.prototype.generated;

Expand Down Expand Up @@ -75,6 +76,7 @@ class RemoveWalletDialogContainer extends Component<AllProps> {
quickAccess: walletsNavigation.quickAccess.filter(walletId => walletId !== selectedWalletId)
}
await this.generated.actions.profile.updateSortedWalletList.trigger(newWalletsNavigation);
trackRemoveWallet();
}

this.props.publicDeriver &&
Expand All @@ -99,11 +101,14 @@ class RemoveWalletDialogContainer extends Component<AllProps> {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -65,6 +66,7 @@ export default class ResyncWalletDialogContainer extends Component<Props> {
publicDeriver: this.props.publicDeriver,
});
this.generated.actions.dialogs.closeActiveDialog.trigger();
trackResyncWallet();
}
}}
onCancel={this.generated.actions.dialogs.closeActiveDialog.trigger}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -108,9 +109,12 @@ export default class WalletSettingsPage extends Component<InjectedOrGenerated<Ge
})}
/>
<ExportWallet
openDialog={() => actions.dialogs.open.trigger({
dialog: ExportWalletDialogContainer,
})}
openDialog={() => {
actions.dialogs.open.trigger({
dialog: ExportWalletDialogContainer,
});
trackExportWallet();
}}
/>
<RemoveWallet
walletName={settingsCache.conceptualWalletName}
Expand Down
Loading