diff --git a/CHANGELOG.md b/CHANGELOG.md
index d90baa9c33..8c63a79a35 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,9 +4,7 @@
### Features
-
-### Fixes
-
+- Added analytics data collection ([PR 2927](https://github.com/input-output-hk/daedalus/pull/2927), [PR 2989](https://github.com/input-output-hk/daedalus/pull/2989), [PR 3003](https://github.com/input-output-hk/daedalus/pull/3003), [PR 3028](https://github.com/input-output-hk/daedalus/pull/3028))
### Chores
@@ -17,7 +15,6 @@
### Features
- Added new Mnemonic input component ([PR 2979](https://github.com/input-output-hk/daedalus/pull/2979))
-- Updated Terms of Service ([PR 3009](https://github.com/input-output-hk/daedalus/pull/3009))
### Fixes
@@ -32,6 +29,10 @@
- Fix `darwin-launcher.go` to replace its process image with `cardano-launcher` (binary), and not swallow `stdout` ([PR 3023](https://github.com/input-output-hk/daedalus/pull/3023))
- Updated cardano-node to 1.35.1 ([PR 3012](https://github.com/input-output-hk/daedalus/pull/3012))
+### Features
+
+- Updated Terms of Service ([PR 3009](https://github.com/input-output-hk/daedalus/pull/3009))
+
## 4.12.0
### Fixes
diff --git a/matomo.docker-compose.yml b/matomo.docker-compose.yml
new file mode 100644
index 0000000000..326673539b
--- /dev/null
+++ b/matomo.docker-compose.yml
@@ -0,0 +1,32 @@
+version: "3"
+services:
+ mariadb:
+ image: docker.io/bitnami/mariadb:10.6
+ environment:
+ - ALLOW_EMPTY_PASSWORD=yes
+ - MARIADB_USER=bn_matomo
+ - MARIADB_DATABASE=bitnami_matomo
+ - MARIADB_EXTRA_FLAGS=--max_allowed_packet=64MB
+ volumes:
+ - "matomo_db_data:/bitnami/mariadb"
+ matomo:
+ image: docker.io/bitnami/matomo:4
+ ports:
+ - "8080:8080"
+ environment:
+ - MATOMO_DATABASE_HOST=mariadb
+ - MATOMO_DATABASE_PORT_NUMBER=3306
+ - MATOMO_DATABASE_USER=bn_matomo
+ - MATOMO_DATABASE_NAME=bitnami_matomo
+ - MATOMO_USERNAME=user
+ - MATOMO_PASSWORD=password
+ - ALLOW_EMPTY_PASSWORD=yes
+ volumes:
+ - "matomo_data:/bitnami/matomo"
+ depends_on:
+ - mariadb
+volumes:
+ matomo_db_data:
+ driver: local
+ matomo_data:
+ driver: local
diff --git a/package.json b/package.json
index d4c1917f86..f85dcfc6f5 100644
--- a/package.json
+++ b/package.json
@@ -242,7 +242,8 @@
"inquirer": "7.3.3",
"json-bigint": "1.0.0",
"lodash": "4.17.21",
- "lodash-es": "4.17.21",
+ "lodash-es": "4.17.15",
+ "matomo-tracker": "2.2.4",
"mime-types": "2.1.27",
"mkdirp": "1.0.4",
"mobx": "5.15.7",
diff --git a/source/main/environment.ts b/source/main/environment.ts
index 6720986a82..5118eddfee 100644
--- a/source/main/environment.ts
+++ b/source/main/environment.ts
@@ -50,7 +50,7 @@ const isPreview = checkIsPreview(NETWORK);
const isShelleyQA = checkIsShelleyQA(NETWORK);
const isSelfnode = checkIsSelfnode(NETWORK);
const isDevelopment = checkIsDevelopment(NETWORK);
-const analyticsFeatureEnabled = isMainnet || isStaging || isTestnet;
+const analyticsFeatureEnabled = true;
const keepLocalClusterRunning = process.env.KEEP_LOCAL_CLUSTER_RUNNING;
const API_VERSION = process.env.API_VERSION || 'dev';
const NODE_VERSION = '1.35.3'; // TODO: pick up this value from process.env
diff --git a/source/main/ipc/electronStoreConversation.ts b/source/main/ipc/electronStoreConversation.ts
index f35dd075d1..bbed1f68f3 100644
--- a/source/main/ipc/electronStoreConversation.ts
+++ b/source/main/ipc/electronStoreConversation.ts
@@ -25,29 +25,7 @@ const unset = async (key: StorageKey) =>
});
const reset = async () => {
- await unset(keys.APP_AUTOMATIC_UPDATE_FAILED);
- await unset(keys.APP_UPDATE_COMPLETED);
- await unset(keys.CURRENCY_ACTIVE);
- await unset(keys.CURRENCY_SELECTED);
- await unset(keys.DATA_LAYER_MIGRATION_ACCEPTANCE);
- await unset(keys.DISCREET_MODE_ENABLED);
- await unset(keys.DOWNLOAD_MANAGER);
- await unset(keys.HARDWARE_WALLET_DEVICES);
- await unset(keys.HARDWARE_WALLETS);
- await unset(keys.READ_NEWS);
- await unset(keys.SMASH_SERVER);
- await unset(keys.STAKING_INFO_WAS_OPEN);
- await unset(keys.STAKE_POOLS_LIST_VIEW_TOOLTIP);
- await unset(keys.TERMS_OF_USE_ACCEPTANCE);
- await unset(keys.THEME);
- await unset(keys.USER_DATE_FORMAT_ENGLISH);
- await unset(keys.USER_DATE_FORMAT_JAPANESE);
- await unset(keys.USER_LOCALE);
- await unset(keys.USER_NUMBER_FORMAT);
- await unset(keys.USER_TIME_FORMAT);
- await unset(keys.WALLET_MIGRATION_STATUS);
- await unset(keys.WALLETS);
- await unset(keys.WINDOW_BOUNDS);
+ await Promise.all(Object.values(keys).map(unset));
};
export const requestElectronStore = (request: ElectronStoreMessage) => {
diff --git a/source/renderer/app/App.tsx b/source/renderer/app/App.tsx
index 04b87ad789..718fce2b5d 100755
--- a/source/renderer/app/App.tsx
+++ b/source/renderer/app/App.tsx
@@ -22,6 +22,8 @@ import NewsFeedContainer from './containers/news/NewsFeedContainer';
import ToggleRTSFlagsDialogContainer from './containers/knownIssues/ToggleRTSFlagsDialogContainer';
import RTSFlagsRecommendationOverlayContainer from './containers/knownIssues/RTSFlagsRecommendationOverlayContainer';
import { MenuUpdater } from './containers/MenuUpdater';
+import { AnalyticsProvider } from './components/analytics';
+import { AnalyticsTracker } from './analytics';
@observer
class App extends Component<{
diff --git a/source/renderer/app/Routes.tsx b/source/renderer/app/Routes.tsx
index 75ce499e84..7f6129b635 100644
--- a/source/renderer/app/Routes.tsx
+++ b/source/renderer/app/Routes.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Switch, Route, Redirect, withRouter } from 'react-router-dom';
+import { Redirect, Route, Switch, withRouter } from 'react-router-dom';
import { ROUTES } from './routes-config';
// PAGES
import Root from './containers/Root';
@@ -35,6 +35,7 @@ import WalletUtxoPage from './containers/wallet/WalletUtxoPage';
import VotingRegistrationPage from './containers/voting/VotingRegistrationPage';
import { IS_STAKING_INFO_PAGE_AVAILABLE } from './config/stakingConfig';
import AnalyticsConsentPage from './containers/profile/AnalyticsConsentPage';
+import TrackedRoute from './analytics/TrackedRoute';
export const Routes = withRouter(() => (
@@ -54,11 +55,16 @@ export const Routes = withRouter(() => (
path={ROUTES.PROFILE.ANALYTICS}
component={AnalyticsConsentPage}
/>
-
-
+
(
path={ROUTES.WALLETS.ROOT}
component={() => }
/>
-
-
-
+
-
-
+
-
-
+
@@ -94,31 +116,38 @@ export const Routes = withRouter(() => (
path={ROUTES.SETTINGS.ROOT}
component={() => }
/>
-
-
-
-
-
-
-
@@ -137,33 +166,47 @@ export const Routes = withRouter(() => (
)}
/>
-
-
-
-
-
+
{IS_STAKING_INFO_PAGE_AVAILABLE && (
-
+
)}
-
-
diff --git a/source/renderer/app/actions/wallets-actions.ts b/source/renderer/app/actions/wallets-actions.ts
index ed4149038b..22fac530c8 100644
--- a/source/renderer/app/actions/wallets-actions.ts
+++ b/source/renderer/app/actions/wallets-actions.ts
@@ -7,6 +7,7 @@ import type {
import type { CsvFileContent } from '../../../common/types/csv-request.types';
import type { QuitStakePoolRequest } from '../api/staking/types';
import type { AssetToken } from '../api/assets/types';
+import Wallet from '../domains/Wallet';
export type WalletImportFromFileParams = {
filePath: string;
@@ -76,6 +77,7 @@ export default class WalletsActions {
note: string;
address: string;
filePath: string;
+ wallet: Wallet;
}> = new Action();
generateAddressPDFSuccess: Action<{
walletAddress: string;
@@ -83,6 +85,7 @@ export default class WalletsActions {
saveQRCodeImage: Action<{
address: string;
filePath: string;
+ wallet: Wallet;
}> = new Action();
saveQRCodeImageSuccess: Action<{
walletAddress: string;
diff --git a/source/renderer/app/analytics/MatomoAnalyticsTracker.ts b/source/renderer/app/analytics/MatomoAnalyticsTracker.ts
new file mode 100644
index 0000000000..f788175cea
--- /dev/null
+++ b/source/renderer/app/analytics/MatomoAnalyticsTracker.ts
@@ -0,0 +1,47 @@
+import { AnalyticsAcceptanceStatus, AnalyticsTracker } from '.';
+import { AnalyticsClient } from './types';
+import { Environment } from '../../../common/types/environment.types';
+import LocalStorageApi from '../api/utils/localStorage';
+import { MatomoClient } from './MatomoClient';
+import { NoopAnalyticsClient } from './noopAnalyticsClient';
+
+export class MatomoAnalyticsTracker implements AnalyticsTracker {
+ #analyticsClient: AnalyticsClient;
+
+ constructor(
+ private environment: Environment,
+ private localStorageApi: LocalStorageApi
+ ) {
+ this.#analyticsClient = NoopAnalyticsClient;
+ this.#enableTrackingIfAccepted();
+ }
+
+ async enableTracking() {
+ this.#analyticsClient = new MatomoClient(
+ this.environment,
+ await this.localStorageApi.getUserID()
+ );
+ }
+
+ disableTracking() {
+ this.#analyticsClient = NoopAnalyticsClient;
+ }
+
+ sendPageNavigationEvent(pageTitle: string) {
+ return this.#analyticsClient.sendPageNavigationEvent(pageTitle);
+ }
+
+ sendEvent(category: string, name: string, action?: string) {
+ return this.#analyticsClient.sendEvent(category, name, action);
+ }
+
+ async #enableTrackingIfAccepted() {
+ const analyticsAccepted =
+ (await this.localStorageApi.getAnalyticsAcceptance()) ===
+ AnalyticsAcceptanceStatus.ACCEPTED;
+
+ if (this.environment.analyticsFeatureEnabled && analyticsAccepted) {
+ this.enableTracking();
+ }
+ }
+}
diff --git a/source/renderer/app/analytics/MatomoClient.ts b/source/renderer/app/analytics/MatomoClient.ts
new file mode 100644
index 0000000000..50f95a7435
--- /dev/null
+++ b/source/renderer/app/analytics/MatomoClient.ts
@@ -0,0 +1,68 @@
+import MatomoTracker from 'matomo-tracker';
+import { AnalyticsClient } from './types';
+import { Environment } from '../../../common/types/environment.types';
+import formatCpuInfo from '../utils/formatCpuInfo';
+import {
+ ANALYTICS_API_ENDPOINT,
+ CPU_DIMENSION_KEY,
+ DEV_MODE_SITE_MAP_ID,
+ NETWORK_TO_ANALYTICS_SITE_ID_MAP,
+ OS_DIMENSION_KEY,
+ RAM_DIMENSION_KEY,
+ VERSION_DIMENSION_KEY,
+} from '../config/analyticsConfig';
+import { formattedBytesToSize } from '../utils/formatters';
+
+/**
+ * Matomo API reference:
+ * https://developer.matomo.org/api-reference/tracking-api
+ */
+export class MatomoClient implements AnalyticsClient {
+ private matomoTracker: MatomoTracker;
+
+ constructor(private environment: Environment, private userId: string) {
+ this.matomoTracker = new MatomoTracker(
+ this.getMatomoSiteId(environment),
+ ANALYTICS_API_ENDPOINT
+ );
+ }
+
+ sendPageNavigationEvent = async (pageTitle: string) => {
+ this.matomoTracker.track({
+ _id: this.userId,
+ action_name: pageTitle,
+ url: this.getAnalyticsURL(),
+ [CPU_DIMENSION_KEY]: formatCpuInfo(this.environment.cpu),
+ [RAM_DIMENSION_KEY]: formattedBytesToSize(this.environment.ram),
+ [OS_DIMENSION_KEY]: this.environment.os,
+ [VERSION_DIMENSION_KEY]: this.environment.version,
+ });
+ };
+
+ sendEvent = async (
+ category: string,
+ action: string,
+ name?: string
+ ): Promise => {
+ this.matomoTracker.track({
+ _id: this.userId,
+ ca: 1,
+ e_c: category,
+ e_a: action,
+ e_n: name,
+ url: this.getAnalyticsURL(),
+ });
+ };
+
+ private getAnalyticsURL() {
+ return `http://daedalus/${window.location.hash.replace('#/', '')}`;
+ }
+
+ private getMatomoSiteId(environment: Environment) {
+ if (environment.isDev) {
+ return DEV_MODE_SITE_MAP_ID;
+ }
+
+ return NETWORK_TO_ANALYTICS_SITE_ID_MAP[environment.network];
+ }
+}
diff --git a/source/renderer/app/analytics/NoopAnalyticsTracker.ts b/source/renderer/app/analytics/NoopAnalyticsTracker.ts
new file mode 100644
index 0000000000..f9daa5e01d
--- /dev/null
+++ b/source/renderer/app/analytics/NoopAnalyticsTracker.ts
@@ -0,0 +1,10 @@
+import { AnalyticsTracker } from '.';
+
+class NoopAnalyticsTracker implements AnalyticsTracker {
+ async enableTracking() {}
+ disableTracking() {}
+ sendPageNavigationEvent(pageTitle: string) {}
+ sendEvent(category: string, name: string) {}
+}
+
+export const noopAnalyticsTracker = new NoopAnalyticsTracker();
diff --git a/source/renderer/app/analytics/TrackedRoute.tsx b/source/renderer/app/analytics/TrackedRoute.tsx
new file mode 100644
index 0000000000..8696223007
--- /dev/null
+++ b/source/renderer/app/analytics/TrackedRoute.tsx
@@ -0,0 +1,24 @@
+import React, { FC, useEffect } from 'react';
+import { matchPath, Route, RouteProps } from 'react-router';
+import { useAnalytics } from '../components/analytics';
+
+export type TrackedRouteProps = RouteProps & {
+ pageTitle: string;
+};
+
+function TrackedRoute(props: TrackedRouteProps) {
+ const analytics = useAnalytics();
+ const { pageTitle, ...restProps } = props;
+
+ useEffect(() => {
+ const match = matchPath(window.location.hash.replace('#', ''), props);
+
+ if (match !== null) {
+ analytics.sendPageNavigationEvent(pageTitle);
+ }
+ }, [window.location.hash, props]);
+
+ return ;
+}
+
+export default TrackedRoute;
diff --git a/source/renderer/app/analytics/index.ts b/source/renderer/app/analytics/index.ts
index 328c7daa03..903e452e9f 100644
--- a/source/renderer/app/analytics/index.ts
+++ b/source/renderer/app/analytics/index.ts
@@ -1 +1,3 @@
-export { AnalyticsAcceptanceStatus } from './types';
+export { AnalyticsAcceptanceStatus, EventCategories } from './types';
+export type { AnalyticsClient, AnalyticsTracker } from './types';
+export { noopAnalyticsTracker } from './NoopAnalyticsTracker';
diff --git a/source/renderer/app/analytics/noopAnalyticsClient.ts b/source/renderer/app/analytics/noopAnalyticsClient.ts
new file mode 100644
index 0000000000..6348c62cd4
--- /dev/null
+++ b/source/renderer/app/analytics/noopAnalyticsClient.ts
@@ -0,0 +1,8 @@
+import { AnalyticsClient } from './types';
+
+const NoopAnalyticsClient: AnalyticsClient = {
+ sendPageNavigationEvent: () => Promise.resolve(),
+ sendEvent: () => Promise.resolve(),
+};
+
+export { NoopAnalyticsClient };
diff --git a/source/renderer/app/analytics/types.ts b/source/renderer/app/analytics/types.ts
index f67185663c..56737d3a6b 100644
--- a/source/renderer/app/analytics/types.ts
+++ b/source/renderer/app/analytics/types.ts
@@ -1,5 +1,26 @@
+export interface AnalyticsClient {
+ sendPageNavigationEvent(pageTitle: string): Promise;
+ sendEvent(category: string, action: string, name?: string): Promise;
+}
+
export enum AnalyticsAcceptanceStatus {
PENDING = 'PENDING',
ACCEPTED = 'ACCEPTED',
REJECTED = 'REJECTED',
}
+
+export interface AnalyticsTracker {
+ enableTracking(): Promise;
+ disableTracking(): void;
+ sendPageNavigationEvent(pageTitle: string): void;
+ sendEvent(category: EventCategories, name: string, action?: string): void;
+}
+
+export enum EventCategories {
+ WALLETS = 'Wallets',
+ STAKE_POOLS = 'Stake Pools',
+ SETTINGS = 'Settings',
+ LAYOUT = 'Layout',
+ SYSTEM_MENU = 'System Menu',
+ VOTING = 'Voting',
+}
diff --git a/source/renderer/app/analytics/utils/getEventNameFromWallet.ts b/source/renderer/app/analytics/utils/getEventNameFromWallet.ts
new file mode 100644
index 0000000000..7518328fed
--- /dev/null
+++ b/source/renderer/app/analytics/utils/getEventNameFromWallet.ts
@@ -0,0 +1,4 @@
+import Wallet from '../../domains/Wallet';
+
+export const getEventNameFromWallet = (wallet: Wallet) =>
+ wallet.isHardwareWallet ? 'Hardware wallet' : 'Software wallet';
diff --git a/source/renderer/app/api/index.ts b/source/renderer/app/api/index.ts
index ca98f86621..1f54636431 100644
--- a/source/renderer/app/api/index.ts
+++ b/source/renderer/app/api/index.ts
@@ -6,6 +6,7 @@ export type Api = {
localStorage: LocalStorageApi;
setFaultyNodeSettingsApi?: boolean;
};
+
export const setupApi = (isTest: boolean): Api => ({
ada: new AdaApi(isTest, {
hostname: 'localhost',
diff --git a/source/renderer/app/components/analytics/AnalyticsContext.tsx b/source/renderer/app/components/analytics/AnalyticsContext.tsx
new file mode 100644
index 0000000000..b22d080b85
--- /dev/null
+++ b/source/renderer/app/components/analytics/AnalyticsContext.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import { AnalyticsTracker } from '../../analytics';
+
+export const AnalyticsContext = React.createContext(null);
diff --git a/source/renderer/app/components/analytics/AnalyticsProvider.tsx b/source/renderer/app/components/analytics/AnalyticsProvider.tsx
new file mode 100644
index 0000000000..89e22e336c
--- /dev/null
+++ b/source/renderer/app/components/analytics/AnalyticsProvider.tsx
@@ -0,0 +1,18 @@
+import React, { FC } from 'react';
+import { AnalyticsContext } from './AnalyticsContext';
+import { AnalyticsTracker } from '../../analytics';
+
+interface AnalyticsProviderProps {
+ children: React.ReactNode;
+ tracker: AnalyticsTracker;
+}
+
+function AnalyticsProvider({ children, tracker }: AnalyticsProviderProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export { AnalyticsProvider };
diff --git a/source/renderer/app/components/analytics/index.ts b/source/renderer/app/components/analytics/index.ts
new file mode 100644
index 0000000000..7f317c2d5b
--- /dev/null
+++ b/source/renderer/app/components/analytics/index.ts
@@ -0,0 +1,2 @@
+export { AnalyticsProvider } from './AnalyticsProvider';
+export { useAnalytics } from './useAnalytics';
diff --git a/source/renderer/app/components/analytics/useAnalytics.ts b/source/renderer/app/components/analytics/useAnalytics.ts
new file mode 100644
index 0000000000..caefabad39
--- /dev/null
+++ b/source/renderer/app/components/analytics/useAnalytics.ts
@@ -0,0 +1,4 @@
+import { useContext } from 'react';
+import { AnalyticsContext } from './AnalyticsContext';
+
+export const useAnalytics = () => useContext(AnalyticsContext);
diff --git a/source/renderer/app/components/analytics/withAnalytics.tsx b/source/renderer/app/components/analytics/withAnalytics.tsx
new file mode 100644
index 0000000000..6115bebd4c
--- /dev/null
+++ b/source/renderer/app/components/analytics/withAnalytics.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { AnalyticsTracker } from '../../analytics';
+import { useAnalytics } from './useAnalytics';
+
+export interface WithAnalyticsTrackerProps {
+ analyticsTracker: AnalyticsTracker;
+}
+
+export function withAnalytics<
+ T extends WithAnalyticsTrackerProps = WithAnalyticsTrackerProps
+>(WrappedComponent: React.ComponentType) {
+ const displayName =
+ WrappedComponent.displayName || WrappedComponent.name || 'Component';
+
+ function ComponentWithTheme(props: Omit) {
+ const analyticsTracker = useAnalytics();
+
+ // props comes afterwards so the can override the default ones.
+ return (
+
+ );
+ }
+
+ ComponentWithTheme.displayName = `withAnalytics(${displayName})`;
+
+ return ComponentWithTheme;
+}
diff --git a/source/renderer/app/components/profile/analytics/AnalyticsConsentForm.messages.ts b/source/renderer/app/components/profile/analytics/AnalyticsConsentForm.messages.ts
index 0b90e08a8d..91627ff990 100644
--- a/source/renderer/app/components/profile/analytics/AnalyticsConsentForm.messages.ts
+++ b/source/renderer/app/components/profile/analytics/AnalyticsConsentForm.messages.ts
@@ -22,8 +22,8 @@ export const messages = defineMessages({
defaultMessage: '!!!Allow',
description: 'Analytics data collection allow button text',
},
- skipButton: {
- id: 'analytics.dialog.skipButton',
+ disallowButton: {
+ id: 'analytics.form.disallowButton',
defaultMessage: '!!!Skip',
description: 'Analytics data collection skip button text',
},
diff --git a/source/renderer/app/components/profile/analytics/AnalyticsConsentForm.scss b/source/renderer/app/components/profile/analytics/AnalyticsConsentForm.scss
index fc43c7feef..84c990cd88 100644
--- a/source/renderer/app/components/profile/analytics/AnalyticsConsentForm.scss
+++ b/source/renderer/app/components/profile/analytics/AnalyticsConsentForm.scss
@@ -6,6 +6,7 @@
height: 100%;
justify-content: center;
line-height: 1.38;
+ margin: 40px 0;
}
.centeredBox {
@@ -42,7 +43,7 @@
margin-top: 24px;
}
-.skipButton {
+.disallowButton {
margin-right: 12px;
}
diff --git a/source/renderer/app/components/profile/analytics/AnalyticsConsentForm.tsx b/source/renderer/app/components/profile/analytics/AnalyticsConsentForm.tsx
index 53b8ae8d69..41bca62ca2 100644
--- a/source/renderer/app/components/profile/analytics/AnalyticsConsentForm.tsx
+++ b/source/renderer/app/components/profile/analytics/AnalyticsConsentForm.tsx
@@ -1,4 +1,4 @@
-import React, { FC, useCallback } from 'react';
+import React, { useCallback } from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import { Button } from 'react-polymorph/lib/components/Button';
import { Link } from 'react-polymorph/lib/components/Link';
@@ -8,6 +8,7 @@ import styles from './AnalyticsConsentForm.scss';
import { Intl } from '../../../types/i18nTypes';
import { messages } from './AnalyticsConsentForm.messages';
import { CollectedDataOverview } from './CollectedDataOverview';
+import { PRIVACY_POLICY_LINK } from '../../../config/analyticsConfig';
interface AnalyticsConsentFormProps {
intl: Intl;
@@ -32,11 +33,7 @@ function AnalyticsConsentForm({
const privacyPolicyLink = (
- onExternalLinkClick(
- 'https://static.iohk.io/terms/iog-privacy-policy.pdf'
- )
- }
+ onClick={() => onExternalLinkClick(PRIVACY_POLICY_LINK)}
label={intl.formatMessage(messages.privacyPolicyLink)}
hasIconAfter={false}
/>
@@ -60,8 +57,8 @@ function AnalyticsConsentForm({
)}
@@ -1373,6 +1380,10 @@ class WalletSendForm extends Component {
onAdd={(checked) => {
onTokenPickerDialogClose();
checked.forEach(this.addAssetRow);
+ this.props.analyticsTracker.sendEvent(
+ EventCategories.WALLETS,
+ 'Added token to transaction'
+ );
}}
/>
)}
diff --git a/source/renderer/app/components/wallet/send-form/AssetInput.tsx b/source/renderer/app/components/wallet/send-form/AssetInput.tsx
index 6dde6ff1fd..ae2541d470 100644
--- a/source/renderer/app/components/wallet/send-form/AssetInput.tsx
+++ b/source/renderer/app/components/wallet/send-form/AssetInput.tsx
@@ -11,7 +11,7 @@ import removeIcon from '../../../assets/images/remove.inline.svg';
import type { NumberFormat } from '../../../../../common/types/number.types';
import { DiscreetTokenWalletAmount } from '../../../features/discreet-mode';
import Asset from '../../assets/Asset';
-import { Divider } from '../widgets/Divider';
+import { VerticalSeparator } from '../widgets/VerticalSeparator';
import { ClearButton } from '../widgets/ClearButton';
import styles from './AssetInput.scss';
import messages from './messages';
@@ -152,7 +152,7 @@ class AssetInput extends Component {
)}
{ticker ? (
<>
-
+
{ticker}
>
) : null}
diff --git a/source/renderer/app/components/wallet/transactions/WalletTransactionsList.tsx b/source/renderer/app/components/wallet/transactions/WalletTransactionsList.tsx
index 37b3ea9c90..c9c2862988 100644
--- a/source/renderer/app/components/wallet/transactions/WalletTransactionsList.tsx
+++ b/source/renderer/app/components/wallet/transactions/WalletTransactionsList.tsx
@@ -16,6 +16,7 @@ import { SimpleTransactionList } from './render-strategies/SimpleTransactionList
import { TransactionInfo, TransactionsGroup } from './types';
import type { Row } from './types';
import { getNonZeroAssetTokens } from '../../../utils/assets';
+import { AnalyticsTracker, EventCategories } from '../../../analytics';
const messages = defineMessages({
today: {
@@ -69,6 +70,7 @@ type Props = {
getAsset: (...args: Array) => any;
isInternalAddress: (...args: Array) => any;
onCopyAssetParam: (...args: Array) => any;
+ analyticsTracker: AnalyticsTracker;
};
type State = {
isPreloading: boolean;
@@ -203,6 +205,10 @@ class WalletTransactionsList extends Component {
onShowMoreTransactions = (walletId: string) => {
if (this.props.onShowMoreTransactions) {
this.props.onShowMoreTransactions(walletId);
+ this.props.analyticsTracker.sendEvent(
+ EventCategories.WALLETS,
+ 'Clicked Show More Transactions button'
+ );
}
};
getExpandedTransactions = () => this.expandedTransactionIds;
diff --git a/source/renderer/app/components/wallet/widgets/Divider.tsx b/source/renderer/app/components/wallet/widgets/VerticalSeparator.tsx
similarity index 75%
rename from source/renderer/app/components/wallet/widgets/Divider.tsx
rename to source/renderer/app/components/wallet/widgets/VerticalSeparator.tsx
index dee00283b9..d79a795011 100644
--- a/source/renderer/app/components/wallet/widgets/Divider.tsx
+++ b/source/renderer/app/components/wallet/widgets/VerticalSeparator.tsx
@@ -2,6 +2,6 @@ import React from 'react';
import styles from './Divider.scss';
-export function Divider() {
+export function VerticalSeparator() {
return ;
}
diff --git a/source/renderer/app/config/analyticsConfig.ts b/source/renderer/app/config/analyticsConfig.ts
index 9fbfef09db..cd163cddaa 100644
--- a/source/renderer/app/config/analyticsConfig.ts
+++ b/source/renderer/app/config/analyticsConfig.ts
@@ -1,17 +1,28 @@
import { Network } from '../../../common/types/environment.types';
-export const ANALYTICS_API_ENDPOINT = 'https://mazurek.matomo.cloud/matomo.php';
-export const DEV_MODE_SITE_MAP_ID = 5;
+export const ANALYTICS_API_ENDPOINT = 'https://matomo.cw.iog.io/matomo.php';
+export const PRIVACY_POLICY_LINK =
+ 'https://static.iohk.io/terms/iog-privacy-policy.pdf';
+
+// ID used when Daedalus is launched from nix-shell
+export const DEV_MODE_SITE_MAP_ID = 11;
+
+// IDs used when Daedalus is launched as a binary (installed with installer)
export const NETWORK_TO_ANALYTICS_SITE_ID_MAP: Record = {
- mainnet: 4,
- mainnet_flight: 4,
+ mainnet: 2,
+ mainnet_flight: 12,
testnet: 3,
- preprod: 3,
- preview: 3,
- staging: 5,
- shelley_qa: 5,
- alonzo_purple: 5,
- selfnode: 5,
- development: 5,
- vasil_dev: 5,
+ preprod: 9,
+ preview: 10,
+ staging: 4,
+ shelley_qa: 6,
+ alonzo_purple: 7,
+ selfnode: 11,
+ development: 11,
+ vasil_dev: 8,
};
+
+export const CPU_DIMENSION_KEY = 'dimension1';
+export const RAM_DIMENSION_KEY = 'dimension2';
+export const OS_DIMENSION_KEY = 'dimension3';
+export const VERSION_DIMENSION_KEY = 'dimension4';
diff --git a/source/renderer/app/containers/MenuUpdater/useMenuUpdater.ts b/source/renderer/app/containers/MenuUpdater/useMenuUpdater.ts
index ef6957e23e..a701e81979 100644
--- a/source/renderer/app/containers/MenuUpdater/useMenuUpdater.ts
+++ b/source/renderer/app/containers/MenuUpdater/useMenuUpdater.ts
@@ -3,6 +3,7 @@ import { matchPath } from 'react-router-dom';
import { WalletSettingsStateEnum } from '../../../../common/ipc/api';
import { ROUTES } from '../../routes-config';
import type { UseMenuUpdaterArgs } from './types';
+import { AnalyticsAcceptanceStatus } from '../../analytics';
const walletRoutes = Object.values(ROUTES.WALLETS);
@@ -32,7 +33,9 @@ const useMenuUpdater = ({
}
rebuildApplicationMenu.send({
- isNavigationEnabled: profile.areTermsOfUseAccepted,
+ isNavigationEnabled:
+ profile.areTermsOfUseAccepted &&
+ profile.analyticsAcceptanceStatus !== AnalyticsAcceptanceStatus.PENDING,
walletSettingsState,
});
}, [
diff --git a/source/renderer/app/containers/settings/categories/SupportSettingsPage.tsx b/source/renderer/app/containers/settings/categories/SupportSettingsPage.tsx
index 134b374fee..1742b3842d 100644
--- a/source/renderer/app/containers/settings/categories/SupportSettingsPage.tsx
+++ b/source/renderer/app/containers/settings/categories/SupportSettingsPage.tsx
@@ -5,6 +5,7 @@ import SupportSettings from '../../../components/settings/categories/SupportSett
import { generateSupportRequestLink } from '../../../../../common/utils/reporting';
import type { InjectedProps } from '../../../types/injectedPropsType';
import { AnalyticsAcceptanceStatus } from '../../../analytics';
+import { ROUTES } from '../../../routes-config';
const messages = defineMessages({
supportRequestLinkUrl: {
@@ -28,9 +29,9 @@ class SupportSettingsPage extends Component {
};
handleChangeAnalyticsSettings = () => {
- this.props.actions.profile.acceptAnalytics.trigger(
- AnalyticsAcceptanceStatus.PENDING
- );
+ this.props.actions.router.goToRoute.trigger({
+ route: ROUTES.PROFILE.ANALYTICS,
+ });
};
handleSupportRequestClick = async (
diff --git a/source/renderer/app/containers/staking/StakePoolsListPage.tsx b/source/renderer/app/containers/staking/StakePoolsListPage.tsx
index 021da8f412..2ac5d09dc5 100644
--- a/source/renderer/app/containers/staking/StakePoolsListPage.tsx
+++ b/source/renderer/app/containers/staking/StakePoolsListPage.tsx
@@ -6,8 +6,12 @@ import DelegationSetupWizardDialogContainer from './dialogs/DelegationSetupWizar
import DelegationSetupWizardDialog from '../../components/staking/delegation-setup-wizard/DelegationSetupWizardDialog';
import { ROUTES } from '../../routes-config';
import type { InjectedProps } from '../../types/injectedPropsType';
+import {
+ withAnalytics,
+ WithAnalyticsTrackerProps,
+} from '../../components/analytics/withAnalytics';
-type Props = InjectedProps;
+type Props = InjectedProps & WithAnalyticsTrackerProps;
@inject('stores', 'actions')
@observer
@@ -55,6 +59,7 @@ class StakePoolsListPage extends Component {
networkStatus,
profile,
wallets,
+ analytics,
} = this.props.stores;
const { currentTheme, currentLocale } = profile;
const { isSynced } = networkStatus;
@@ -77,6 +82,7 @@ class StakePoolsListPage extends Component {
return (
{
}
}
-export default StakePoolsListPage;
+export default withAnalytics(StakePoolsListPage);
diff --git a/source/renderer/app/containers/wallet/WalletReceivePage.tsx b/source/renderer/app/containers/wallet/WalletReceivePage.tsx
index b518de6a21..9128b75213 100755
--- a/source/renderer/app/containers/wallet/WalletReceivePage.tsx
+++ b/source/renderer/app/containers/wallet/WalletReceivePage.tsx
@@ -13,6 +13,7 @@ import { generateFileNameWithTimestamp } from '../../../../common/utils/files';
import { ellipsis } from '../../utils/strings';
import { generateSupportRequestLink } from '../../../../common/utils/reporting';
import { WalletLocalData } from '../../types/localDataTypes';
+import Wallet from '../../domains/Wallet';
const messages = defineMessages({
address: {
@@ -40,7 +41,7 @@ class WalletReceivePage extends Component {
addressToShare: null,
};
- get activeWallet() {
+ get activeWallet(): Wallet {
return this.props.stores.wallets.active;
}
@@ -142,6 +143,7 @@ class WalletReceivePage extends Component {
note,
address,
filePath,
+ wallet: this.activeWallet,
});
};
handleSaveQRCodeImage = async () => {
@@ -151,6 +153,7 @@ class WalletReceivePage extends Component {
this.props.actions.wallets.saveQRCodeImage.trigger({
address,
filePath,
+ wallet: this.activeWallet,
});
};
handleGenerateAddress = (passphrase: string) => {
diff --git a/source/renderer/app/containers/wallet/WalletSendPage.tsx b/source/renderer/app/containers/wallet/WalletSendPage.tsx
index 87d5802984..f7509247ae 100755
--- a/source/renderer/app/containers/wallet/WalletSendPage.tsx
+++ b/source/renderer/app/containers/wallet/WalletSendPage.tsx
@@ -15,9 +15,13 @@ import { WALLET_ASSETS_ENABLED } from '../../config/walletsConfig';
import Asset from '../../domains/Asset';
import type { ApiTokens } from '../../api/assets/types';
import { getNonZeroAssetTokens } from '../../utils/assets';
+import {
+ withAnalytics,
+ WithAnalyticsTrackerProps,
+} from '../../components/analytics/withAnalytics';
import { CoinSelectionsResponse } from '../../api/transactions/types';
-type Props = InjectedProps;
+type Props = InjectedProps & WithAnalyticsTrackerProps;
type State = {
confirmationDialogData: ConfirmationDialogData;
};
@@ -186,10 +190,11 @@ class WalletSendPage extends Component {
walletName={walletName}
onTokenPickerDialogOpen={this.openTokenPickerDialog}
onTokenPickerDialogClose={this.closeTokenPickerDialog}
+ analyticsTracker={this.props.analyticsTracker}
confirmationDialogData={this.state.confirmationDialogData}
/>
);
}
}
-export default WalletSendPage;
+export default withAnalytics(WalletSendPage);
diff --git a/source/renderer/app/containers/wallet/WalletSummaryPage.tsx b/source/renderer/app/containers/wallet/WalletSummaryPage.tsx
index 5e1642812d..5dd71ccf91 100755
--- a/source/renderer/app/containers/wallet/WalletSummaryPage.tsx
+++ b/source/renderer/app/containers/wallet/WalletSummaryPage.tsx
@@ -14,6 +14,10 @@ import { formattedWalletAmount } from '../../utils/formatters';
import { getNetworkExplorerUrlByType } from '../../utils/network';
import { WALLET_ASSETS_ENABLED } from '../../config/walletsConfig';
import { getAssetTokens, sortAssets } from '../../utils/assets';
+import {
+ withAnalytics,
+ WithAnalyticsTrackerProps,
+} from '../../components/analytics/withAnalytics';
import type { InjectedProps } from '../../types/injectedPropsType';
export const messages = defineMessages({
@@ -24,7 +28,7 @@ export const messages = defineMessages({
'Message shown when wallet has no transactions on wallet summary page.',
},
});
-type Props = InjectedProps;
+type Props = InjectedProps & WithAnalyticsTrackerProps;
type OpenAssetSettingsDialogArgs = {
asset: AssetToken;
};
@@ -162,6 +166,7 @@ class WalletSummaryPage extends Component {
hasAssetsEnabled={hasAssetsEnabled}
getAsset={getAsset}
onCopyAssetParam={onCopyAssetParam.trigger}
+ analyticsTracker={this.props.analyticsTracker}
/>
);
} else if (!hasAny) {
@@ -203,4 +208,4 @@ class WalletSummaryPage extends Component {
}
}
-export default WalletSummaryPage;
+export default withAnalytics(WalletSummaryPage);
diff --git a/source/renderer/app/features/discreet-mode/context.tsx b/source/renderer/app/features/discreet-mode/context.tsx
index ad5cd5c240..752686fe4f 100644
--- a/source/renderer/app/features/discreet-mode/context.tsx
+++ b/source/renderer/app/features/discreet-mode/context.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
// @ts-ignore ts-migrate(2305) FIXME: Module '"react"' has no exported member 'Node'.
import type { Node } from 'react';
import { merge } from 'lodash/fp';
@@ -9,6 +9,7 @@ import {
import { useLocalStorageFeature } from '../local-storage';
import { DiscreetMode } from './feature';
import { DiscreetModeApi } from './api';
+import { useAnalytics } from '../../components/analytics';
export const discreetModeContext = React.createContext(
null
@@ -16,19 +17,28 @@ export const discreetModeContext = React.createContext(
interface Props {
children: Node;
}
+
export function DiscreetModeFeatureProvider({ children }: Props) {
const localStorageFeature = useLocalStorageFeature();
+ const analyticsTracker = useAnalytics();
+
const [discreetModeFeature] = useState(() => {
- const feature = new DiscreetMode(new DiscreetModeApi(localStorageFeature));
- // @ts-ignore ts-migrate(2339) FIXME: Property 'daedalus' does not exist on type 'Window... Remove this comment to see the full error message
+ const feature = new DiscreetMode(
+ new DiscreetModeApi(localStorageFeature),
+ analyticsTracker
+ );
+
window.daedalus = merge(window.daedalus, {
features: {
discreetModeFeature: feature,
},
});
+
return feature;
});
+
useFeature(discreetModeFeature);
+
return (
{children}
diff --git a/source/renderer/app/features/discreet-mode/feature.ts b/source/renderer/app/features/discreet-mode/feature.ts
index 1f783de586..9d59a72e99 100644
--- a/source/renderer/app/features/discreet-mode/feature.ts
+++ b/source/renderer/app/features/discreet-mode/feature.ts
@@ -5,13 +5,14 @@ import { DiscreetModeApi } from './api';
import { SENSITIVE_DATA_SYMBOL } from './config';
import { defaultReplacer } from './replacers/defaultReplacer';
import type { ReplacerFn } from './types';
+import { AnalyticsTracker, EventCategories } from '../../analytics';
export class DiscreetMode extends Feature {
- api: DiscreetModeApi;
-
- constructor(api: DiscreetModeApi) {
+ constructor(
+ private api: DiscreetModeApi,
+ private analyticsTracker: AnalyticsTracker
+ ) {
super();
- this.api = api;
runInAction(() => {
this.getDiscreetModeSettingsRequest = new Request(
this.api.getDiscreetModeSettings
@@ -50,6 +51,10 @@ export class DiscreetMode extends Feature {
@action
toggleDiscreetMode = () => {
this.isDiscreetMode = !this.isDiscreetMode;
+ this.analyticsTracker.sendEvent(
+ EventCategories.SETTINGS,
+ `Turned ${this.isDiscreetMode ? 'on' : 'off'} discreet mode`
+ );
};
@action
toggleOpenInDiscreetMode = async () => {
@@ -59,6 +64,10 @@ export class DiscreetMode extends Feature {
runInAction('Update open in discreet mode settings', () => {
this.openInDiscreetMode = nextSetting;
});
+ this.analyticsTracker.sendEvent(
+ EventCategories.SETTINGS,
+ `Turned ${nextSetting ? 'on' : 'off'} discreet mode by default`
+ );
};
discreetValue({
diff --git a/source/renderer/app/features/discreet-mode/integration-tests.spec.tsx b/source/renderer/app/features/discreet-mode/integration-tests.spec.tsx
index 77f20d3288..3d7b89e8e2 100644
--- a/source/renderer/app/features/discreet-mode/integration-tests.spec.tsx
+++ b/source/renderer/app/features/discreet-mode/integration-tests.spec.tsx
@@ -16,6 +16,8 @@ import {
withDiscreetMode,
} from './ui';
import { DiscreetMode } from './feature';
+import { AnalyticsProvider } from '../../components/analytics';
+import { noopAnalyticsTracker } from '../../analytics';
describe('Discreet Mode feature', () => {
afterEach(cleanup);
@@ -29,15 +31,17 @@ describe('Discreet Mode feature', () => {
}) {
return (
-
-
- <>
-
+
+
+
+ <>
+
- {children}
- >
-
-
+ {children}
+ >
+
+
+
);
}
diff --git a/source/renderer/app/i18n/locales/defaultMessages.json b/source/renderer/app/i18n/locales/defaultMessages.json
index cdac62b7d3..6e1cd7b2ab 100644
--- a/source/renderer/app/i18n/locales/defaultMessages.json
+++ b/source/renderer/app/i18n/locales/defaultMessages.json
@@ -2677,15 +2677,20 @@
"id": "settings.support.steps.reportProblem.link"
},
{
- "defaultMessage": "!!!You have opted in to analytics data collection. You can {changeAnalyticsSettingsLink}.",
+ "defaultMessage": "!!!You have opted in to analytics data collection. You can {changeAnalyticsSettingsLink}.",
"description": "Analytics data collection description when user opted in",
"id": "analytics.form.analyticsAcceptedDescription"
},
{
- "defaultMessage": "!!!You have opted out of analytics data collection. You can {changeAnalyticsSettingsLink}.",
+ "defaultMessage": "!!!You have opted out of analytics data collection. You can {changeAnalyticsSettingsLink}.",
"description": "Analytics data collection description when user opted out ",
"id": "analytics.form.analyticsDeclinedDescription"
},
+ {
+ "defaultMessage": "!!!You can {changeAnalyticsSettingsLink}.",
+ "description": "Change analytics settings link",
+ "id": "analytics.form.changeAnalyticsSettings"
+ },
{
"defaultMessage": "!!!change this setting here",
"description": "Change analytics settings link text",
@@ -2714,7 +2719,7 @@
{
"defaultMessage": "!!!Skip",
"description": "Analytics data collection skip button text",
- "id": "analytics.dialog.skipButton"
+ "id": "analytics.form.disallowButton"
},
{
"defaultMessage": "!!!Daedalus Privacy Policy",
diff --git a/source/renderer/app/i18n/locales/en-US.json b/source/renderer/app/i18n/locales/en-US.json
index ffa6a90ddf..4bb4bb4ef2 100755
--- a/source/renderer/app/i18n/locales/en-US.json
+++ b/source/renderer/app/i18n/locales/en-US.json
@@ -3,11 +3,11 @@
"ImageUploadWidget.dropFileHint": "!!!Drop file here",
"analytics.dialog.collapseButton": "Collapse details",
"analytics.dialog.expandButton": "Expand details",
- "analytics.dialog.skipButton": "Skip",
"analytics.form.allowButton": "Allow",
- "analytics.form.analyticsAcceptedDescription": "You have opted in to analytics data collection. You can {changeAnalyticsSettingsLink}.",
- "analytics.form.analyticsDeclinedDescription": "You have opted out of analytics data collection. You can {changeAnalyticsSettingsLink}.",
+ "analytics.form.analyticsAcceptedDescription": "You have opted in to analytics data collection. ",
+ "analytics.form.analyticsDeclinedDescription": "You have opted out of analytics data collection. ",
"analytics.form.analyticsSectionPrivacyPolicy": "Read more about our privacy practices in the {privacyPolicyLink}.",
+ "analytics.form.changeAnalyticsSettings": "You can {changeAnalyticsSettingsLink}.",
"analytics.form.changeAnalyticsSettingsLink": "change this setting here",
"analytics.form.dataCollectionDetailsDeviceInfoText": "Operating system, RAM, and disk space.",
"analytics.form.dataCollectionDetailsDeviceInfoTitle": "Device info",
@@ -16,6 +16,7 @@
"analytics.form.dataCollectionDetailsUserBehaviourTitle": "User click behavior",
"analytics.form.dataCollectionSwitchText": "Allow anonymous data collection",
"analytics.form.description": "All data is anonymous and is used only for product development purposes.",
+ "analytics.form.disallowButton": "Disallow",
"analytics.form.privacyPolicyLink": "Daedalus Privacy Policy",
"analytics.form.title": "Analytics data collection",
"api.errors.ApiMethodNotYetImplementedError": "This API method is not yet implemented.",
diff --git a/source/renderer/app/i18n/locales/ja-JP.json b/source/renderer/app/i18n/locales/ja-JP.json
index 173dca13a1..56659c05b2 100755
--- a/source/renderer/app/i18n/locales/ja-JP.json
+++ b/source/renderer/app/i18n/locales/ja-JP.json
@@ -3,11 +3,11 @@
"ImageUploadWidget.dropFileHint": "!!!Drop file here",
"analytics.dialog.collapseButton": "詳細を隠す",
"analytics.dialog.expandButton": "詳細を表示する",
- "analytics.dialog.skipButton": "スキップする",
"analytics.form.allowButton": "許可する",
- "analytics.form.analyticsAcceptedDescription": "分析データの収集が設定されています。この設定は{changeAnalyticsSettingsLink}できます。",
- "analytics.form.analyticsDeclinedDescription": "分析データの収集が解除されています。この設定は{changeAnalyticsSettingsLink}できます。",
+ "analytics.form.analyticsAcceptedDescription": "分析データの収集が設定されています。",
+ "analytics.form.analyticsDeclinedDescription": "分析データの収集が解除されています。",
"analytics.form.analyticsSectionPrivacyPolicy": "プライバシーに関する詳細は、{privacyPolicyLink}をご覧ください。",
+ "analytics.form.changeAnalyticsSettings": "この設定は{changeAnalyticsSettingsLink}できます。",
"analytics.form.changeAnalyticsSettingsLink": "ここから変更",
"analytics.form.dataCollectionDetailsDeviceInfoText": "オペレーティングシステム、RAM、ディスク空き容量",
"analytics.form.dataCollectionDetailsDeviceInfoTitle": "デバイス情報",
@@ -16,6 +16,7 @@
"analytics.form.dataCollectionDetailsUserBehaviourTitle": "ユーザーのクリック行動",
"analytics.form.dataCollectionSwitchText": "匿名データ収集を許可する",
"analytics.form.description": "分析データは、製品開発の目的でのみ使用されます。",
+ "analytics.form.disallowButton": "許可しない",
"analytics.form.privacyPolicyLink": "Daedalusプライバシーポリシー",
"analytics.form.title": "分析データの収集",
"api.errors.ApiMethodNotYetImplementedError": "このAPIはまだ実装されていません。",
diff --git a/source/renderer/app/index.tsx b/source/renderer/app/index.tsx
index 1dd930ec81..0db1b27654 100755
--- a/source/renderer/app/index.tsx
+++ b/source/renderer/app/index.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { configure, action } from 'mobx';
+import { action, configure } from 'mobx';
import { render } from 'react-dom';
import { addLocaleData } from 'react-intl';
import en from 'react-intl/locale-data/en';
@@ -7,19 +7,20 @@ import ja from 'react-intl/locale-data/ja';
import { createHashHistory } from 'history';
import { RouterStore, syncHistoryWithStore } from 'mobx-react-router';
import App from './App';
-import setupStores from './stores';
+import { setUpStores } from './stores';
import actions from './actions';
import utils from './utils';
import Action from './actions/lib/Action';
import translations from './i18n/translations';
import '!style-loader!css-loader!sass-loader!./themes/index.global.scss'; // eslint-disable-line
-
import { setupApi } from './api';
import LocalStorageApi from './api/utils/localStorage';
import {
DiscreetModeFeatureProvider,
LocalStorageFeatureProvider,
} from './features';
+import { MatomoAnalyticsTracker } from './analytics/MatomoAnalyticsTracker';
+import { AnalyticsProvider } from './components/analytics';
// run MobX in strict mode
configure({
enforceActions: 'always',
@@ -34,7 +35,11 @@ const initializeDaedalus = () => {
const api = setupApi(isTest);
const hashHistory = createHashHistory();
const routingStore = new RouterStore();
- const stores = setupStores(api, actions, routingStore);
+ const analyticsTracker = new MatomoAnalyticsTracker(
+ environment,
+ api.localStorage
+ );
+ const stores = setUpStores(api, actions, routingStore, analyticsTracker);
const history = syncHistoryWithStore(hashHistory, routingStore);
// @ts-ignore ts-migrate(2339) FIXME: Property 'daedalus' does not exist on type 'Window... Remove this comment to see the full error message
window.daedalus = {
@@ -46,16 +51,18 @@ const initializeDaedalus = () => {
translations,
reset: action(() => {
Action.resetAllActions();
- setupStores(api, actions, routingStore);
+ setUpStores(api, actions, routingStore, analyticsTracker);
}),
};
const rootElement = document.getElementById('root');
if (!rootElement) throw new Error('No #root element found.');
render(
-
-
-
+
+
+
+
+
,
rootElement
);
diff --git a/source/renderer/app/stores/AppStore.ts b/source/renderer/app/stores/AppStore.ts
index 75968db700..04d90bd00a 100644
--- a/source/renderer/app/stores/AppStore.ts
+++ b/source/renderer/app/stores/AppStore.ts
@@ -15,6 +15,7 @@ import { getGPUStatusChannel } from '../ipc/get-gpu-status.ipc';
import { generateFileNameWithTimestamp } from '../../../common/utils/files';
import type { GpuStatus } from '../types/gpuStatus';
import type { ApplicationDialog } from '../types/applicationDialogTypes';
+import { EventCategories } from '../analytics';
export default class AppStore extends Store {
@observable
@@ -87,6 +88,10 @@ export default class AppStore extends Store {
@action
_toggleNewsFeed = () => {
this.newsFeedIsOpen = !this.newsFeedIsOpen;
+
+ if (this.newsFeedIsOpen) {
+ this.analytics.sendEvent(EventCategories.LAYOUT, 'Opened newsfeed');
+ }
};
@action
_closeNewsFeed = () => {
@@ -114,29 +119,46 @@ export default class AppStore extends Store {
case DIALOGS.ABOUT:
// @ts-ignore ts-migrate(2345) FIXME: Argument of type 'string' is not assignable to par... Remove this comment to see the full error message
this._updateActiveDialog(DIALOGS.ABOUT);
-
+ this.analytics.sendEvent(
+ EventCategories.SYSTEM_MENU,
+ 'Showed about dialog'
+ );
break;
case DIALOGS.DAEDALUS_DIAGNOSTICS:
// @ts-ignore ts-migrate(2345) FIXME: Argument of type 'string' is not assignable to par... Remove this comment to see the full error message
this._updateActiveDialog(DIALOGS.DAEDALUS_DIAGNOSTICS);
+ this.analytics.sendEvent(
+ EventCategories.SYSTEM_MENU,
+ 'Showed diagnostics dialog'
+ );
break;
case DIALOGS.TOGGLE_RTS_FLAGS_MODE:
// @ts-ignore ts-migrate(2345) FIXME: Argument of type 'string' is not assignable to par... Remove this comment to see the full error message
this._updateActiveDialog(DIALOGS.TOGGLE_RTS_FLAGS_MODE);
-
+ this.analytics.sendEvent(
+ EventCategories.SYSTEM_MENU,
+ 'Showed toggle RTS flags dialog'
+ );
break;
case DIALOGS.ITN_REWARDS_REDEMPTION:
// @ts-ignore ts-migrate(2554) FIXME: Expected 1 arguments, but got 0.
this.actions.staking.onRedeemStart.trigger();
+ this.analytics.sendEvent(
+ EventCategories.SYSTEM_MENU,
+ 'Showed ITN rewards redemption dialog'
+ );
break;
case NOTIFICATIONS.DOWNLOAD_LOGS:
this._downloadLogs();
-
+ this.analytics.sendEvent(
+ EventCategories.SYSTEM_MENU,
+ 'Downloaded logs'
+ );
break;
case PAGES.SETTINGS:
diff --git a/source/renderer/app/stores/AssetsStore.ts b/source/renderer/app/stores/AssetsStore.ts
index c8ff22af56..bc23ef3d10 100644
--- a/source/renderer/app/stores/AssetsStore.ts
+++ b/source/renderer/app/stores/AssetsStore.ts
@@ -6,6 +6,7 @@ import Asset from '../domains/Asset';
import { ROUTES } from '../routes-config';
import { ellipsis } from '../utils/strings';
import type { GetAssetsResponse, AssetToken } from '../api/assets/types';
+import { EventCategories } from '../analytics';
type WalletId = string;
export default class AssetsStore extends Store {
@@ -107,6 +108,11 @@ export default class AssetsStore extends Store {
await this.api.localStorage.setAssetLocalData(policyId, assetName, {
decimals,
});
+
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Changed native token settings'
+ );
};
@action
_onEditedAssetUnset = () => {
@@ -193,6 +199,11 @@ export default class AssetsStore extends Store {
);
// @ts-ignore ts-migrate(1320) FIXME: Type of 'await' operand must either be a valid pro... Remove this comment to see the full error message
await this.favoritesRequest.execute();
+
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ `${!isFavorite ? 'Added token to' : 'Removed token from'} favorites`
+ );
};
_retrieveAssetsRequest = (walletId: string): Request =>
this.assetsRequests[walletId] || this._createWalletTokensRequest(walletId);
diff --git a/source/renderer/app/stores/CurrencyStore.ts b/source/renderer/app/stores/CurrencyStore.ts
index 8ac7ad688c..cbdaddb31a 100644
--- a/source/renderer/app/stores/CurrencyStore.ts
+++ b/source/renderer/app/stores/CurrencyStore.ts
@@ -7,6 +7,7 @@ import {
getCurrencyFromCode,
} from '../config/currencyConfig';
import type { Currency, LocalizedCurrency } from '../types/currencyTypes';
+import { EventCategories } from '../analytics';
export default class CurrencyStore extends Store {
@observable
@@ -127,10 +128,23 @@ export default class CurrencyStore extends Store {
this.getCurrencyRate();
await this.api.localStorage.setCurrencySelected(selected.code);
}
+
+ this.analytics.sendEvent(
+ EventCategories.SETTINGS,
+ 'Changed currency',
+ code
+ );
};
@action
_toggleCurrencyIsActive = () => {
this.isActive = !this.isActive;
this.api.localStorage.setCurrencyIsActive(this.isActive);
+
+ this.analytics.sendEvent(
+ EventCategories.SETTINGS,
+ `Turned ${
+ this.isActive ? 'on' : 'off'
+ } displaying ada balances in other currency`
+ );
};
}
diff --git a/source/renderer/app/stores/HardwareWalletsStore.ts b/source/renderer/app/stores/HardwareWalletsStore.ts
index 56d9985649..fb6dc7cccf 100644
--- a/source/renderer/app/stores/HardwareWalletsStore.ts
+++ b/source/renderer/app/stores/HardwareWalletsStore.ts
@@ -97,6 +97,7 @@ import type {
TrezorWitness,
} from '../../../common/types/hardware-wallets.types';
import { logger } from '../utils/logging';
+import { EventCategories } from '../analytics';
import {
HardwareWalletDevicesType,
HardwareWalletLocalData,
@@ -526,6 +527,12 @@ export default class HardwareWalletsStore extends Store {
isVotingRegistrationTransaction,
}
);
+
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Transaction made',
+ 'Hardware wallet'
+ );
} else {
this.setTransactionPendingState(false);
}
@@ -1276,6 +1283,7 @@ export default class HardwareWalletsStore extends Store {
this.unfinishedWalletAddressVerification = address;
this.hwDeviceStatus = HwDeviceStatuses.CONNECTING;
});
+
const walletId = get(this.stores.wallets, ['active', 'id']);
const hardwareWalletConnectionData = get(
this.hardwareWalletsConnectionData,
@@ -1473,6 +1481,11 @@ export default class HardwareWalletsStore extends Store {
);
this.showAddress(params);
}
+
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Verified wallet address with hardware wallet'
+ );
} else {
runInAction(
'HardwareWalletsStore:: Address Verified but not correct',
diff --git a/source/renderer/app/stores/NetworkStatusStore.ts b/source/renderer/app/stores/NetworkStatusStore.ts
index 4f6998e1cd..3c14077ec0 100644
--- a/source/renderer/app/stores/NetworkStatusStore.ts
+++ b/source/renderer/app/stores/NetworkStatusStore.ts
@@ -42,6 +42,7 @@ import type { CheckDiskSpaceResponse } from '../../../common/types/no-disk-space
import { TlsCertificateNotValidError } from '../api/nodes/errors';
import { openLocalDirectoryChannel } from '../ipc/open-local-directory';
import { toggleRTSFlagsModeChannel } from '../ipc/toggleRTSFlagsModeChannel';
+import { EventCategories } from '../analytics';
// DEFINE CONSTANTS -------------------------
const NETWORK_STATUS = {
@@ -431,6 +432,10 @@ export default class NetworkStatusStore extends Store {
// DEFINE ACTIONS
@action _toggleRTSFlagsMode = async () => {
+ this.analytics.sendEvent(
+ EventCategories.SETTINGS,
+ `RTS flags ${this.isRTSFlagsModeEnabled ? 'disabled' : 'enabled'}`
+ );
await toggleRTSFlagsModeChannel.send();
};
diff --git a/source/renderer/app/stores/ProfileStore.ts b/source/renderer/app/stores/ProfileStore.ts
index ea8c53ee06..57c0b5e549 100644
--- a/source/renderer/app/stores/ProfileStore.ts
+++ b/source/renderer/app/stores/ProfileStore.ts
@@ -1,8 +1,6 @@
-import { action, observable, computed, runInAction } from 'mobx';
+import { action, computed, observable, runInAction } from 'mobx';
import BigNumber from 'bignumber.js';
-import { includes, camelCase } from 'lodash';
-import axios from 'axios';
-import { v4 as uuidv4 } from 'uuid';
+import { camelCase, includes } from 'lodash';
import { toJS } from '../../../common/utils/helper';
import Store from './lib/Store';
import Request from './lib/LocalizedRequest';
@@ -15,35 +13,35 @@ import { logger } from '../utils/logging';
import { setStateSnapshotLogChannel } from '../ipc/setStateSnapshotLogChannel';
import { getDesktopDirectoryPathChannel } from '../ipc/getDesktopDirectoryPathChannel';
import { getSystemLocaleChannel } from '../ipc/getSystemLocaleChannel';
+import type { Locale } from '../../../common/types/locales.types';
import { LOCALES } from '../../../common/types/locales.types';
import {
compressLogsChannel,
downloadLogsChannel,
getLogsChannel,
} from '../ipc/logs.ipc';
-import type { LogFiles, CompressedLogStatus } from '../types/LogTypes';
+import type { CompressedLogStatus, LogFiles } from '../types/LogTypes';
import type { StateSnapshotLogParams } from '../../../common/types/logging.types';
-import type { Locale } from '../../../common/types/locales.types';
import {
DEFAULT_NUMBER_FORMAT,
NUMBER_FORMATS,
} from '../../../common/types/number.types';
import {
+ getRequestKeys,
hasLoadedRequest,
isRequestSet,
requestGetter,
requestGetterLocale,
- getRequestKeys,
} from '../utils/storesUtils';
import {
- NUMBER_OPTIONS,
DATE_ENGLISH_OPTIONS,
DATE_JAPANESE_OPTIONS,
- TIME_OPTIONS,
+ NUMBER_OPTIONS,
PROFILE_SETTINGS,
+ TIME_OPTIONS,
} from '../config/profileConfig';
import { buildSystemInfo } from '../utils/buildSystemInfo';
-import { AnalyticsAcceptanceStatus } from '../analytics/types';
+import { AnalyticsAcceptanceStatus, EventCategories } from '../analytics/types';
export default class ProfileStore extends Store {
@observable
@@ -173,8 +171,7 @@ export default class ProfileStore extends Store {
this._updateBigNumberFormat,
this._redirectToInitialSettingsIfNoLocaleSet,
this._redirectToAnalyticsScreenIfNotConfirmed,
- this._redirectToTermsOfUseScreenIfTermsNotAccepted, // this._redirectToDataLayerMigrationScreenIfMigrationHasNotAccepted,
- this._redirectToMainUiAfterAnalyticsAreConfirmed,
+ this._redirectToTermsOfUseScreenIfTermsNotAccepted,
this._redirectToMainUiAfterTermsAreAccepted,
this._redirectToMainUiAfterDataLayerMigrationIsAccepted,
]);
@@ -308,10 +305,7 @@ export default class ProfileStore extends Store {
@computed
get analyticsAcceptanceStatus(): AnalyticsAcceptanceStatus {
- return (
- this.getAnalyticsAcceptanceRequest.result ||
- AnalyticsAcceptanceStatus.PENDING
- );
+ return this.getAnalyticsAcceptanceRequest.result;
}
@computed
@@ -371,12 +365,20 @@ export default class ProfileStore extends Store {
// @ts-ignore ts-migrate(2339) FIXME: Property 'stores' does not exist on type 'ProfileS... Remove this comment to see the full error message
this.stores.wallets.refreshWalletsData();
}
+
+ this.analytics.sendEvent(
+ EventCategories.SETTINGS,
+ 'Changed user settings',
+ param
+ );
};
_updateTheme = async ({ theme }: { theme: string }) => {
// @ts-ignore ts-migrate(1320) FIXME: Type of 'await' operand must either be a valid pro... Remove this comment to see the full error message
await this.setThemeRequest.execute(theme);
// @ts-ignore ts-migrate(1320) FIXME: Type of 'await' operand must either be a valid pro... Remove this comment to see the full error message
await this.getThemeRequest.execute();
+
+ this.analytics.sendEvent(EventCategories.SETTINGS, 'Changed theme', theme);
};
_acceptTermsOfUse = async () => {
// @ts-ignore ts-migrate(1320) FIXME: Type of 'await' operand must either be a valid pro... Remove this comment to see the full error message
@@ -388,8 +390,24 @@ export default class ProfileStore extends Store {
this.getTermsOfUseAcceptanceRequest.execute();
};
_setAnalyticsAcceptanceStatus = (status: AnalyticsAcceptanceStatus) => {
+ const previousStatus = this.analyticsAcceptanceStatus;
+
this.setAnalyticsAcceptanceRequest.execute(status);
this.getAnalyticsAcceptanceRequest.execute();
+
+ if (status === AnalyticsAcceptanceStatus.ACCEPTED) {
+ this.analytics.enableTracking();
+ } else if (status === AnalyticsAcceptanceStatus.REJECTED) {
+ this.analytics.disableTracking();
+ }
+
+ if (previousStatus === AnalyticsAcceptanceStatus.PENDING) {
+ this._redirectToRoot();
+ } else {
+ this.actions.router.goToRoute.trigger({
+ route: ROUTES.SETTINGS.SUPPORT,
+ });
+ }
};
_getAnalyticsAcceptance = () => {
this.getAnalyticsAcceptanceRequest.execute();
@@ -495,14 +513,6 @@ export default class ProfileStore extends Store {
this._redirectToRoot();
}
};
- _redirectToMainUiAfterAnalyticsAreConfirmed = () => {
- if (
- this.analyticsAcceptanceStatus !== AnalyticsAcceptanceStatus.PENDING &&
- this._isOnAnalyticsPage()
- ) {
- this._redirectToRoot();
- }
- };
_redirectToMainUiAfterDataLayerMigrationIsAccepted = () => {
if (
this.isDataLayerMigrationAccepted &&
diff --git a/source/renderer/app/stores/SidebarStore.spec.ts b/source/renderer/app/stores/SidebarStore.spec.ts
index ad1ba96246..718898aedc 100644
--- a/source/renderer/app/stores/SidebarStore.spec.ts
+++ b/source/renderer/app/stores/SidebarStore.spec.ts
@@ -4,6 +4,7 @@ import type { ActionsMap } from '../actions/index';
import { WalletSortBy, WalletSortOrder } from '../types/sidebarTypes';
import type { SidebarWalletType } from '../types/sidebarTypes';
import SidebarStore from './SidebarStore';
+import { noopAnalyticsTracker } from '../analytics';
describe('Sidebar Store', () => {
const api: Api = {
@@ -22,7 +23,7 @@ describe('Sidebar Store', () => {
isLegacy?: boolean;
}>;
}) {
- const sidebarStore = new SidebarStore(api, actions);
+ const sidebarStore = new SidebarStore(api, actions, noopAnalyticsTracker);
sidebarStore.stores = {
wallets: {
all: wallets,
diff --git a/source/renderer/app/stores/SidebarStore.ts b/source/renderer/app/stores/SidebarStore.ts
index 702f8288fd..1310ca29c2 100644
--- a/source/renderer/app/stores/SidebarStore.ts
+++ b/source/renderer/app/stores/SidebarStore.ts
@@ -1,5 +1,5 @@
import { action, computed, observable } from 'mobx';
-import { get } from 'lodash';
+import { debounce, get } from 'lodash';
import { sidebarConfig } from '../config/sidebarConfig';
import type { SidebarCategoryInfo } from '../config/sidebarConfig';
import type {
@@ -10,6 +10,7 @@ import type {
import { WalletSortBy, WalletSortOrder } from '../types/sidebarTypes';
import { changeWalletSorting, sortWallets } from '../utils/walletSorting';
import Store from './lib/Store';
+import { EventCategories } from '../analytics';
export default class SidebarStore extends Store {
@observable
@@ -97,10 +98,16 @@ export default class SidebarStore extends Store {
sortOrder: this.walletSortConfig.sortOrder,
currentSortBy: this.walletSortConfig.sortBy,
});
+
+ this.analytics.sendEvent(
+ EventCategories.LAYOUT,
+ 'Changed wallet sorting settings'
+ );
};
@action
onSearchValueUpdated = (searchValue: string) => {
this.searchValue = searchValue;
+ this._sendSearchAnalyticsEvent();
};
@action
_configureCategories = () => {
@@ -175,14 +182,17 @@ export default class SidebarStore extends Store {
@action
_showSubMenus = () => {
this.isShowingSubMenus = true;
+ this.analytics.sendEvent(EventCategories.LAYOUT, 'Toggled submenu');
};
@action
_hideSubMenus = () => {
this.isShowingSubMenus = false;
+ this.analytics.sendEvent(EventCategories.LAYOUT, 'Toggled submenu');
};
@action
_toggleSubMenus = () => {
this.isShowingSubMenus = !this.isShowingSubMenus;
+ this.analytics.sendEvent(EventCategories.LAYOUT, 'Toggled submenu');
};
_syncSidebarRouteWithRouter = () => {
const route = this.stores.app.currentRoute;
@@ -199,4 +209,7 @@ export default class SidebarStore extends Store {
this._configureCategories();
}
};
+ _sendSearchAnalyticsEvent = debounce(() => {
+ this.analytics.sendEvent(EventCategories.LAYOUT, 'Used wallet search');
+ }, 5000);
}
diff --git a/source/renderer/app/stores/StakingStore.ts b/source/renderer/app/stores/StakingStore.ts
index 704fa767c6..fb33de3e34 100644
--- a/source/renderer/app/stores/StakingStore.ts
+++ b/source/renderer/app/stores/StakingStore.ts
@@ -1,7 +1,7 @@
import { computed, action, observable, runInAction } from 'mobx';
import BigNumber from 'bignumber.js';
import path from 'path';
-import { orderBy, find, map, get } from 'lodash';
+import { orderBy, find, map, get, debounce } from 'lodash';
import Store from './lib/Store';
import Request from './lib/LocalizedRequest';
import { ROUTES } from '../routes-config';
@@ -36,6 +36,7 @@ import { showSaveDialogChannel } from '../ipc/show-file-dialog-channels';
import { generateFileNameWithTimestamp } from '../../../common/utils/files';
import type { RedeemItnRewardsStep } from '../types/stakingTypes';
import type { CsvFileContent } from '../../../common/types/csv-request.types';
+import { EventCategories } from '../analytics';
export default class StakingStore extends Store {
@observable
@@ -227,10 +228,20 @@ export default class StakingStore extends Store {
_setSelectedDelegationWalletId = (walletId: string) => {
this.selectedDelegationWalletId = walletId;
};
+
+ _sendStakePoolsSliderUsedAnalyticsEvent = debounce(() => {
+ this.analytics.sendEvent(
+ EventCategories.STAKE_POOLS,
+ 'Used stake pools amount slider'
+ );
+ }, 5000);
+
@action
_setStake = (stake: number) => {
this.stake = stake;
+ this._sendStakePoolsSliderUsedAnalyticsEvent();
};
+
@action
_rankStakePools = () => {
this.isRanking = true;
@@ -266,6 +277,11 @@ export default class StakingStore extends Store {
// Update
// @ts-ignore ts-migrate(2339) FIXME: Property 'api' does not exist on type 'StakingStor... Remove this comment to see the full error message
await this.api.localStorage.setSmashServer(smashServerUrl);
+ this.analytics.sendEvent(
+ EventCategories.SETTINGS,
+ 'Changed SMASH server',
+ smashServerUrl
+ );
} catch (error) {
runInAction(() => {
this.smashServerUrlError = error;
@@ -383,6 +399,13 @@ export default class StakingStore extends Store {
setTimeout(() => {
this.resetStakePoolTransactionChecker();
}, STAKE_POOL_TRANSACTION_CHECKER_TIMEOUT);
+
+ const wallet = this.stores.wallets.getWalletById(walletId);
+
+ this.analytics.sendEvent(
+ EventCategories.STAKE_POOLS,
+ wallet.isDelegating ? 'Redelegated a wallet' : 'Delegated a wallet'
+ );
} catch (error) {
this.resetStakePoolTransactionChecker();
throw error;
@@ -507,6 +530,10 @@ export default class StakingStore extends Store {
});
// @ts-ignore ts-migrate(2339) FIXME: Property 'actions' does not exist on type 'Staking... Remove this comment to see the full error message
this.actions.staking.requestCSVFileSuccess.trigger();
+ this.analytics.sendEvent(
+ EventCategories.STAKE_POOLS,
+ 'Exported rewards as CSV'
+ );
};
calculateDelegationFee = async (
delegationFeeRequest: GetDelegationFeeRequest
diff --git a/source/renderer/app/stores/TransactionsStore.ts b/source/renderer/app/stores/TransactionsStore.ts
index 8f003da4f6..ab480dd3b0 100644
--- a/source/renderer/app/stores/TransactionsStore.ts
+++ b/source/renderer/app/stores/TransactionsStore.ts
@@ -28,6 +28,7 @@ import {
isTransactionInFilterRange,
} from '../utils/transaction';
import type { ApiTokens } from '../api/assets/types';
+import { EventCategories } from '../analytics';
const INITIAL_SEARCH_LIMIT = null; // 'null' value stands for 'load all'
@@ -367,6 +368,11 @@ export default class TransactionsStore extends Store {
...currentFilterOptions,
...filterOptions,
};
+
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Set transaction filters'
+ );
return true;
};
@action
@@ -376,6 +382,7 @@ export default class TransactionsStore extends Store {
this._filterOptionsForWallets[wallet.id] = {
...emptyTransactionFilterOptions,
};
+
return true;
};
@action
@@ -402,8 +409,13 @@ export default class TransactionsStore extends Store {
getAsset,
isInternalAddress,
});
- // @ts-ignore ts-migrate(2554) FIXME: Expected 1 arguments, but got 0.
- if (success) actions.transactions.requestCSVFileSuccess.trigger();
+ if (success) {
+ actions.transactions.requestCSVFileSuccess.trigger();
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Exported transactions as CSV'
+ );
+ }
};
@action
_createExternalTransaction = async (signedTransactionBlob: Buffer) => {
diff --git a/source/renderer/app/stores/UiDialogsStore.ts b/source/renderer/app/stores/UiDialogsStore.ts
index 5585eef1b0..064cb8dc87 100644
--- a/source/renderer/app/stores/UiDialogsStore.ts
+++ b/source/renderer/app/stores/UiDialogsStore.ts
@@ -1,6 +1,10 @@
-import { ReactNode } from 'react';
+import { Component, ReactNode } from 'react';
import { observable, action } from 'mobx';
import Store from './lib/Store';
+import WalletReceiveDialog from '../components/wallet/receive/WalletReceiveDialog';
+import AssetSettingsDialog from '../components/assets/AssetSettingsDialog';
+import DelegationSetupWizardDialog from '../components/staking/delegation-setup-wizard/DelegationSetupWizardDialog';
+import { EventCategories } from '../analytics';
export default class UiDialogsStore extends Store {
@observable
@@ -25,7 +29,7 @@ export default class UiDialogsStore extends Store {
countdownSinceDialogOpened = (countDownTo: number) =>
Math.max(countDownTo - this.secondsSinceActiveDialogIsOpen, 0);
@action
- _onOpen = ({ dialog }: { dialog: (...args: Array) => any }) => {
+ _onOpen = ({ dialog }: { dialog: object }) => {
this._reset();
this.activeDialog = dialog;
@@ -34,6 +38,8 @@ export default class UiDialogsStore extends Store {
this.secondsSinceActiveDialogIsOpen = 0;
if (this._secondsTimerInterval) clearInterval(this._secondsTimerInterval);
this._secondsTimerInterval = setInterval(this._updateSeconds, 1000);
+
+ this._handleAnalytics(dialog);
};
@action
_onClose = () => {
@@ -53,4 +59,31 @@ export default class UiDialogsStore extends Store {
this.secondsSinceActiveDialogIsOpen = 0;
this.dataForActiveDialog = {};
};
+ _handleAnalytics = (dialog: object) => {
+ switch (dialog) {
+ case WalletReceiveDialog:
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Opened share wallet address modal'
+ );
+ break;
+
+ case AssetSettingsDialog:
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Opened native token settings'
+ );
+ break;
+
+ case DelegationSetupWizardDialog:
+ this.analytics.sendEvent(
+ EventCategories.STAKE_POOLS,
+ 'Opened delegate wallet dialog'
+ );
+ break;
+
+ default:
+ break;
+ }
+ };
}
diff --git a/source/renderer/app/stores/VotingStore.spec.ts b/source/renderer/app/stores/VotingStore.spec.ts
index 89d0fb292b..6d049c494a 100644
--- a/source/renderer/app/stores/VotingStore.spec.ts
+++ b/source/renderer/app/stores/VotingStore.spec.ts
@@ -2,6 +2,7 @@ import type { Api } from '../api/index';
import type { ActionsMap } from '../actions/index';
import VotingStore, { FundPhase } from './VotingStore';
import type { CatalystFund } from '../api/voting/types';
+import { noopAnalyticsTracker } from '../analytics';
const mockFundInfo = {
current: {
@@ -17,6 +18,7 @@ describe('VotingStore', () => {
ada: jest.fn(),
} as any;
const actions: ActionsMap = jest.fn() as any;
+
const cases = [
[undefined, null],
[
@@ -40,7 +42,7 @@ describe('VotingStore', () => {
],
[mockFundInfo.current.resultsTime, FundPhase.RESULTS],
];
- const votingStore = new VotingStore(api, actions);
+ const votingStore = new VotingStore(api, actions, noopAnalyticsTracker);
beforeAll(() => {
votingStore.catalystFund = mockFundInfo as CatalystFund;
diff --git a/source/renderer/app/stores/VotingStore.ts b/source/renderer/app/stores/VotingStore.ts
index fcbb497998..d64ae61c6f 100644
--- a/source/renderer/app/stores/VotingStore.ts
+++ b/source/renderer/app/stores/VotingStore.ts
@@ -25,6 +25,7 @@ import type {
VotingMetadataType,
} from '../api/transactions/types';
import type { CatalystFund } from '../api/voting/types';
+import { EventCategories } from '../analytics';
export type VotingRegistrationKeyType = {
bytes: (...args: Array) => any;
@@ -409,6 +410,7 @@ export default class VotingStore extends Store {
this._setQrCode(formattedArrayBufferToHexString(encrypt));
this._nextRegistrationStep();
+ this.analytics.sendEvent(EventCategories.VOTING, 'Registered for voting');
};
_saveAsPDF = async () => {
const { qrCode, selectedWalletId } = this;
diff --git a/source/renderer/app/stores/WalletMigrationStore.ts b/source/renderer/app/stores/WalletMigrationStore.ts
index a593f1e5e7..ee07ac5f5a 100644
--- a/source/renderer/app/stores/WalletMigrationStore.ts
+++ b/source/renderer/app/stores/WalletMigrationStore.ts
@@ -29,6 +29,7 @@ import {
import { IMPORT_WALLET_STEPS } from '../config/walletRestoreConfig';
import { IS_AUTOMATIC_WALLET_MIGRATION_ENABLED } from '../config/walletsConfig';
import type { ImportWalletStep } from '../types/walletRestoreTypes';
+import { EventCategories } from '../analytics';
export type WalletMigrationStatus =
| 'unstarted'
@@ -311,6 +312,11 @@ export default class WalletMigrationStore extends Store {
runInAction('update isRestorationRunning', () => {
this.isRestorationRunning = false;
});
+
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Restored legacy wallet(s)'
+ );
};
@action
_restoreWallet = async (exportedWallet: ExportedByronWallet) => {
diff --git a/source/renderer/app/stores/WalletSettingsStore.ts b/source/renderer/app/stores/WalletSettingsStore.ts
index 10ab29743b..46ae6116c5 100644
--- a/source/renderer/app/stores/WalletSettingsStore.ts
+++ b/source/renderer/app/stores/WalletSettingsStore.ts
@@ -10,6 +10,7 @@ import { getRawWalletId } from '../api/utils';
import type { WalletExportToFileParams } from '../actions/wallet-settings-actions';
import type { WalletUtxos } from '../api/wallets/types';
import { RECOVERY_PHRASE_VERIFICATION_STATUSES } from '../config/walletRecoveryPhraseVerificationConfig';
+import { EventCategories } from '../analytics';
import { WalletLocalData } from '../types/localDataTypes';
export default class WalletSettingsStore extends Store {
@@ -156,6 +157,11 @@ export default class WalletSettingsStore extends Store {
this.actions.dialogs.closeActiveDialog.trigger();
this.updateSpendingPasswordRequest.reset();
this.stores.wallets.refreshWalletsData();
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Changed wallet settings',
+ 'password'
+ );
};
@action
_updateWalletField = async ({
@@ -189,6 +195,11 @@ export default class WalletSettingsStore extends Store {
});
this.updateWalletRequest.reset();
this.stores.wallets.refreshWalletsData();
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Changed wallet settings',
+ field
+ );
};
@action
_exportToFile = async (params: WalletExportToFileParams) => {
@@ -279,6 +290,11 @@ export default class WalletSettingsStore extends Store {
const isCorrect = walletId === activeWalletId;
const nextStep = isCorrect ? 3 : 4;
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Verified recovery phrase'
+ );
+
if (isCorrect) {
const recoveryPhraseVerificationDate = new Date();
await this.actions.walletsLocal.setWalletLocalData.trigger({
diff --git a/source/renderer/app/stores/WalletsStore.ts b/source/renderer/app/stores/WalletsStore.ts
index f7f4799e0a..2c29904715 100644
--- a/source/renderer/app/stores/WalletsStore.ts
+++ b/source/renderer/app/stores/WalletsStore.ts
@@ -57,6 +57,8 @@ import type {
HardwareWalletExtendedPublicKeyResponse,
} from '../../../common/types/hardware-wallets.types';
import { NetworkMagics } from '../../../common/types/cardano-node.types';
+import { EventCategories } from '../analytics';
+import { getEventNameFromWallet } from '../analytics/utils/getEventNameFromWallet';
/* eslint-disable consistent-return */
/**
@@ -365,6 +367,10 @@ export default class WalletsStore extends Store {
runInAction('update account public key', () => {
this.activePublicKey = accountPublicKey;
});
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Revealed wallet public key'
+ );
} catch (error) {
throw error;
}
@@ -397,6 +403,10 @@ export default class WalletsStore extends Store {
runInAction('update ICO public key', () => {
this.icoPublicKey = icoPublicKey;
});
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Revealed wallet multi-signature public key'
+ );
} catch (error) {
throw error;
}
@@ -464,8 +474,13 @@ export default class WalletsStore extends Store {
this.goToWalletRoute(restoredWallet.id);
this.refreshWalletsData();
- this._restoreWalletResetRequests();
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Restored a software wallet',
+ this.walletKind
+ );
+ this._restoreWalletResetRequests();
this._restoreWalletResetData();
}
};
@@ -619,6 +634,10 @@ export default class WalletsStore extends Store {
this.goToWalletRoute(wallet.id);
this.refreshWalletsData();
this.actions.dialogs.closeActiveDialog.trigger();
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Created a new hardware wallet'
+ );
}
} catch (error) {
throw error;
@@ -639,6 +658,10 @@ export default class WalletsStore extends Store {
this.actions.dialogs.closeActiveDialog.trigger();
this.goToWalletRoute(wallet.id);
this.refreshWalletsData();
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Created a new software wallet'
+ );
}
};
_deleteWallet = async (params: { walletId: string; isLegacy: boolean }) => {
@@ -681,6 +704,13 @@ export default class WalletsStore extends Store {
});
}
});
+
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Wallet deleted',
+ getEventNameFromWallet(walletToDelete)
+ );
+
this.actions.dialogs.closeActiveDialog.trigger();
this.actions.walletsLocal.unsetWalletLocalData.trigger({
walletId: params.walletId,
@@ -817,6 +847,11 @@ export default class WalletsStore extends Store {
assets: formattedAssets,
hasAssetsRemainingAfterTransaction,
});
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Transaction made',
+ 'Software wallet'
+ );
this.refreshWalletsData();
this.actions.dialogs.closeActiveDialog.trigger();
this.sendMoneyRequest.reset();
@@ -1479,10 +1514,12 @@ export default class WalletsStore extends Store {
note,
address,
filePath,
+ wallet,
}: {
note: string;
address: string;
filePath: string;
+ wallet: Wallet;
}) => {
const {
currentLocale,
@@ -1508,6 +1545,11 @@ export default class WalletsStore extends Store {
this.actions.wallets.generateAddressPDFSuccess.trigger({
walletAddress,
});
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Saved wallet address as PDF',
+ getEventNameFromWallet(wallet)
+ );
} catch (error) {
throw new Error(error);
}
@@ -1515,9 +1557,11 @@ export default class WalletsStore extends Store {
_saveQRCodeImage = async ({
address,
filePath,
+ wallet,
}: {
address: string;
filePath: string;
+ wallet: Wallet;
}) => {
try {
await saveQRCodeImageChannel.send({
@@ -1528,6 +1572,11 @@ export default class WalletsStore extends Store {
this.actions.wallets.saveQRCodeImageSuccess.trigger({
walletAddress,
});
+ this.analytics.sendEvent(
+ EventCategories.WALLETS,
+ 'Saved wallet address as QR code',
+ getEventNameFromWallet(wallet)
+ );
} catch (error) {
throw new Error(error);
}
diff --git a/source/renderer/app/stores/index.ts b/source/renderer/app/stores/index.ts
index f19c566371..56a5e6e958 100644
--- a/source/renderer/app/stores/index.ts
+++ b/source/renderer/app/stores/index.ts
@@ -24,6 +24,9 @@ import WalletSettingsStore from './WalletSettingsStore';
import WalletsLocalStore from './WalletsLocalStore';
import WalletsStore from './WalletsStore';
import WindowStore from './WindowStore';
+import { AnalyticsTracker } from '../analytics';
+import { Api } from '../api';
+import { ActionsMap } from '../actions';
export const storeClasses = {
addresses: AddressesStore,
@@ -82,12 +85,17 @@ function executeOnEveryStore(fn: (store: Store) => void) {
});
} // Set up and return the stores for this app -> also used to reset all stores to defaults
-export default action(
- (api, actions, router): StoresMap => {
+export const setUpStores = action(
+ (
+ api: Api,
+ actions: ActionsMap,
+ router: RouterStore,
+ analyticsTracker: AnalyticsTracker
+ ): StoresMap => {
function createStoreInstanceOf(
StoreSubClass: Class
): T {
- return new StoreSubClass(api, actions);
+ return new StoreSubClass(api, actions, analyticsTracker);
}
// Teardown existing stores
diff --git a/source/renderer/app/stores/lib/Store.ts b/source/renderer/app/stores/lib/Store.ts
index 6dee796702..d53f15e9bc 100644
--- a/source/renderer/app/stores/lib/Store.ts
+++ b/source/renderer/app/stores/lib/Store.ts
@@ -3,18 +3,18 @@ import type { ActionsMap } from '../../actions/index';
import type { StoresMap } from '../index';
import type { Api } from '../../api/index';
import type { Environment } from '../../../../common/types/environment.types';
+import { AnalyticsTracker } from '../../analytics';
export default class Store {
stores: StoresMap;
- api: Api;
- actions: ActionsMap;
environment: Environment = global.environment;
_reactions: Array = [];
- constructor(api: Api, actions: ActionsMap) {
- this.api = api;
- this.actions = actions;
- }
+ constructor(
+ protected api: Api,
+ protected actions: ActionsMap,
+ protected analytics: AnalyticsTracker
+ ) {}
registerReactions(reactions: Array<(...args: Array) => any>) {
reactions.forEach((reaction) =>
diff --git a/source/renderer/index.ejs b/source/renderer/index.ejs
index 7ee5a3f289..e627e30fa1 100644
--- a/source/renderer/index.ejs
+++ b/source/renderer/index.ejs
@@ -13,7 +13,7 @@
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
- connect-src 'self' https://*.matomo.cloud/;
+ connect-src 'self' https://matomo.cw.iog.io/ http://localhost:8080;
"
>
Daedalus
diff --git a/storybook/stories/common/Widgets.stories.tsx b/storybook/stories/common/Widgets.stories.tsx
index a8ac8c0543..f5eb7c7d76 100644
--- a/storybook/stories/common/Widgets.stories.tsx
+++ b/storybook/stories/common/Widgets.stories.tsx
@@ -23,6 +23,7 @@ import ButtonLink from '../../../source/renderer/app/components/widgets/ButtonLi
import NormalSwitch from '../../../source/renderer/app/components/widgets/forms/NormalSwitch';
import { Separator } from '../../../source/renderer/app/components/widgets/separator/Separator';
import { CollapsibleSection } from '../../../source/renderer/app/components/widgets/collapsible-section/CollapsibleSection';
+import { VerticalSeparator } from '../../../source/renderer/app/components/wallet/widgets/VerticalSeparator';
const { intl: enIntl } = new IntlProvider({
locale: 'en-US',
@@ -250,4 +251,9 @@ storiesOf('Common / Widgets', module)
+ ))
+ .add('VerticalSeparator', () => (
+
+
+
));
diff --git a/storybook/stories/wallets/send/WalletSend.stories.tsx b/storybook/stories/wallets/send/WalletSend.stories.tsx
index 5f43d24b25..27b0d9e727 100644
--- a/storybook/stories/wallets/send/WalletSend.stories.tsx
+++ b/storybook/stories/wallets/send/WalletSend.stories.tsx
@@ -19,6 +19,7 @@ import Wallet, {
import WalletSendForm from '../../../../source/renderer/app/components/wallet/WalletSendForm';
import type { WalletTokens } from '../../../../source/renderer/app/api/assets/types';
import { WalletSendConfirmationDialogView } from '../../../../source/renderer/app/containers/wallet/dialogs/send-confirmation/SendConfirmation.view';
+import { noopAnalyticsTracker as analyticsTracker } from '../../../../source/renderer/app/analytics';
const allAssets = [
generateAssetToken(
@@ -232,6 +233,7 @@ storiesOf('Wallets / Send', module)
walletName="My wallet"
onTokenPickerDialogClose={action('onTokenPickerDialogClose')}
onTokenPickerDialogOpen={action('onTokenPickerDialogOpen')}
+ analyticsTracker={analyticsTracker}
/>
))
.add('Send - Hardware wallet verifying transaction', () => (
@@ -260,6 +262,7 @@ storiesOf('Wallets / Send', module)
walletName="My wallet"
onTokenPickerDialogClose={action('onTokenPickerDialogClose')}
onTokenPickerDialogOpen={action('onTokenPickerDialogOpen')}
+ analyticsTracker={analyticsTracker}
/>
))
.add('Send - Hardware wallet verifying transaction succeeded', () => (
@@ -288,6 +291,7 @@ storiesOf('Wallets / Send', module)
walletName="My wallet"
onTokenPickerDialogClose={action('onTokenPickerDialogClose')}
onTokenPickerDialogOpen={action('onTokenPickerDialogOpen')}
+ analyticsTracker={analyticsTracker}
/>
))
.add('Send - Hardware wallet verifying transaction failed', () => (
@@ -316,6 +320,7 @@ storiesOf('Wallets / Send', module)
walletName="My wallet"
onTokenPickerDialogClose={action('onTokenPickerDialogClose')}
onTokenPickerDialogOpen={action('onTokenPickerDialogOpen')}
+ analyticsTracker={analyticsTracker}
/>
))
.add('Send - With Assets', () => (
@@ -347,6 +352,7 @@ storiesOf('Wallets / Send', module)
walletName="My wallet"
onTokenPickerDialogClose={action('onTokenPickerDialogClose')}
onTokenPickerDialogOpen={action('onTokenPickerDialogOpen')}
+ analyticsTracker={analyticsTracker}
/>
))
.add('Wallet Send Confirmation Dialog With Assets', () => {
diff --git a/translations/messages.json b/translations/messages.json
index db054205c9..18a4cebc9c 100644
--- a/translations/messages.json
+++ b/translations/messages.json
@@ -2677,15 +2677,20 @@
"id": "settings.support.steps.reportProblem.link"
},
{
- "defaultMessage": "!!!You have opted in to analytics data collection. You can {changeAnalyticsSettingsLink}.",
+ "defaultMessage": "!!!You have opted in to analytics data collection. You can {changeAnalyticsSettingsLink}.",
"description": "Analytics data collection description when user opted in",
"id": "analytics.form.analyticsAcceptedDescription"
},
{
- "defaultMessage": "!!!You have opted out of analytics data collection. You can {changeAnalyticsSettingsLink}.",
+ "defaultMessage": "!!!You have opted out of analytics data collection. You can {changeAnalyticsSettingsLink}.",
"description": "Analytics data collection description when user opted out ",
"id": "analytics.form.analyticsDeclinedDescription"
},
+ {
+ "defaultMessage": "!!!You can {changeAnalyticsSettingsLink}.",
+ "description": "Change analytics settings link",
+ "id": "analytics.form.changeAnalyticsSettings"
+ },
{
"defaultMessage": "!!!change this setting here",
"description": "Change analytics settings link text",
@@ -2714,7 +2719,7 @@
{
"defaultMessage": "!!!Skip",
"description": "Analytics data collection skip button text",
- "id": "analytics.dialog.skipButton"
+ "id": "analytics.form.disallowButton"
},
{
"defaultMessage": "!!!Daedalus Privacy Policy",
diff --git a/yarn.lock b/yarn.lock
index 5421857215..6263079d33 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3093,14 +3093,7 @@
"@types/react" "*"
"@types/react-router" "*"
-"@types/react-router@*":
- version "5.1.18"
- resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.18.tgz#c8851884b60bc23733500d86c1266e1cfbbd9ef3"
- dependencies:
- "@types/history" "^4.7.11"
- "@types/react" "*"
-
-"@types/react-router@5.1.18":
+"@types/react-router@*", "@types/react-router@5.1.18":
version "5.1.18"
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.18.tgz#c8851884b60bc23733500d86c1266e1cfbbd9ef3"
dependencies:
@@ -11218,9 +11211,9 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
-lodash-es@4.17.21:
- version "4.17.21"
- resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+lodash-es@4.17.15:
+ version "4.17.15"
+ resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
lodash._reinterpolate@^3.0.0:
version "3.0.0"
@@ -11563,6 +11556,10 @@ mathml-tag-names@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
+matomo-tracker@2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/matomo-tracker/-/matomo-tracker-2.2.4.tgz#ee397d915d7b2e7964996ca28a0a03f4f0692453"
+
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"