Skip to content

Commit

Permalink
feat(mobile): upgrade FW report to analytics
Browse files Browse the repository at this point in the history
  • Loading branch information
Nodonisko committed Jan 12, 2025
1 parent 126fd7e commit 2baa553
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 6 deletions.
4 changes: 4 additions & 0 deletions suite-native/analytics/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ export enum EventType {
SendFlowExited = 'send/flow_exited',
DeviceSettingsPinProtectionChange = 'device_settings/pin_protection_change',
DeviceSettingsAuthenticityCheck = 'device_settings/authenticity_check',
FirmwareUpdateStarted = 'firmware/firmware_update_started',
FirmwareUpdateCancel = 'firmware/firmware_update_cancel',
FirmwareUpdateFinished = 'firmware/firmware_update_finished',
FirmwareUpdateStucked = 'firmware/firmware_update_stucked',
}
32 changes: 31 additions & 1 deletion suite-native/analytics/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import { FeeLevelLabel, TokenAddress, TokenSymbol } from '@suite-common/wallet-t
import { DeviceModelInternal, VersionArray } from '@trezor/connect';

import { EventType } from './constants';
import { AnalyticsSendFlowStep, DeviceAuthenticityCheckResult } from './types';
import {
AnalyticsSendFlowStep,
DeviceAuthenticityCheckResult,
FirmwareUpdatePayload,
FirmwareUpdateStartType,
FirmwareUpdateStuckedState,
} from './types';

export type SuiteNativeAnalyticsEvent =
| {
Expand Down Expand Up @@ -316,4 +322,28 @@ export type SuiteNativeAnalyticsEvent =
payload: {
result: DeviceAuthenticityCheckResult;
};
}
| {
type: EventType.FirmwareUpdateStarted;
payload: FirmwareUpdatePayload & {
startType: FirmwareUpdateStartType;
};
}
| {
type: EventType.FirmwareUpdateCancel;
payload: FirmwareUpdatePayload;
}
| {
type: EventType.FirmwareUpdateFinished;
payload: FirmwareUpdatePayload & {
duration: number;
error?: string;
};
}
| {
type: EventType.FirmwareUpdateStucked;
payload: FirmwareUpdatePayload & {
duration: number;
state: FirmwareUpdateStuckedState;
};
};
14 changes: 14 additions & 0 deletions suite-native/analytics/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DeviceModelInternal, FirmwareType } from '@trezor/connect';

export type AnalyticsSendFlowStep =
| 'address_and_amount'
| 'fee_settings'
Expand All @@ -6,3 +8,15 @@ export type AnalyticsSendFlowStep =
| 'destination_tag_review';

export type DeviceAuthenticityCheckResult = 'successful' | 'compromised' | 'cancelled' | 'failed';

export type FirmwareUpdatePayload = {
model: DeviceModelInternal;
fromBootloaderVersion: string;
fromFwVersion: string;
toFwVersion: string;
fromFwType: FirmwareType | 'none';
toFwType: FirmwareType;
};

export type FirmwareUpdateStuckedState = 'modalPart1' | 'modalPart2' | 'buttonVisible';
export type FirmwareUpdateStartType = 'normal' | 'retry';
20 changes: 18 additions & 2 deletions suite-native/firmware/src/components/MayBeStuckedBottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import Animated, { FadeIn } from 'react-native-reanimated';

import { BottomSheet, Box, Button, NumberedListItem, Text, VStack } from '@suite-native/atoms';
import { Translation } from '@suite-native/intl';
import { FirmwareUpdateStuckedState } from '@suite-native/analytics';

type MayBeStuckedBottomSheetProps = {
isOpened: boolean;
onClose: () => void;
onAnalyticsReportStucked: (state: FirmwareUpdateStuckedState) => void;
};

export const MayBeStuckedBottomSheet = ({ isOpened, onClose }: MayBeStuckedBottomSheetProps) => {
export const MayBeStuckedBottomSheet = ({
isOpened,
onClose,
onAnalyticsReportStucked,
}: MayBeStuckedBottomSheetProps) => {
const [visiblePart, setVisiblePart] = useState<1 | 2>(1);

const handleClose = () => {
onClose();
setVisiblePart(1);
};

useEffect(() => {
if (isOpened) {
if (visiblePart === 1) {
onAnalyticsReportStucked('modalPart1');
} else if (visiblePart === 2) {
onAnalyticsReportStucked('modalPart2');
}
}
}, [visiblePart, onAnalyticsReportStucked, isOpened]);

return (
<BottomSheet
isVisible={isOpened}
Expand Down
8 changes: 7 additions & 1 deletion suite-native/firmware/src/hooks/useFirmware.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TxKeyPath, useTranslate } from '@suite-native/intl';
import { setPriorityMode } from '@trezor/react-native-usb';

import { nativeFirmwareActions } from '../nativeFirmwareSlice';
import { useFirmwareAnalytics } from './useFirmwareAnalytics';

// If progress doesn't change for 1 minute
const MAYBE_STUCKED_TIMEOUT = 1 * 60 * 1000; // 1 minute
Expand All @@ -28,6 +29,10 @@ export const useFirmware = (params: UseFirmwareInstallationParams) => {
const { translate } = useTranslate();
const [mayBeStucked, setMayBeStucked] = useState(false);
const mayBeStuckedTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
const { handleAnalyticsReportStucked } = useFirmwareAnalytics({
device: firmwareInstallation.originalDevice,
targetFirmwareType: firmwareInstallation.targetFirmwareType,
});

const setIsFirmwareInstallationRunning = useCallback(
(isRunning: boolean) => {
Expand All @@ -46,9 +51,10 @@ export const useFirmware = (params: UseFirmwareInstallationParams) => {
const setMayBeStuckedTimeout = useCallback(() => {
resetMayBeStuckedTimeout();
mayBeStuckedTimeout.current = setTimeout(() => {
handleAnalyticsReportStucked('buttonVisible');
setMayBeStucked(true);
}, MAYBE_STUCKED_TIMEOUT);
}, [resetMayBeStuckedTimeout]);
}, [resetMayBeStuckedTimeout, handleAnalyticsReportStucked]);

useEffect(() => {
if (status === 'started' && progress < 100) {
Expand Down
112 changes: 112 additions & 0 deletions suite-native/firmware/src/hooks/useFirmwareAnalytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useCallback, useEffect, useRef } from 'react';

import { DeviceModelInternal, FirmwareType } from '@trezor/connect';
import {
analytics,
EventType,
FirmwareUpdatePayload,
FirmwareUpdateStartType,
} from '@suite-native/analytics';
import { TrezorDevice } from '@suite-common/suite-types';
import { getFirmwareVersion, getBootloaderVersion } from '@trezor/device-utils';

export const useFirmwareAnalytics = ({
device,
targetFirmwareType,
}: {
device?: TrezorDevice;
targetFirmwareType: FirmwareType;
}) => {
const prepareAnalyticsPayload = useCallback(() => {

Check failure on line 20 in suite-native/firmware/src/hooks/useFirmwareAnalytics.ts

View workflow job for this annotation

GitHub Actions / Linting and formatting

Unexpected block statement surrounding arrow body; parenthesize the returned value and move it immediately after the `=>`
return {
model: device?.features?.internal_model ?? DeviceModelInternal.UNKNOWN,
fromBootloaderVersion: getBootloaderVersion(device),
fromFwVersion: device?.firmware === 'none' ? 'none' : getFirmwareVersion(device),
toFwVersion: targetFirmwareType,
fromFwType: (device?.firmwareType || 'none') as FirmwareType | 'none',
toFwType: targetFirmwareType,
};
}, [device, targetFirmwareType]);

// Use refs to avoid any re-renders because of analytics and to make useCallback dependencies stable
// so it won't trigger any useEffect which could interfere with other business logic.
const analyticsPayload = useRef<FirmwareUpdatePayload>(prepareAnalyticsPayload());
const timeStarted = useRef<number>(Date.now());

useEffect(() => {
analyticsPayload.current = prepareAnalyticsPayload();
}, [prepareAnalyticsPayload]);

const getElapsedTimeInSeconds = useCallback(() => {

Check failure on line 40 in suite-native/firmware/src/hooks/useFirmwareAnalytics.ts

View workflow job for this annotation

GitHub Actions / Linting and formatting

Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`
return Math.floor((Date.now() - timeStarted.current) / 1000);
}, []);

const getAnalyticsPayload = useCallback(() => {

Check failure on line 44 in suite-native/firmware/src/hooks/useFirmwareAnalytics.ts

View workflow job for this annotation

GitHub Actions / Linting and formatting

Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`
return analyticsPayload.current;
}, [analyticsPayload]);

const resetTimeStarted = useCallback(() => {
timeStarted.current = Date.now();
}, []);

const handleAnalyticsReportStarted = useCallback(
({ startType }: { startType: FirmwareUpdateStartType }) => {
// Q: Maybe we should reset timeStarted only if it's not a retry?
resetTimeStarted();

analytics.report({
type: EventType.FirmwareUpdateStarted,
payload: {
...getAnalyticsPayload(),
startType,
},
});
},
[getAnalyticsPayload, resetTimeStarted],
);

const handleAnalyticsReportStucked = useCallback(
(state: 'modalPart1' | 'modalPart2' | 'buttonVisible') => {
analytics.report({
type: EventType.FirmwareUpdateStucked,
payload: {
...getAnalyticsPayload(),
duration: getElapsedTimeInSeconds(),
state,
},
});
},
[getElapsedTimeInSeconds, getAnalyticsPayload],
);

const handleAnalyticsReportFinished = useCallback(
({ error }: { error?: string } = {}) => {
analytics.report({
type: EventType.FirmwareUpdateFinished,
payload: {
...getAnalyticsPayload(),
duration: getElapsedTimeInSeconds(),
error,
},
});
},
[getElapsedTimeInSeconds, getAnalyticsPayload],
);

const handleAnalyticsReportCancelled = useCallback(() => {
analytics.report({
type: EventType.FirmwareUpdateCancel,
payload: getAnalyticsPayload(),
});
}, [getAnalyticsPayload]);

return {
getElapsedTimeInSeconds,
getAnalyticsPayload,
resetTimeStarted,
handleAnalyticsReportStucked,
handleAnalyticsReportFinished,
handleAnalyticsReportCancelled,
handleAnalyticsReportStarted,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
} from '../components/UpdateProgressIndicator';
import { useFirmware } from '../hooks/useFirmware';
import { MayBeStuckedBottomSheet } from '../components/MayBeStuckedBottomSheet';
import { useFirmwareAnalytics } from '../hooks/useFirmwareAnalytics';

type NavigationProp = StackNavigationProps<
DeviceSettingsStackParamList,
Expand Down Expand Up @@ -68,7 +69,18 @@ export const FirmwareUpdateInProgressScreen = () => {
resetReducer,
translatedText,
mayBeStucked,
originalDevice,
targetFirmwareType,
} = useFirmware({});
const {
handleAnalyticsReportFinished,
handleAnalyticsReportStucked,
handleAnalyticsReportCancelled,
handleAnalyticsReportStarted,
} = useFirmwareAnalytics({
device: originalDevice,
targetFirmwareType,
});
const openLink = useOpenLink();

useEffect(() => {
Expand Down Expand Up @@ -98,6 +110,8 @@ export const FirmwareUpdateInProgressScreen = () => {
const result = await firmwareUpdate();

if (!result) {
handleAnalyticsReportFinished({ error: 'Unknown error swallowed by redux.' });

// some error happened probably, handled in redux, we don't want to navigate anywhere
return;
}
Expand All @@ -106,12 +120,17 @@ export const FirmwareUpdateInProgressScreen = () => {
// Action cancelled on device
result.payload?.code === 'Failure_ActionCancelled'
) {
handleAnalyticsReportCancelled();
navigation.navigate(DeviceStackRoutes.FirmwareUpdate);
}

handleAnalyticsReportFinished({ error: result.payload?.error });

return;
}

handleAnalyticsReportFinished();

// wait few seconds to animation to finish and let user orientate little bit
setTimeout(() => {
// setting this to false will trigger standart device connection flow
Expand All @@ -123,13 +142,17 @@ export const FirmwareUpdateInProgressScreen = () => {
navigation,
handleFirmwareUpdateFinished,
firmwareUpdate,
handleAnalyticsReportFinished,
handleAnalyticsReportCancelled,
]);

const handleRetry = useCallback(async () => {
await TrezorConnect.cancel();
// We should put retry before resetReducer because then we may not have information about the device
handleAnalyticsReportStarted({ startType: 'retry' });
resetReducer();
startFirmwareUpdate();
}, [startFirmwareUpdate, resetReducer]);
}, [startFirmwareUpdate, resetReducer, handleAnalyticsReportStarted]);

const openMayBeStuckedBottomSheet = useCallback(() => {
setIsMayBeStuckedBottomSheetOpened(true);
Expand All @@ -146,11 +169,12 @@ export const FirmwareUpdateInProgressScreen = () => {
useEffect(() => {
// Small delay to let initial screen animation finish
const timeout = setTimeout(() => {
handleAnalyticsReportStarted({ startType: 'normal' });
startFirmwareUpdate();
}, 2000);

return () => clearTimeout(timeout);
}, [startFirmwareUpdate]);
}, [startFirmwareUpdate, handleAnalyticsReportStarted]);

const isError = status === 'error';

Expand Down Expand Up @@ -238,6 +262,7 @@ export const FirmwareUpdateInProgressScreen = () => {
<MayBeStuckedBottomSheet
isOpened={isMayBeStuckedBottomSheetOpened}
onClose={closeMayBeStuckedBottomSheet}
onAnalyticsReportStucked={handleAnalyticsReportStucked}
/>
</Screen>
);
Expand Down

0 comments on commit 2baa553

Please sign in to comment.