From 820a02bfd68977263a5316a396090020c89c4117 Mon Sep 17 00:00:00 2001 From: "Nalivaiko, Aleksej" Date: Fri, 2 Feb 2024 14:46:48 +0300 Subject: [PATCH 1/5] M2-4997: Mixpanel Tracking: Applet ID in event properties ; M2-5076: [Mobile] Mixpanel Tracking: Applet ID in event properties ; --- src/app/index.tsx | 4 +- src/app/ui/AppProvider/AnalyticsProvider.tsx | 19 ++++ .../ui/AppProvider/SystemBootUpProvider.tsx | 2 +- src/app/ui/AppProvider/index.tsx | 48 +++++--- .../activity/lib/hooks/useRetryUpload.ts | 8 +- .../activity/ui/UploadRetryBanner.tsx | 4 +- .../applet/model/hooks/useStartEntity.ts | 105 ++++++++++++------ src/features/login/ui/LoginForm.tsx | 3 +- .../model/hooks/useRegistrationMutation.ts | 4 +- src/screens/ui/ActivityListScreen.tsx | 23 ++-- src/screens/ui/AppletDataScreen.tsx | 12 ++ src/screens/ui/AppletsScreen.tsx | 22 +++- src/shared/lib/analytics/AnalyticsService.ts | 33 +++++- src/shared/lib/analytics/MixpanelAnalytics.ts | 4 +- src/shared/lib/analytics/index.ts | 2 + src/shared/lib/hooks/index.ts | 1 + src/shared/lib/hooks/useOnFocus.ts | 18 +++ src/widgets/survey/ui/Finish.tsx | 6 +- src/widgets/survey/ui/Intermediate.tsx | 6 +- 19 files changed, 249 insertions(+), 75 deletions(-) create mode 100644 src/app/ui/AppProvider/AnalyticsProvider.tsx create mode 100644 src/shared/lib/hooks/useOnFocus.ts diff --git a/src/app/index.tsx b/src/app/index.tsx index c122e45f6..fc603e8f6 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -3,7 +3,7 @@ import { LogBox } from 'react-native'; import { RootNavigator } from '@screens'; import localization from '@jobs/localization'; -import { Logger, jobRunner, AnalyticsService } from '@shared/lib'; +import { Logger, jobRunner } from '@shared/lib'; import { AppProvider } from './ui'; @@ -18,8 +18,6 @@ jobRunner.runAll([localization]); Logger.configure(); -AnalyticsService.init(); - const App = () => { return ( diff --git a/src/app/ui/AppProvider/AnalyticsProvider.tsx b/src/app/ui/AppProvider/AnalyticsProvider.tsx new file mode 100644 index 000000000..a274e214d --- /dev/null +++ b/src/app/ui/AppProvider/AnalyticsProvider.tsx @@ -0,0 +1,19 @@ +import { FC, PropsWithChildren, useEffect } from 'react'; + +import { AnalyticsService, Logger, useSystemBootUp } from '@app/shared/lib'; + +const AnalyticsProvider: FC = ({ children }) => { + const { onModuleInitialized } = useSystemBootUp(); + + useEffect(() => { + AnalyticsService.init().then(() => { + Logger.log('[AnalyticsProvider]: Initialized'); + + onModuleInitialized('analytics'); + }); + }, [onModuleInitialized]); + + return <>{children}; +}; + +export default AnalyticsProvider; diff --git a/src/app/ui/AppProvider/SystemBootUpProvider.tsx b/src/app/ui/AppProvider/SystemBootUpProvider.tsx index 2b39867ea..b588529d7 100644 --- a/src/app/ui/AppProvider/SystemBootUpProvider.tsx +++ b/src/app/ui/AppProvider/SystemBootUpProvider.tsx @@ -9,7 +9,7 @@ type Props = PropsWithChildren<{ const SYSTEM_BOOT_UP_DELAY = 300; function SystemBootUpProvider({ children, onLoadingFinished }: Props) { - const moduleWaitList = useRef(['cache', 'state']); + const moduleWaitList = useRef(['cache', 'state', 'analytics']); const onLoadingFinishedRef = useRef(onLoadingFinished); diff --git a/src/app/ui/AppProvider/index.tsx b/src/app/ui/AppProvider/index.tsx index feee90001..44e1f3339 100644 --- a/src/app/ui/AppProvider/index.tsx +++ b/src/app/ui/AppProvider/index.tsx @@ -7,7 +7,9 @@ import { Dirs } from 'react-native-file-access'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { LocalizationProvider } from '@app/entities/localization'; +import { AnalyticsService, Logger, MixEvents } from '@app/shared/lib'; +import AnalyticsProvider from './AnalyticsProvider'; import NavigationProvider from './NavigationProvider'; import ReactQueryProvider from './ReactQueryProvider'; import ReduxProvider from './ReduxProvider'; @@ -27,26 +29,36 @@ CacheManager.config = { const AppProvider: FC = ({ children }) => { const [isBootingUp, setIsBootingUp] = useState(true); + const onLoadingFinished = () => { + Logger.log('[AppProvider]: App loaded'); + + AnalyticsService.track(MixEvents.AppOpen); + + setIsBootingUp(false); + }; + return ( - setIsBootingUp(false)}> - - - - - - - - - {children} - - - - - - - - + + + + + + + + + + + {children} + + + + + + + + + ); diff --git a/src/entities/activity/lib/hooks/useRetryUpload.ts b/src/entities/activity/lib/hooks/useRetryUpload.ts index 4696e266c..cda50d5e8 100644 --- a/src/entities/activity/lib/hooks/useRetryUpload.ts +++ b/src/entities/activity/lib/hooks/useRetryUpload.ts @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { AnalyticsService } from '@shared/lib'; +import { AnalyticsService, MixEvents, MixProperties } from '@shared/lib'; import { showUploadErrorAlert } from '../alerts'; import { UploadObservable } from '../observables'; @@ -9,6 +9,7 @@ type Input = { retryUpload: () => Promise; onPostpone?: () => void; onSuccess?: () => void; + appletId: string; }; type Result = { @@ -20,6 +21,7 @@ export const useRetryUpload = ({ retryUpload, onPostpone: postpone, onSuccess: success, + appletId, }: Input): Result => { const [isAlertOpened, setIsAlertOpened] = useState(false); @@ -28,7 +30,9 @@ export const useRetryUpload = ({ showUploadErrorAlert({ onRetry: async () => { - AnalyticsService.track('Retry button pressed'); + AnalyticsService.track(MixEvents.RetryButtonPressed, { + [MixProperties.AppletId]: appletId, + }); try { setIsAlertOpened(false); diff --git a/src/entities/activity/ui/UploadRetryBanner.tsx b/src/entities/activity/ui/UploadRetryBanner.tsx index 33da24720..d18603ea4 100644 --- a/src/entities/activity/ui/UploadRetryBanner.tsx +++ b/src/entities/activity/ui/UploadRetryBanner.tsx @@ -4,7 +4,7 @@ import { AccessibilityProps } from 'react-native'; import { useTranslation } from 'react-i18next'; import { Box, Button, Text } from '@app/shared/ui'; -import { AnalyticsService } from '@shared/lib'; +import { AnalyticsService, MixEvents } from '@shared/lib'; import useQueueProcessing from '../lib/hooks/useQueueProcessing'; @@ -16,7 +16,7 @@ const UploadRetryBanner: FC = () => { const { t } = useTranslation(); const onRetry = () => { - AnalyticsService.track('Retry button pressed'); + AnalyticsService.track(MixEvents.RetryButtonPressed); process(); }; diff --git a/src/entities/applet/model/hooks/useStartEntity.ts b/src/entities/applet/model/hooks/useStartEntity.ts index f985020b1..0778275f6 100644 --- a/src/entities/applet/model/hooks/useStartEntity.ts +++ b/src/entities/applet/model/hooks/useStartEntity.ts @@ -13,6 +13,8 @@ import { ILogger, isAppOnline, Logger, + MixEvents, + MixProperties, useAppDispatch, useAppletInfo, useAppSelector, @@ -125,6 +127,39 @@ function useStartEntity({ }); }; + const appletName = getAppletDisplayName(appletId); + + const logStart = () => { + logger.log( + `[useStartEntity.startActivity]: Activity "${entityName}|${activityId}" started, applet "${appletName}|${appletId}"`, + ); + AnalyticsService.track(MixEvents.AssessmentStarted, { + [MixProperties.AppletId]: appletId, + }); + }; + const logRestart = () => { + logger.log( + `[useStartEntity.startActivity]: Activity "${entityName}|${activityId}" restarted, applet "${appletName}|${appletId}"`, + ); + AnalyticsService.track(MixEvents.ActivityRestart, { + [MixProperties.AppletId]: appletId, + }); + AnalyticsService.track(MixEvents.AssessmentStarted, { + [MixProperties.AppletId]: appletId, + }); + }; + const logResume = () => { + logger.log( + `[useStartEntity.startActivity]: Activity "${entityName}|${activityId}" resumed, applet "${appletName}|${appletId}"`, + ); + AnalyticsService.track(MixEvents.ActivityResume, { + [MixProperties.AppletId]: appletId, + }); + AnalyticsService.track(MixEvents.AssessmentStarted, { + [MixProperties.AppletId]: appletId, + }); + }; + return new Promise(resolve => { if (shouldBreakDueToMediaReferences()) { onMediaReferencesFound(); @@ -148,8 +183,6 @@ function useStartEntity({ getProgress(appletId, activityId, eventId), ); - const appletName = getAppletDisplayName(appletId); - if (isActivityInProgress) { if (isTimerElapsed) { resolve({ startedFromScratch: false }); @@ -159,27 +192,17 @@ function useStartEntity({ onBeforeStartingActivity({ onRestart: () => { cleanUpMediaFiles({ activityId, appletId, eventId, order: 0 }); - - logger.log( - `[useStartEntity.startActivity]: Activity "${entityName}|${activityId}" restarted, applet "${appletName}|${appletId}"`, - ); + logRestart(); activityStarted(appletId, activityId, eventId); resolve({ startedFromScratch: true }); }, onResume: () => { - logger.log( - `[useStartEntity.startActivity]: Activity "${entityName}|${activityId}" resumed, applet "${appletName}|${appletId}"`, - ); + logResume(); return resolve({ startedFromScratch: false }); }, }); } else { - logger.log( - `[useStartEntity.startActivity]: Activity "${entityName}|${activityId}" started, applet "${appletName}|${appletId}"`, - ); - - AnalyticsService.track('Assessment started'); - + logStart(); activityStarted(appletId, activityId, eventId); resolve({ startedFromScratch: true }); } @@ -230,6 +253,39 @@ function useStartEntity({ return flow.activityIds; }; + const appletName = getAppletDisplayName(appletId); + + const logStart = () => { + logger.log( + `[useStartEntity.startFlow]: Flow "${entityName}|${flowId}" started, applet "${appletName}|${appletId}"`, + ); + AnalyticsService.track(MixEvents.AssessmentStarted, { + [MixProperties.AppletId]: appletId, + }); + }; + const logRestart = () => { + logger.log( + `[useStartEntity.startFlow]: Flow "${entityName}|${flowId}" restarted, applet "${appletName}|${appletId}"`, + ); + AnalyticsService.track(MixEvents.ActivityRestart, { + [MixProperties.AppletId]: appletId, + }); + AnalyticsService.track(MixEvents.AssessmentStarted, { + [MixProperties.AppletId]: appletId, + }); + }; + const logResume = () => { + logger.log( + `[useStartEntity.startFlow]: Flow "${entityName}|${flowId}" resumed, applet "${appletName}|${appletId}"`, + ); + AnalyticsService.track(MixEvents.ActivityResume, { + [MixProperties.AppletId]: appletId, + }); + AnalyticsService.track(MixEvents.AssessmentStarted, { + [MixProperties.AppletId]: appletId, + }); + }; + const flowActivities: string[] = getFlowActivities(); const firstActivityId: string = flowActivities[0]; @@ -255,8 +311,6 @@ function useStartEntity({ getProgress(appletId, flowId, eventId), ); - const appletName = getAppletDisplayName(appletId); - if (isFlowInProgress) { if (isTimerElapsed) { resolve({ @@ -275,32 +329,21 @@ function useStartEntity({ order: i, }); } - logger.log( - `[useStartEntity.startFlow]: Flow "${entityName}|${flowId}" restarted, applet "${appletName}|${appletId}"`, - ); - + logRestart(); flowStarted(appletId, flowId, firstActivityId, eventId, 0); resolve({ startedFromScratch: true, }); }, onResume: () => { - logger.log( - `[useStartEntity.startFlow]: Flow "${entityName}|${flowId}" resumed, applet "${appletName}|${appletId}"`, - ); - + logResume(); return resolve({ startedFromScratch: false, }); }, }); } else { - logger.log( - `[useStartEntity.startFlow]: Flow "${entityName}|${flowId}" started, applet "${appletName}|${appletId}"`, - ); - - AnalyticsService.track('Assessment started'); - + logStart(); flowStarted(appletId, flowId, firstActivityId, eventId, 0); resolve({ startedFromScratch: true, diff --git a/src/features/login/ui/LoginForm.tsx b/src/features/login/ui/LoginForm.tsx index 6e42d4e0a..42e80df2b 100644 --- a/src/features/login/ui/LoginForm.tsx +++ b/src/features/login/ui/LoginForm.tsx @@ -12,6 +12,7 @@ import { SessionModel } from '@entities/session'; import { AnalyticsService, executeIfOnline, + MixEvents, useAppDispatch, useAppForm, useFormChanges, @@ -61,7 +62,7 @@ const LoginForm: FC = props => { SessionModel.storeSession(session); AnalyticsService.login(user.id).then(() => { - AnalyticsService.track('Login Successful'); + AnalyticsService.track(MixEvents.LoginSuccessful); }); props.onLoginSuccess(); diff --git a/src/features/sign-up/model/hooks/useRegistrationMutation.ts b/src/features/sign-up/model/hooks/useRegistrationMutation.ts index 07882d09f..7ecb8a766 100644 --- a/src/features/sign-up/model/hooks/useRegistrationMutation.ts +++ b/src/features/sign-up/model/hooks/useRegistrationMutation.ts @@ -9,7 +9,7 @@ import { IdentityModel, } from '@entities/identity'; import { SessionModel } from '@entities/session'; -import { AnalyticsService, useAppDispatch } from '@shared/lib'; +import { AnalyticsService, MixEvents, useAppDispatch } from '@shared/lib'; import { encryption } from '@shared/lib'; type UseRegistrationReturn = { @@ -49,7 +49,7 @@ export const useRegistrationMutation = ( SessionModel.storeSession(session); AnalyticsService.login(user.id).then(() => { - AnalyticsService.track('Signup Successful'); + AnalyticsService.track(MixEvents.SignupSuccessful); }); if (onSuccess) { diff --git a/src/screens/ui/ActivityListScreen.tsx b/src/screens/ui/ActivityListScreen.tsx index c2ef36dc2..2f5181a85 100644 --- a/src/screens/ui/ActivityListScreen.tsx +++ b/src/screens/ui/ActivityListScreen.tsx @@ -4,6 +4,12 @@ import { BottomTabScreenProps } from '@react-navigation/bottom-tabs'; import { useIsFocused } from '@react-navigation/core'; import { UploadRetryBanner } from '@app/entities/activity'; +import { + AnalyticsService, + MixEvents, + MixProperties, + useOnFocus, +} from '@app/shared/lib'; import { ActivityGroups } from '@app/widgets/activity-group'; import { StreamingStatusBar } from '@features/streaming'; import { AppletDetailsParamList } from '@screens/config'; @@ -12,21 +18,22 @@ import { Box, HorizontalCalendar } from '@shared/ui'; type Props = BottomTabScreenProps; const ActivityListScreen: FC = props => { + const appletId = props.route.params.appletId; + const isFocused = useIsFocused(); + useOnFocus(() => { + AnalyticsService.track(MixEvents.AppletView, { + [MixProperties.AppletId]: appletId, + }); + }); + return ( - - {isFocused && ( - - )} + {isFocused && } ); }; diff --git a/src/screens/ui/AppletDataScreen.tsx b/src/screens/ui/AppletDataScreen.tsx index ab7dcc729..0368a50bb 100644 --- a/src/screens/ui/AppletDataScreen.tsx +++ b/src/screens/ui/AppletDataScreen.tsx @@ -5,6 +5,12 @@ import { BottomTabScreenProps } from '@react-navigation/bottom-tabs'; import { UploadRetryBanner } from '@app/entities/activity'; import { ActivityAnalyticsList } from '@app/entities/applet'; import { useAppletAnalytics } from '@app/entities/applet/lib/hooks'; +import { + AnalyticsService, + MixEvents, + MixProperties, + useOnFocus, +} from '@app/shared/lib'; import { ActivityIndicator, Box, HorizontalCalendar } from '@app/shared/ui'; import { AppletDetailsParamList } from '@screens/config'; @@ -17,6 +23,12 @@ const AppletDataScreen: FC = ({ route }) => { const { analytics, isLoading } = useAppletAnalytics(appletId); + useOnFocus(() => { + AnalyticsService.track(MixEvents.DataView, { + [MixProperties.AppletId]: appletId, + }); + }); + if (isLoading) { return ( diff --git a/src/screens/ui/AppletsScreen.tsx b/src/screens/ui/AppletsScreen.tsx index 6e64ed916..eaebf0e59 100644 --- a/src/screens/ui/AppletsScreen.tsx +++ b/src/screens/ui/AppletsScreen.tsx @@ -8,7 +8,14 @@ import { StoreProgress } from '@app/abstract/lib'; import { UploadRetryBanner } from '@app/entities/activity'; import { NotificationModel } from '@app/entities/notification'; import { LogTrigger } from '@app/shared/api'; -import { Logger, useAppSelector } from '@app/shared/lib'; +import { + AnalyticsService, + Logger, + MixEvents, + MixProperties, + useAppSelector, + useOnFocus, +} from '@app/shared/lib'; import { Applet, AppletList, AppletModel } from '@entities/applet'; import { IdentityModel } from '@entities/identity'; import { AppletsRefresh, AppletsRefreshModel } from '@features/applets-refresh'; @@ -34,6 +41,10 @@ const AppletsScreen: FC = () => { } }, [t, userFirstName, setOptions]); + useOnFocus(() => { + AnalyticsService.track(MixEvents.HomeView); + }); + const queryClient = useQueryClient(); const storeProgress: StoreProgress = useAppSelector( @@ -43,8 +54,13 @@ const AppletsScreen: FC = () => { const completions = useAppSelector(AppletModel.selectors.selectCompletions); const navigateAppletDetails: (applet: SelectedApplet) => void = useCallback( - ({ id, displayName }) => - navigate('AppletDetails', { appletId: id, title: displayName }), + ({ id, displayName }) => { + navigate('AppletDetails', { appletId: id, title: displayName }); + + AnalyticsService.track(MixEvents.AppletSelected, { + [MixProperties.AppletId]: id, + }); + }, [navigate], ); diff --git a/src/shared/lib/analytics/AnalyticsService.ts b/src/shared/lib/analytics/AnalyticsService.ts index 7e25937e5..75aa0f185 100644 --- a/src/shared/lib/analytics/AnalyticsService.ts +++ b/src/shared/lib/analytics/AnalyticsService.ts @@ -1,5 +1,6 @@ import MixpanelAnalytics from './MixpanelAnalytics'; import { ENV, MIXPANEL_TOKEN } from '../constants'; +import { Logger } from '../services'; import { createStorage } from '../storages'; const isProduction = !ENV; @@ -11,17 +12,47 @@ export interface IAnalyticsService { track(action: string, payload?: Record): void; login(id: string): Promise; logout(): void; + init(): Promise; } let service: IAnalyticsService; +export const MixProperties = { + AppletId: 'Applet ID', +}; + +export const MixEvents = { + DataView: 'Data View', + AppletView: 'Applet View', + HomeView: 'Home Page View', + AssessmentStarted: "'Assessment started'", + RetryButtonPressed: 'Retry button pressed', + LoginSuccessful: 'Login Successful', + SignupSuccessful: 'Signup Successful', + AppOpen: 'App Open', + ActivityRestart: 'Activity Restart Button Pressed', + ActivityResume: 'Activity Resume Button Pressed', + AppletSelected: 'Applet Selected', +}; + const AnalyticsService = { - init() { + async init(): Promise { if (shouldEnableMixpanel && MIXPANEL_TOKEN) { service = new MixpanelAnalytics(MIXPANEL_TOKEN); + return service.init(); } }, track(action: string, payload?: Record) { + if (payload) { + Logger.log( + `[AnalyticsService]: Action: ${action}, payload: ${JSON.stringify( + payload, + )}`, + ); + } else { + Logger.log('[AnalyticsService]: Action: ' + action); + } + if (shouldEnableMixpanel) { service.track(`[Mobile] ${action}`, payload); } diff --git a/src/shared/lib/analytics/MixpanelAnalytics.ts b/src/shared/lib/analytics/MixpanelAnalytics.ts index 65db2a9cc..d2609306d 100644 --- a/src/shared/lib/analytics/MixpanelAnalytics.ts +++ b/src/shared/lib/analytics/MixpanelAnalytics.ts @@ -7,8 +7,10 @@ class MixpanelAnalytics implements IAnalyticsService { constructor(projectToken: string) { this.mixpanel = new Mixpanel(projectToken, false); + } - this.mixpanel.init(); + init(): Promise { + return this.mixpanel.init(); } track(action: string, payload?: Record | undefined): void { diff --git a/src/shared/lib/analytics/index.ts b/src/shared/lib/analytics/index.ts index ef607d57e..e81128588 100644 --- a/src/shared/lib/analytics/index.ts +++ b/src/shared/lib/analytics/index.ts @@ -1 +1,3 @@ export { default as AnalyticsService } from './AnalyticsService'; +export { MixProperties } from './AnalyticsService'; +export { MixEvents } from './AnalyticsService'; diff --git a/src/shared/lib/hooks/index.ts b/src/shared/lib/hooks/index.ts index 119ca3576..5487fc747 100644 --- a/src/shared/lib/hooks/index.ts +++ b/src/shared/lib/hooks/index.ts @@ -22,5 +22,6 @@ export { default as useCurrentRoute } from './useCurrentRoute'; export { default as useCachedImage } from './useCachedImage'; export { default as useCallbacksRefs } from './useCallbacksRefs'; export { default as useCachedItem } from './useCachedItem'; +export { default as useOnFocus } from './useOnFocus'; export * from './redux'; diff --git a/src/shared/lib/hooks/useOnFocus.ts b/src/shared/lib/hooks/useOnFocus.ts new file mode 100644 index 000000000..6b5714b35 --- /dev/null +++ b/src/shared/lib/hooks/useOnFocus.ts @@ -0,0 +1,18 @@ +import { useEffect, useRef } from 'react'; + +import { useIsFocused } from '@react-navigation/native'; + +const useOnFocus = (callback: () => void) => { + const isFocused = useIsFocused(); + + const callbackRef = useRef(callback); + callbackRef.current = callback; + + useEffect(() => { + if (isFocused) { + callbackRef.current(); + } + }, [isFocused]); +}; + +export default useOnFocus; diff --git a/src/widgets/survey/ui/Finish.tsx b/src/widgets/survey/ui/Finish.tsx index 31422fe8d..c79be68ac 100644 --- a/src/widgets/survey/ui/Finish.tsx +++ b/src/widgets/survey/ui/Finish.tsx @@ -18,6 +18,7 @@ import { useActivityInfo, useAppDispatch, useAppSelector, + MixProperties, } from '@shared/lib'; import { Center, ImageBackground, Text, Button } from '@shared/ui'; @@ -102,6 +103,7 @@ function FinishItem({ const { isAlertOpened: isRetryAlertOpened, openAlert: openRetryAlert } = useRetryUpload({ retryUpload: processQueue, + appletId, }); let finishReason: FinishReason = isTimerElapsed ? 'time-is-up' : 'regular'; @@ -198,7 +200,9 @@ function FinishItem({ clearActivityStorageRecord(); - AnalyticsService.track('Assessment completed'); + AnalyticsService.track('Assessment completed', { + [MixProperties.AppletId]: appletId, + }); const success = await processQueue(); diff --git a/src/widgets/survey/ui/Intermediate.tsx b/src/widgets/survey/ui/Intermediate.tsx index bd8c89e9e..47817483a 100644 --- a/src/widgets/survey/ui/Intermediate.tsx +++ b/src/widgets/survey/ui/Intermediate.tsx @@ -26,6 +26,7 @@ import { useActivityInfo, useAppDispatch, useAppSelector, + MixProperties, } from '@app/shared/lib'; import { badge } from '@assets/images'; import { Center, YStack, Text, Button, Image, XStack } from '@shared/ui'; @@ -156,13 +157,16 @@ function Intermediate({ changeActivity(); onFinish(); }, + appletId, }); const { getName: getActivityName } = useActivityInfo(); const changeActivity = useCallback(() => { if (!nextActivity) { - AnalyticsService.track('Assessment completed'); + AnalyticsService.track('Assessment completed', { + [MixProperties.AppletId]: appletId, + }); return; } From a390267241263b26574995405b97412e953021d1 Mon Sep 17 00:00:00 2001 From: "Nalivaiko, Aleksej" Date: Tue, 6 Feb 2024 11:32:18 +0300 Subject: [PATCH 2/5] PR fixes --- .../model/tests/ScoresCalculator.collectMaxScores.test.ts | 1 + .../model/tests/ScoresCalculator.collectScoreForRadio.test.ts | 1 + src/shared/lib/analytics/AnalyticsService.ts | 3 ++- src/widgets/survey/ui/Finish.tsx | 3 ++- src/widgets/survey/ui/Intermediate.tsx | 3 ++- 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/features/pass-survey/model/tests/ScoresCalculator.collectMaxScores.test.ts b/src/features/pass-survey/model/tests/ScoresCalculator.collectMaxScores.test.ts index 23ae745a7..6e30f5686 100644 --- a/src/features/pass-survey/model/tests/ScoresCalculator.collectMaxScores.test.ts +++ b/src/features/pass-survey/model/tests/ScoresCalculator.collectMaxScores.test.ts @@ -59,6 +59,7 @@ const getEmptyRadioItem = (name: string): RadioPipelineItem => { setAlerts: false, setPalette: false, options: [], + autoAdvance: false, }, type: 'Radio', }; diff --git a/src/features/pass-survey/model/tests/ScoresCalculator.collectScoreForRadio.test.ts b/src/features/pass-survey/model/tests/ScoresCalculator.collectScoreForRadio.test.ts index 7c95bccf5..0a7695066 100644 --- a/src/features/pass-survey/model/tests/ScoresCalculator.collectScoreForRadio.test.ts +++ b/src/features/pass-survey/model/tests/ScoresCalculator.collectScoreForRadio.test.ts @@ -34,6 +34,7 @@ const getEmptyItem = (): RadioPipelineItem => { setAlerts: false, setPalette: false, options: [], + autoAdvance: false, }, type: 'Radio', }; diff --git a/src/shared/lib/analytics/AnalyticsService.ts b/src/shared/lib/analytics/AnalyticsService.ts index 75aa0f185..dc6d21aad 100644 --- a/src/shared/lib/analytics/AnalyticsService.ts +++ b/src/shared/lib/analytics/AnalyticsService.ts @@ -25,7 +25,8 @@ export const MixEvents = { DataView: 'Data View', AppletView: 'Applet View', HomeView: 'Home Page View', - AssessmentStarted: "'Assessment started'", + AssessmentStarted: 'Assessment started', + AssessmentCompleted: 'Assessment completed', RetryButtonPressed: 'Retry button pressed', LoginSuccessful: 'Login Successful', SignupSuccessful: 'Signup Successful', diff --git a/src/widgets/survey/ui/Finish.tsx b/src/widgets/survey/ui/Finish.tsx index c79be68ac..cad0e60e4 100644 --- a/src/widgets/survey/ui/Finish.tsx +++ b/src/widgets/survey/ui/Finish.tsx @@ -19,6 +19,7 @@ import { useAppDispatch, useAppSelector, MixProperties, + MixEvents, } from '@shared/lib'; import { Center, ImageBackground, Text, Button } from '@shared/ui'; @@ -200,7 +201,7 @@ function FinishItem({ clearActivityStorageRecord(); - AnalyticsService.track('Assessment completed', { + AnalyticsService.track(MixEvents.AssessmentCompleted, { [MixProperties.AppletId]: appletId, }); diff --git a/src/widgets/survey/ui/Intermediate.tsx b/src/widgets/survey/ui/Intermediate.tsx index 47817483a..25143e5c5 100644 --- a/src/widgets/survey/ui/Intermediate.tsx +++ b/src/widgets/survey/ui/Intermediate.tsx @@ -27,6 +27,7 @@ import { useAppDispatch, useAppSelector, MixProperties, + MixEvents, } from '@app/shared/lib'; import { badge } from '@assets/images'; import { Center, YStack, Text, Button, Image, XStack } from '@shared/ui'; @@ -164,7 +165,7 @@ function Intermediate({ const changeActivity = useCallback(() => { if (!nextActivity) { - AnalyticsService.track('Assessment completed', { + AnalyticsService.track(MixEvents.AssessmentCompleted, { [MixProperties.AppletId]: appletId, }); return; From 157880013c543dc99f27c3ca563d403f9a2712e6 Mon Sep 17 00:00:00 2001 From: "Nalivaiko, Aleksej" Date: Tue, 6 Feb 2024 14:07:10 +0300 Subject: [PATCH 3/5] PR fix --- src/shared/lib/hooks/useOnFocus.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/shared/lib/hooks/useOnFocus.ts b/src/shared/lib/hooks/useOnFocus.ts index 6b5714b35..791746bdb 100644 --- a/src/shared/lib/hooks/useOnFocus.ts +++ b/src/shared/lib/hooks/useOnFocus.ts @@ -1,18 +1,14 @@ -import { useEffect, useRef } from 'react'; +import { useCallback, useRef } from 'react'; -import { useIsFocused } from '@react-navigation/native'; +import { useFocusEffect } from '@react-navigation/native'; const useOnFocus = (callback: () => void) => { - const isFocused = useIsFocused(); - const callbackRef = useRef(callback); callbackRef.current = callback; - useEffect(() => { - if (isFocused) { - callbackRef.current(); - } - }, [isFocused]); + const focusHandler = useCallback(() => callbackRef.current(), []); + + useFocusEffect(focusHandler); }; export default useOnFocus; From ee81d3cd3880acc7fd3854712f77550834cb6058 Mon Sep 17 00:00:00 2001 From: "Nalivaiko, Aleksej" Date: Tue, 13 Feb 2024 11:45:05 +0300 Subject: [PATCH 4/5] Some fixes re. discussions in the M2-4997 --- src/entities/activity/lib/hooks/useRetryUpload.ts | 8 ++------ src/entities/applet/model/hooks/useStartEntity.ts | 6 ------ src/features/pass-survey/ui/ActivityStepper.tsx | 8 ++++++-- src/shared/lib/analytics/AnalyticsService.ts | 1 + src/widgets/survey/ui/Finish.tsx | 1 - src/widgets/survey/ui/FlowElementSwitch.tsx | 14 ++++++++++++-- src/widgets/survey/ui/Intermediate.tsx | 1 - 7 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/entities/activity/lib/hooks/useRetryUpload.ts b/src/entities/activity/lib/hooks/useRetryUpload.ts index cda50d5e8..631814bfa 100644 --- a/src/entities/activity/lib/hooks/useRetryUpload.ts +++ b/src/entities/activity/lib/hooks/useRetryUpload.ts @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { AnalyticsService, MixEvents, MixProperties } from '@shared/lib'; +import { AnalyticsService, MixEvents } from '@shared/lib'; import { showUploadErrorAlert } from '../alerts'; import { UploadObservable } from '../observables'; @@ -9,7 +9,6 @@ type Input = { retryUpload: () => Promise; onPostpone?: () => void; onSuccess?: () => void; - appletId: string; }; type Result = { @@ -21,7 +20,6 @@ export const useRetryUpload = ({ retryUpload, onPostpone: postpone, onSuccess: success, - appletId, }: Input): Result => { const [isAlertOpened, setIsAlertOpened] = useState(false); @@ -30,9 +28,7 @@ export const useRetryUpload = ({ showUploadErrorAlert({ onRetry: async () => { - AnalyticsService.track(MixEvents.RetryButtonPressed, { - [MixProperties.AppletId]: appletId, - }); + AnalyticsService.track(MixEvents.RetryButtonPressed); try { setIsAlertOpened(false); diff --git a/src/entities/applet/model/hooks/useStartEntity.ts b/src/entities/applet/model/hooks/useStartEntity.ts index 0778275f6..92fb8a6f9 100644 --- a/src/entities/applet/model/hooks/useStartEntity.ts +++ b/src/entities/applet/model/hooks/useStartEntity.ts @@ -155,9 +155,6 @@ function useStartEntity({ AnalyticsService.track(MixEvents.ActivityResume, { [MixProperties.AppletId]: appletId, }); - AnalyticsService.track(MixEvents.AssessmentStarted, { - [MixProperties.AppletId]: appletId, - }); }; return new Promise(resolve => { @@ -281,9 +278,6 @@ function useStartEntity({ AnalyticsService.track(MixEvents.ActivityResume, { [MixProperties.AppletId]: appletId, }); - AnalyticsService.track(MixEvents.AssessmentStarted, { - [MixProperties.AppletId]: appletId, - }); }; const flowActivities: string[] = getFlowActivities(); diff --git a/src/features/pass-survey/ui/ActivityStepper.tsx b/src/features/pass-survey/ui/ActivityStepper.tsx index 4b19c59b7..e6b1def98 100644 --- a/src/features/pass-survey/ui/ActivityStepper.tsx +++ b/src/features/pass-survey/ui/ActivityStepper.tsx @@ -31,7 +31,7 @@ type Props = { idleTimer: HourMinute | null; entityStartedAt: number; timer: HourMinute | null; - onClose: () => void; + onClose: (reason: 'regular' | 'click-on-return') => void; onFinish: (reason: 'regular' | 'idle') => void; }; @@ -207,6 +207,10 @@ function ActivityStepper({ restartIdleTimer(); }; + const onStartReached = () => { + onClose('click-on-return'); + }; + const onEndReached = (isForced: boolean) => { if (!isForced) { trackUserAction(userActionCreator.done()); @@ -233,7 +237,7 @@ function ActivityStepper({ onBack={onBack} onBeforeNext={onBeforeNext} onBeforeBack={onBeforeBack} - onStartReached={onClose} + onStartReached={onStartReached} onEndReached={onEndReached} onUndo={onUndo} > diff --git a/src/shared/lib/analytics/AnalyticsService.ts b/src/shared/lib/analytics/AnalyticsService.ts index dc6d21aad..46e4372f7 100644 --- a/src/shared/lib/analytics/AnalyticsService.ts +++ b/src/shared/lib/analytics/AnalyticsService.ts @@ -34,6 +34,7 @@ export const MixEvents = { ActivityRestart: 'Activity Restart Button Pressed', ActivityResume: 'Activity Resume Button Pressed', AppletSelected: 'Applet Selected', + ReturnToActivitiesPressed: 'Return to Activities pressed', }; const AnalyticsService = { diff --git a/src/widgets/survey/ui/Finish.tsx b/src/widgets/survey/ui/Finish.tsx index cad0e60e4..6c76d0c31 100644 --- a/src/widgets/survey/ui/Finish.tsx +++ b/src/widgets/survey/ui/Finish.tsx @@ -104,7 +104,6 @@ function FinishItem({ const { isAlertOpened: isRetryAlertOpened, openAlert: openRetryAlert } = useRetryUpload({ retryUpload: processQueue, - appletId, }); let finishReason: FinishReason = isTimerElapsed ? 'time-is-up' : 'regular'; diff --git a/src/widgets/survey/ui/FlowElementSwitch.tsx b/src/widgets/survey/ui/FlowElementSwitch.tsx index f61d7904a..381008952 100644 --- a/src/widgets/survey/ui/FlowElementSwitch.tsx +++ b/src/widgets/survey/ui/FlowElementSwitch.tsx @@ -7,7 +7,12 @@ import { ActivityIdentityContext, ActivityStepper, } from '@app/features/pass-survey'; -import { colors } from '@app/shared/lib'; +import { + AnalyticsService, + colors, + MixEvents, + MixProperties, +} from '@app/shared/lib'; import { BackButton, CrossIcon, Box } from '@app/shared/ui'; import Finish from './Finish'; @@ -43,7 +48,12 @@ function FlowElementSwitch({ const navigator = useNavigation(); - const closeAssessment = () => { + const closeAssessment = (reason: 'regular' | 'click-on-return') => { + if (reason === 'click-on-return') { + AnalyticsService.track(MixEvents.ReturnToActivitiesPressed, { + [MixProperties.AppletId]: context.appletId, + }); + } navigator.goBack(); }; diff --git a/src/widgets/survey/ui/Intermediate.tsx b/src/widgets/survey/ui/Intermediate.tsx index 25143e5c5..a95fcd848 100644 --- a/src/widgets/survey/ui/Intermediate.tsx +++ b/src/widgets/survey/ui/Intermediate.tsx @@ -158,7 +158,6 @@ function Intermediate({ changeActivity(); onFinish(); }, - appletId, }); const { getName: getActivityName } = useActivityInfo(); From ddd22db17f89de83494a618e3cf11996277544d2 Mon Sep 17 00:00:00 2001 From: "Nalivaiko, Aleksej" Date: Wed, 14 Feb 2024 13:50:43 +0300 Subject: [PATCH 5/5] M2-5257: [Mobile][DevOps] Apply mobile analytics for test environements --- src/shared/lib/analytics/AnalyticsService.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/shared/lib/analytics/AnalyticsService.ts b/src/shared/lib/analytics/AnalyticsService.ts index 46e4372f7..2dfd5d24b 100644 --- a/src/shared/lib/analytics/AnalyticsService.ts +++ b/src/shared/lib/analytics/AnalyticsService.ts @@ -1,10 +1,9 @@ import MixpanelAnalytics from './MixpanelAnalytics'; -import { ENV, MIXPANEL_TOKEN } from '../constants'; +import { MIXPANEL_TOKEN } from '../constants'; import { Logger } from '../services'; import { createStorage } from '../storages'; -const isProduction = !ENV; -const shouldEnableMixpanel = MIXPANEL_TOKEN && isProduction; +const shouldEnableMixpanel = !!MIXPANEL_TOKEN; export const storage = createStorage('analytics-storage'); @@ -39,8 +38,12 @@ export const MixEvents = { const AnalyticsService = { async init(): Promise { - if (shouldEnableMixpanel && MIXPANEL_TOKEN) { - service = new MixpanelAnalytics(MIXPANEL_TOKEN); + if (shouldEnableMixpanel) { + Logger.log( + '[AnalyticsService]: Create and init MixpanelAnalytics object', + ); + + service = new MixpanelAnalytics(MIXPANEL_TOKEN!); return service.init(); } },