From abec657db2b66b0427d01ba385bf23f82452dc50 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 20 May 2024 12:28:56 -0400 Subject: [PATCH] use the new yyyy_mm_dd endpoint for agg metrics Instead of querying by timestamp we can now query by "YYYY-MM-DD" strings. The yyyy_mm_dd endpoint also has a different format for metric_list: instead of just an array of metrics, it is a mapping of "metric names" to "grouping fields" (https://github.com/e-mission/e-mission-server/pull/966#issuecomment-2119705314) We will specify this in the config as "metrics_list". The yyyy_mm_dd endpoint will also ask for app_config, so we will pass that through to `fetchMetricsFromServer` and include it in the `query` (note that `survey_info` is the only field that needs to be included) We will be supporting another metric called "response_count"; added a card to the 'summary' section (if it is configured to show) Updated types in appConfigTypes.ts --- www/js/metrics/MetricsTab.tsx | 90 +++++++++++++++++++++------------- www/js/metrics/metricsTypes.ts | 4 +- www/js/types/appConfigTypes.ts | 49 +++++++++++------- 3 files changed, 87 insertions(+), 56 deletions(-) diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 4e047604b..9244a0887 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -17,7 +17,7 @@ import ActiveMinutesTableCard from './ActiveMinutesTableCard'; import { getAggregateData, getMetrics } from '../services/commHelper'; import { displayErrorMsg, logDebug, logWarn } from '../plugin/logger'; import useAppConfig from '../useAppConfig'; -import { ServerConnConfig } from '../types/appConfigTypes'; +import { AppConfig, MetricsList, MetricsUiSection } from '../types/appConfigTypes'; import DateSelect from '../diary/list/DateSelect'; import TimelineContext from '../TimelineContext'; import { isoDateRangeToTsRange, isoDatesDifference } from '../diary/timelineHelper'; @@ -28,9 +28,17 @@ import SurveyComparisonCard from './SurveyComparisonCard'; // 2 weeks of data is needed in order to compare "past week" vs "previous week" const N_DAYS_TO_LOAD = 14; // 2 weeks -const DEFAULT_SECTIONS_TO_SHOW = ['footprint', 'active_travel', 'summary'] as const; -export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; -const DEFAULT_SUMMARY_LIST = ['distance', 'count', 'duration'] as const; +const DEFAULT_SECTIONS_TO_SHOW: MetricsUiSection[] = [ + 'footprint', + 'active_travel', + 'summary', +] as const; +export const DEFAULT_METRICS_LIST: MetricsList = { + distance: ['mode_confirm'], + duration: ['mode_confirm'], + count: ['mode_confirm'], + response_count: ['mode_confirm'], +}; export type SurveyObject = { answered: number; @@ -132,19 +140,21 @@ const DUMMY_SURVEY_METRIC: SurveyMetric = { async function fetchMetricsFromServer( type: 'user' | 'aggregate', dateRange: [string, string], - serverConnConfig: ServerConnConfig, + metricsList: MetricsList, + appConfig: AppConfig, ) { const [startTs, endTs] = isoDateRangeToTsRange(dateRange); logDebug('MetricsTab: fetching metrics from server for ts range ' + startTs + ' to ' + endTs); const query = { freq: 'D', - start_time: startTs, - end_time: endTs, - metric_list: METRIC_LIST, + start_time: dateRange[0], + end_time: dateRange[1], + metric_list: metricsList, is_return_aggregate: type == 'aggregate', + app_config: { survey_info: appConfig.survey_info }, }; if (type == 'user') return getMetrics('timestamp', query); - return getAggregateData('result/metrics/timestamp', query, serverConnConfig); + return getAggregateData('result/metrics/yyyy_mm_dd', query, appConfig.server); } const MetricsTab = () => { @@ -163,26 +173,25 @@ const MetricsTab = () => { loadMoreDays, } = useContext(TimelineContext); + const metricsList = appConfig?.metrics?.phone_dashboard_ui?.metrics_list ?? DEFAULT_METRICS_LIST; + const [aggMetrics, setAggMetrics] = useState(undefined); // user metrics are computed on the phone from the timeline data const userMetrics = useMemo(() => { - console.time('MetricsTab: generate_summaries'); if (!timelineMap) return; - console.time('MetricsTab: timelineMap.values()'); const timelineValues = [...timelineMap.values()]; - console.timeEnd('MetricsTab: timelineMap.values()'); const result = metrics_summaries.generate_summaries( - METRIC_LIST, + { ...metricsList }, timelineValues, timelineLabelMap, ) as MetricsData; - console.timeEnd('MetricsTab: generate_summaries'); + logDebug('MetricsTab: computed userMetrics' + JSON.stringify(result)); return result; }, [timelineMap]); // at least N_DAYS_TO_LOAD of timeline data should be loaded for the user metrics useEffect(() => { - if (!appConfig?.server) return; + if (!appConfig) return; const dateRangeDays = isoDatesDifference(...dateRange); // this tab uses the last N_DAYS_TO_LOAD of data; if we need more, we should fetch it @@ -196,10 +205,11 @@ const MetricsTab = () => { } else { logDebug(`MetricsTab: date range >= ${N_DAYS_TO_LOAD} days, not loading more days`); } - }, [dateRange, timelineIsLoading, appConfig?.server]); + }, [dateRange, timelineIsLoading, appConfig]); // aggregate metrics fetched from the server whenever the date range is set useEffect(() => { + if (!appConfig) return; logDebug('MetricsTab: dateRange updated to ' + JSON.stringify(dateRange)); const dateRangeDays = isoDatesDifference(...dateRange); if (dateRangeDays < N_DAYS_TO_LOAD) { @@ -207,13 +217,14 @@ const MetricsTab = () => { `MetricsTab: date range < ${N_DAYS_TO_LOAD} days, not loading aggregate metrics yet`, ); } else { - loadMetricsForPopulation('aggregate', dateRange); + loadMetricsForPopulation('aggregate', dateRange, appConfig); } - }, [dateRange]); + }, [dateRange, appConfig]); async function loadMetricsForPopulation( population: 'user' | 'aggregate', dateRange: [string, string], + appConfig: AppConfig, ) { try { logDebug(`MetricsTab: fetching metrics for population ${population}' @@ -221,20 +232,22 @@ const MetricsTab = () => { const serverResponse: any = await fetchMetricsFromServer( population, dateRange, - appConfig.server, + metricsList, + appConfig, ); logDebug('MetricsTab: received metrics: ' + JSON.stringify(serverResponse)); - const metrics = {}; - const dataKey = population == 'user' ? 'user_metrics' : 'aggregate_metrics'; - METRIC_LIST.forEach((metricName, i) => { - metrics[metricName] = serverResponse[dataKey][i]; - }); - logDebug('MetricsTab: parsed metrics: ' + JSON.stringify(metrics)); - if (population == 'user') { - // setUserMetrics(metrics as MetricsData); - } else { - setAggMetrics(metrics as MetricsData); - } + // const metrics = {}; + // const dataKey = population == 'user' ? 'user_metrics' : 'aggregate_metrics'; + // METRIC_LIST.forEach((metricName, i) => { + // metrics[metricName] = serverResponse[dataKey][i]; + // }); + // logDebug('MetricsTab: parsed metrics: ' + JSON.stringify(metrics)); + // if (population == 'user') { + // // setUserMetrics(metrics as MetricsData); + // } else { + console.debug('MetricsTab: aggMetrics', serverResponse); + setAggMetrics(serverResponse as MetricsData); + // } } catch (e) { logWarn(e + t('errors.while-loading-metrics')); // replace with displayErr } @@ -242,8 +255,6 @@ const MetricsTab = () => { const sectionsToShow = appConfig?.metrics?.phone_dashboard_ui?.sections || DEFAULT_SECTIONS_TO_SHOW; - const summaryList = - appConfig?.metrics?.phone_dashboard_ui?.summary_options?.metrics_list ?? DEFAULT_SUMMARY_LIST; const { width: windowWidth } = useWindowDimensions(); const cardWidth = windowWidth * 0.88; const studyStartDate = `${appConfig?.intro.start_month} / ${appConfig?.intro.start_year}`; @@ -279,7 +290,7 @@ const MetricsTab = () => { )} {sectionsToShow.includes('summary') && ( - {summaryList.includes('distance') && ( + {(userMetrics?.distance || aggMetrics?.distance) && ( { unitFormatFn={getFormattedDistance} /> )} - {summaryList.includes('count') && ( + {(userMetrics?.count || aggMetrics?.count) && ( { unitFormatFn={formatForDisplay} /> )} - {summaryList.includes('duration') && ( + {(userMetrics?.duration || aggMetrics?.duration) && ( { unitFormatFn={secondsToHours} /> )} + {(userMetrics?.response_count || aggMetrics?.response_count) && ( + + )} {/* , where could be anything export type DayOfServerMetricData = LabelProps & { diff --git a/www/js/types/appConfigTypes.ts b/www/js/types/appConfigTypes.ts index 0415e7cfd..09c12a451 100644 --- a/www/js/types/appConfigTypes.ts +++ b/www/js/types/appConfigTypes.ts @@ -14,24 +14,7 @@ export type AppConfig = { tracking?: { bluetooth_only: boolean; }; - metrics: { - include_test_users: boolean; - phone_dashboard_ui?: { - sections: ('footprint' | 'active_travel' | 'summary' | 'engagement' | 'surveys')[]; - footprint_options?: { - unlabeled_uncertainty: boolean; - }; - summary_options?: { - metrics_list: ('distance' | 'count' | 'duration')[]; - }; - engagement_options?: { - leaderboard_metric: [string, string]; - }; - active_travel_options?: { - modes_list: string[]; - }; - }; - }; + metrics: MetricsConfig; reminderSchemes?: ReminderSchemesConfig; [k: string]: any; // TODO fill in all the other fields }; @@ -110,3 +93,33 @@ export type ReminderSchemesConfig = { defaultTime?: string; // format is HH:MM in 24 hour time }; }; + +// the available metrics that can be displayed in the phone dashboard +export type MetricName = 'distance' | 'count' | 'duration' | 'response_count'; +// the available trip / userinput properties that can be used to group the metrics +export const groupingFields = [ + 'mode_confirm', + 'purpose_confirm', + 'replaced_mode_confirm', + 'primary_ble_sensed_mode', +] as const; +export type GroupingField = (typeof groupingFields)[number]; +export type MetricsList = { [k in MetricName]?: GroupingField[] }; +export type MetricsUiSection = 'footprint' | 'active_travel' | 'summary' | 'engagement' | 'surveys'; +export type MetricsConfig = { + include_test_users: boolean; + phone_dashboard_ui?: { + sections: MetricsUiSection[]; + metrics_list: MetricsList; + footprint_options?: { + unlabeled_uncertainty: boolean; + }; + summary_options?: {}; + engagement_options?: { + leaderboard_metric: [string, string]; + }; + active_travel_options?: { + modes_list: string[]; + }; + }; +};