diff --git a/www/__tests__/clientStats.test.ts b/www/__tests__/clientStats.test.ts index a3a953582..632eafddf 100644 --- a/www/__tests__/clientStats.test.ts +++ b/www/__tests__/clientStats.test.ts @@ -1,11 +1,5 @@ import { mockBEMUserCache, mockDevice, mockGetAppVersion } from '../__mocks__/cordovaMocks'; -import { - addStatError, - addStatEvent, - addStatReading, - getAppVersion, - statKeys, -} from '../js/plugin/clientStats'; +import { addStatError, addStatReading, getAppVersion } from '../js/plugin/clientStats'; mockDevice(); // this mocks cordova-plugin-app-version, generating a "Mock App", version "1.2.3" @@ -21,10 +15,10 @@ it('gets the app version', async () => { it('stores a client stats reading', async () => { const reading = { a: 1, b: 2 }; - await addStatReading(statKeys.REMINDER_PREFS, reading); + await addStatReading('set_reminder_prefs', reading); const storedMessages = await db.getAllMessages('stats/client_time', false); expect(storedMessages).toContainEqual({ - name: statKeys.REMINDER_PREFS, + name: 'set_reminder_prefs', ts: expect.any(Number), reading, client_app_version: '1.2.3', @@ -33,10 +27,10 @@ it('stores a client stats reading', async () => { }); it('stores a client stats event', async () => { - await addStatEvent(statKeys.BUTTON_FORCE_SYNC); - const storedMessages = await db.getAllMessages('stats/client_nav_event', false); + await addStatReading('force_sync'); + const storedMessages = await db.getAllMessages('stats/client_time', false); expect(storedMessages).toContainEqual({ - name: statKeys.BUTTON_FORCE_SYNC, + name: 'force_sync', ts: expect.any(Number), reading: null, client_app_version: '1.2.3', @@ -46,10 +40,10 @@ it('stores a client stats event', async () => { it('stores a client stats error', async () => { const errorStr = 'test error'; - await addStatError(statKeys.MISSING_KEYS, errorStr); + await addStatError('missing_keys', errorStr); const storedMessages = await db.getAllMessages('stats/client_error', false); expect(storedMessages).toContainEqual({ - name: statKeys.MISSING_KEYS, + name: 'missing_keys', ts: expect.any(Number), reading: errorStr, client_app_version: '1.2.3', diff --git a/www/__tests__/remoteNotifyHandler.test.ts b/www/__tests__/remoteNotifyHandler.test.ts index 320877c6b..4d3761547 100644 --- a/www/__tests__/remoteNotifyHandler.test.ts +++ b/www/__tests__/remoteNotifyHandler.test.ts @@ -26,7 +26,7 @@ beforeEach(() => { it('does not adds a statEvent if not subscribed', async () => { publish(EVENTS.CLOUD_NOTIFICATION_EVENT, 'test data'); - const storedMessages = await db.getAllMessages('stats/client_nav_event', false); + const storedMessages = await db.getAllMessages('stats/client_time', false); expect(storedMessages).toEqual([]); }); @@ -35,11 +35,11 @@ it('adds a statEvent if subscribed', async () => { await new Promise((r) => setTimeout(r, 500)); //wait for subscription publish(EVENTS.CLOUD_NOTIFICATION_EVENT, 'test data'); await new Promise((r) => setTimeout(r, 500)); //wait for event handling - const storedMessages = await db.getAllMessages('stats/client_nav_event', false); + const storedMessages = await db.getAllMessages('stats/client_time', false); expect(storedMessages).toContainEqual({ - name: 'notification_open', + name: 'open_notification', ts: expect.any(Number), - reading: null, + reading: 'test data', client_app_version: '1.2.3', client_os_version: '14.0.0', }); diff --git a/www/js/Main.tsx b/www/js/Main.tsx index 650ed4044..cb232535d 100644 --- a/www/js/Main.tsx +++ b/www/js/Main.tsx @@ -1,7 +1,7 @@ /* Once onboarding is done, this is the main app content. Includes the bottom navigation bar and each of the tabs. */ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useContext, useMemo, useState } from 'react'; import { BottomNavigation, useTheme } from 'react-native-paper'; import { AppContext } from './App'; @@ -11,6 +11,7 @@ import LabelTab from './diary/LabelTab'; import MetricsTab from './metrics/MetricsTab'; import ProfileSettings from './control/ProfileSettings'; import TimelineContext, { useTimelineContext } from './TimelineContext'; +import { addStatReading } from './plugin/clientStats'; const defaultRoutes = (t) => [ { @@ -57,6 +58,14 @@ const Main = () => { return showMetrics ? defaultRoutes(t) : defaultRoutes(t).filter((r) => r.key != 'metrics'); }, [appConfig, t]); + const onIndexChange = useCallback( + (i: number) => { + addStatReading('nav_tab_change', routes[i].key); + setIndex(i); + }, + [routes], + ); + useEffect(() => { const { setShouldUpdateTimeline } = timelineContext; // update TimelineScrollList component only when the active tab is 'label' to fix leaflet map issue @@ -67,7 +76,7 @@ const Main = () => { { const { t } = useTranslation(); const appConfig = useAppConfig(); + useAppStateChange(() => refreshTimeline()); const [labelOptions, setLabelOptions] = useState(null); // timestamp range that has been processed by the pipeline on the server diff --git a/www/js/control/ControlSyncHelper.tsx b/www/js/control/ControlSyncHelper.tsx index bdb565f61..30ba7a87c 100644 --- a/www/js/control/ControlSyncHelper.tsx +++ b/www/js/control/ControlSyncHelper.tsx @@ -6,7 +6,7 @@ import { settingStyles } from './ProfileSettings'; import ActionMenu from '../components/ActionMenu'; import SettingRow from './SettingRow'; import { AlertManager } from '../components/AlertBar'; -import { addStatEvent, statKeys } from '../plugin/clientStats'; +import { addStatReading } from '../plugin/clientStats'; import { updateUser } from '../services/commHelper'; import { displayError, logDebug, logWarn } from '../plugin/logger'; import { DateTime } from 'luxon'; @@ -42,8 +42,8 @@ export const ForceSyncRow = ({ getState }) => { async function forceSync() { try { - let addedEvent = await addStatEvent(statKeys.BUTTON_FORCE_SYNC); - let sync = await forcePluginSync(); + await addStatReading('force_sync'); + await forcePluginSync(); /* * Change to sensorKey to "background/location" after fixing issues * with getLastSensorData and getLastMessages in the usercache diff --git a/www/js/diary/cards/TripCard.tsx b/www/js/diary/cards/TripCard.tsx index a309d63aa..23d7db224 100644 --- a/www/js/diary/cards/TripCard.tsx +++ b/www/js/diary/cards/TripCard.tsx @@ -25,6 +25,7 @@ import ModesIndicator from './ModesIndicator'; import { useGeojsonForTrip } from '../timelineHelper'; import { CompositeTrip } from '../../types/diaryTypes'; import { EnketoUserInputEntry } from '../../survey/enketo/enketoHelper'; +import { addStatReading } from '../../plugin/clientStats'; type Props = { trip: CompositeTrip; isFirstInList?: boolean }; const TripCard = ({ trip, isFirstInList }: Props) => { @@ -51,6 +52,7 @@ const TripCard = ({ trip, isFirstInList }: Props) => { function showDetail() { const tripId = trip._id.$oid; + addStatReading('view_trip_details', { tripId }); navigation.navigate('label.details', { tripId, flavoredTheme }); } diff --git a/www/js/plugin/clientStats.ts b/www/js/plugin/clientStats.ts index bdf4c1888..e106dd7cf 100644 --- a/www/js/plugin/clientStats.ts +++ b/www/js/plugin/clientStats.ts @@ -2,24 +2,17 @@ import { displayErrorMsg, logDebug } from './logger'; const CLIENT_TIME = 'stats/client_time'; const CLIENT_ERROR = 'stats/client_error'; -const CLIENT_NAV_EVENT = 'stats/client_nav_event'; -export const statKeys = { - STATE_CHANGED: 'state_changed', - BUTTON_FORCE_SYNC: 'button_sync_forced', - CHECKED_DIARY: 'checked_diary', - DIARY_TIME: 'diary_time', - METRICS_TIME: 'metrics_time', - CHECKED_INF_SCROLL: 'checked_inf_scroll', - INF_SCROLL_TIME: 'inf_scroll_time', - VERIFY_TRIP: 'verify_trip', - LABEL_TAB_SWITCH: 'label_tab_switch', - SELECT_LABEL: 'select_label', - EXPANDED_TRIP: 'expanded_trip', - NOTIFICATION_OPEN: 'notification_open', - REMINDER_PREFS: 'reminder_time_prefs', - MISSING_KEYS: 'missing_keys', -}; +type StatKey = + | 'app_state_change' + | 'nav_tab_change' + | 'view_trip_details' + | 'multilabel_open' + | 'multilabel_choose' + | 'set_reminder_prefs' + | 'force_sync' + | 'open_notification' + | 'missing_keys'; let appVersion; export function getAppVersion() { @@ -30,14 +23,15 @@ export function getAppVersion() { }); } -async function getStatsEvent(name: string, reading: any) { +async function getStatsEvent(name: StatKey, reading?: any) { const ts = Date.now() / 1000; const client_app_version = await getAppVersion(); const client_os_version = window['device'].version; + reading = reading || null; return { name, ts, reading, client_app_version, client_os_version }; } -export async function addStatReading(name: string, reading: any) { +export async function addStatReading(name: StatKey, reading?: any) { const db = window['cordova']?.plugins?.BEMUserCache; const event = await getStatsEvent(name, reading); logDebug('addStatReading: adding CLIENT_TIME event: ' + JSON.stringify(event)); @@ -45,15 +39,7 @@ export async function addStatReading(name: string, reading: any) { displayErrorMsg('addStatReading: db is not defined'); } -export async function addStatEvent(name: string) { - const db = window['cordova']?.plugins?.BEMUserCache; - const event = await getStatsEvent(name, null); - logDebug('addStatEvent: adding CLIENT_NAV_EVENT event: ' + JSON.stringify(event)); - if (db) return db.putMessage(CLIENT_NAV_EVENT, event); - displayErrorMsg('addStatEvent: db is not defined'); -} - -export async function addStatError(name: string, errorStr: string) { +export async function addStatError(name: StatKey, errorStr: string) { const db = window['cordova']?.plugins?.BEMUserCache; const event = await getStatsEvent(name, errorStr); logDebug('addStatError: adding CLIENT_ERROR event: ' + JSON.stringify(event)); diff --git a/www/js/plugin/storage.ts b/www/js/plugin/storage.ts index e22bb4669..51701730e 100644 --- a/www/js/plugin/storage.ts +++ b/www/js/plugin/storage.ts @@ -1,4 +1,4 @@ -import { addStatReading, statKeys } from './clientStats'; +import { addStatReading } from './clientStats'; import { logDebug, logWarn } from './logger'; function mungeValue(key, value) { @@ -162,7 +162,7 @@ export function storageSyncLocalAndNative() { logDebug('STORAGE_PLUGIN: Syncing all missing keys ' + allMissing); allMissing.forEach(getUnifiedValue); if (allMissing.length != 0) { - addStatReading(statKeys.MISSING_KEYS, { + addStatReading('missing_keys', { type: 'local_storage_mismatch', allMissingLength: allMissing.length, missingWebLength: missingWeb.length, @@ -178,7 +178,7 @@ export function storageSyncLocalAndNative() { (nativeKeys) => { logDebug('STORAGE_PLUGIN: For the record, all unique native keys are ' + nativeKeys); if (nativeKeys.length == 0) { - addStatReading(statKeys.MISSING_KEYS, { + addStatReading('missing_keys', { type: 'all_native', }).then(logDebug('Logged all missing native keys to client stats')); } diff --git a/www/js/splash/notifScheduler.ts b/www/js/splash/notifScheduler.ts index 10d20b18b..d882c5cb6 100644 --- a/www/js/splash/notifScheduler.ts +++ b/www/js/splash/notifScheduler.ts @@ -1,4 +1,4 @@ -import { addStatReading, statKeys } from '../plugin/clientStats'; +import { addStatReading } from '../plugin/clientStats'; import { getUser, updateUser } from '../services/commHelper'; import { displayErrorMsg, logDebug } from '../plugin/logger'; import { DateTime } from 'luxon'; @@ -286,7 +286,7 @@ export async function setReminderPrefs( // extract only the relevant fields from the prefs, // and add as a reading to client stats const { reminder_assignment, reminder_join_date, reminder_time_of_day } = prefs; - addStatReading(statKeys.REMINDER_PREFS, { + addStatReading('set_reminder_prefs', { reminder_assignment, reminder_join_date, reminder_time_of_day, diff --git a/www/js/splash/remoteNotifyHandler.ts b/www/js/splash/remoteNotifyHandler.ts index 098625ee2..513b3719e 100644 --- a/www/js/splash/remoteNotifyHandler.ts +++ b/www/js/splash/remoteNotifyHandler.ts @@ -11,7 +11,7 @@ * notification handling gets more complex, we should consider decoupling it as well. */ import { EVENTS, subscribe } from '../customEventHandler'; -import { addStatEvent, statKeys } from '../plugin/clientStats'; +import { addStatReading } from '../plugin/clientStats'; import { displayErrorMsg, logDebug } from '../plugin/logger'; const options = 'location=yes,clearcache=no,toolbar=yes,hideurlbar=yes'; @@ -31,7 +31,7 @@ const launchWebpage = (url) => window['cordova'].InAppBrowser.open(url, '_blank' */ function onCloudNotifEvent(event) { const data = event.detail; - addStatEvent(statKeys.NOTIFICATION_OPEN); + addStatReading('open_notification', data); logDebug('data = ' + JSON.stringify(data)); if ( data.additionalData && diff --git a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx index 41257b5d0..5cc1de3d7 100644 --- a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx +++ b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx @@ -32,6 +32,7 @@ import useAppConfig from '../../useAppConfig'; import { MultilabelKey } from '../../types/labelTypes'; // import { updateUserCustomLabel } from '../../services/commHelper'; import { AppContext } from '../../App'; +import { addStatReading } from '../../plugin/clientStats'; const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => { const { colors } = useTheme(); @@ -72,6 +73,11 @@ const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => { } } + function openModalFor(inputType: MultilabelKey) { + addStatReading('multilabel_open', inputType); + setModalVisibleFor(inputType); + } + function dismiss() { setModalVisibleFor(null); setOtherLabel(null); @@ -123,6 +129,7 @@ const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => { } Promise.all(storePromises).then(() => { logDebug('Successfully stored input data ' + JSON.stringify(inputsToStore)); + addStatReading('multilabel_choose', inputsToStore); dismiss(); addUserInputToEntry(trip._id.$oid, inputsToStore, 'label'); }); @@ -156,7 +163,7 @@ const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => { fillColor={fillColor} borderColor={borderColor} textColor={textColor} - onPress={(e) => setModalVisibleFor(input.name)}> + onPress={(e) => openModalFor(input.name)}> {t(btnText)} diff --git a/www/js/useAppStateChange.ts b/www/js/useAppStateChange.ts index 9a77909a0..760cbd8fa 100644 --- a/www/js/useAppStateChange.ts +++ b/www/js/useAppStateChange.ts @@ -5,7 +5,7 @@ import { useEffect, useRef } from 'react'; import { AppState } from 'react-native'; -import { logDebug } from './plugin/logger'; +import { addStatReading } from './plugin/clientStats'; const useAppStateChange = (onResume) => { const appState = useRef(AppState.currentState); @@ -17,8 +17,9 @@ const useAppStateChange = (onResume) => { } appState.current = nextAppState; - logDebug('new AppState: ' + appState.current); + addStatReading('app_state_change', appState.current); }); + return () => subscription.remove(); }, []); return {};