From 4c2bbf0d32736b80b935ed86dc3f4cc4bc78d593 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 9 Nov 2023 11:16:21 -0500 Subject: [PATCH 1/5] add ErrorBoundary to each tab --- www/js/App.tsx | 7 ++++--- www/js/plugin/ErrorBoundary.tsx | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 www/js/plugin/ErrorBoundary.tsx diff --git a/www/js/App.tsx b/www/js/App.tsx index ab4caebf7..54b677add 100644 --- a/www/js/App.tsx +++ b/www/js/App.tsx @@ -15,6 +15,7 @@ import { import { setServerConnSettings } from './config/serverConn'; import AppStatusModal from './control/AppStatusModal'; import usePermissionStatus from './usePermissionStatus'; +import { withErrorBoundary } from './plugin/ErrorBoundary'; const defaultRoutes = (t) => [ { @@ -55,9 +56,9 @@ const App = () => { }, [appConfig, t]); const renderScene = BottomNavigation.SceneMap({ - label: LabelTab, - metrics: MetricsTab, - control: ProfileSettings, + label: withErrorBoundary(LabelTab), + metrics: withErrorBoundary(MetricsTab), + control: withErrorBoundary(ProfileSettings), }); const refreshOnboardingState = () => getPendingOnboardingState().then(setOnboardingState); diff --git a/www/js/plugin/ErrorBoundary.tsx b/www/js/plugin/ErrorBoundary.tsx new file mode 100644 index 000000000..61fdf023e --- /dev/null +++ b/www/js/plugin/ErrorBoundary.tsx @@ -0,0 +1,36 @@ +// based on https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary + +import React from 'react'; +import { displayError } from './logger'; +import { Icon } from '../components/Icon'; + +class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + return { hasError: true }; + } + + componentDidCatch(error, info) { + displayError(error, info.componentStack); + } + + render() { + if (this.state.hasError) { + return ; + } + + return this.props.children; + } +} + +export const withErrorBoundary = (Component) => (props) => ( + + + +); + +export default ErrorBoundary; From 0be22344380c41dffd1ea45de0a08e20d1ff6b32 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 9 Nov 2023 13:35:05 -0500 Subject: [PATCH 2/5] add logDebug statements in Chart.tsx This may help with debugging https://github.com/e-mission/e-mission-docs/issues/986#issuecomment-1799229701 --- www/js/components/Chart.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx index d7687e424..4ebf49c24 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -5,6 +5,7 @@ import { Chart as ChartJS, registerables } from 'chart.js'; import { Chart as ChartJSChart } from 'react-chartjs-2'; import Annotation, { AnnotationOptions, LabelPosition } from 'chartjs-plugin-annotation'; import { dedupColors, getChartHeight, darkenOrLighten } from './charting'; +import { logDebug } from '../plugin/logger'; ChartJS.register(...registerables, Annotation); @@ -134,6 +135,9 @@ const Chart = ({ ? {} : { callback: (value, i) => { + logDebug(`Horizontal axis callback: i = ${i}; + chartDatasets = ${JSON.stringify(chartDatasets)}; + chartDatasets[0].data = ${JSON.stringify(chartDatasets[0].data)}`); const label = chartDatasets[0].data[i].y; if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); @@ -168,7 +172,9 @@ const Chart = ({ ? {} : { callback: (value, i) => { - console.log('testing vertical', chartData, i); + logDebug(`Vertical axis callback: i = ${i}; + chartDatasets = ${JSON.stringify(chartDatasets)}; + chartDatasets[0].data = ${JSON.stringify(chartDatasets[0].data)}`); const label = chartDatasets[0].data[i].x; if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); From 1388eee2efdfbefa8dd14399665e74d02047ebcb Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 9 Nov 2023 13:40:34 -0500 Subject: [PATCH 3/5] add more logging in LabelTab -added several try/catches to cover all top-level calls -added logDebug statements throughout, using `` for multi-line so they are not ugly -replaced any uses of the old Logger service --- www/js/diary/LabelTab.tsx | 201 ++++++++++++++++++++++---------------- 1 file changed, 115 insertions(+), 86 deletions(-) diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 8b6e65d52..d5fa8a7c5 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -25,7 +25,7 @@ import { import { fillLocationNamesOfTrip, resetNominatimLimiter } from './addressNamesHelper'; import { SurveyOptions } from '../survey/survey'; import { getLabelOptions } from '../survey/multilabel/confirmHelper'; -import { displayError } from '../plugin/logger'; +import { displayError, displayErrorMsg, logDebug, logWarn } from '../plugin/logger'; import { useTheme } from 'react-native-paper'; import { getPipelineRangeTs } from '../commHelper'; @@ -58,55 +58,66 @@ const LabelTab = () => { // initialization, once the appConfig is loaded useEffect(() => { - if (!appConfig) return; - const surveyOptKey = appConfig.survey_info['trip-labels']; - const surveyOpt = SurveyOptions[surveyOptKey]; - setSurveyOpt(surveyOpt); - showPlaces = appConfig.survey_info?.buttons?.['place-notes']; - getLabelOptions(appConfig).then((labelOptions) => setLabelOptions(labelOptions)); - labelPopulateFactory = getAngularService(surveyOpt.service); - const tripSurveyName = appConfig.survey_info?.buttons?.['trip-notes']?.surveyName; - const placeSurveyName = appConfig.survey_info?.buttons?.['place-notes']?.surveyName; - enbs.initConfig(tripSurveyName, placeSurveyName); + try { + if (!appConfig) return; + const surveyOptKey = appConfig.survey_info['trip-labels']; + const surveyOpt = SurveyOptions[surveyOptKey]; + setSurveyOpt(surveyOpt); + showPlaces = appConfig.survey_info?.buttons?.['place-notes']; + getLabelOptions(appConfig).then((labelOptions) => setLabelOptions(labelOptions)); + labelPopulateFactory = getAngularService(surveyOpt.service); + const tripSurveyName = appConfig.survey_info?.buttons?.['trip-notes']?.surveyName; + const placeSurveyName = appConfig.survey_info?.buttons?.['place-notes']?.surveyName; + enbs.initConfig(tripSurveyName, placeSurveyName); - // we will show filters if 'additions' are not configured - // https://github.com/e-mission/e-mission-docs/issues/894 - if (appConfig.survey_info?.buttons == undefined) { - // initalize filters - const tripFilter = surveyOpt.filter; - const allFalseFilters = tripFilter.map((f, i) => ({ - ...f, - state: i == 0 ? true : false, // only the first filter will have state true on init - })); - setFilterInputs(allFalseFilters); + // we will show filters if 'additions' are not configured + // https://github.com/e-mission/e-mission-docs/issues/894 + if (appConfig.survey_info?.buttons == undefined) { + // initalize filters + const tripFilter = surveyOpt.filter; + const allFalseFilters = tripFilter.map((f, i) => ({ + ...f, + state: i == 0 ? true : false, // only the first filter will have state true on init + })); + setFilterInputs(allFalseFilters); + } + loadTimelineEntries(); + } catch (e) { + displayError(e, t('errors.while-initializing-label')); } - loadTimelineEntries(); }, [appConfig, refreshTime]); // whenever timelineMap is updated, update the displayedEntries // according to the active filter useEffect(() => { - if (!timelineMap) return setDisplayedEntries(null); - const allEntries = Array.from(timelineMap.values()); - const activeFilter = filterInputs?.find((f) => f.state == true); - let entriesToDisplay = allEntries; - if (activeFilter) { - const entriesAfterFilter = allEntries.filter( - (t) => t.justRepopulated || activeFilter?.filter(t), - ); - /* next, filter out any untracked time if the trips that came before and + try { + if (!timelineMap) return setDisplayedEntries(null); + const allEntries = Array.from(timelineMap.values()); + const activeFilter = filterInputs?.find((f) => f.state == true); + let entriesToDisplay = allEntries; + if (activeFilter) { + const entriesAfterFilter = allEntries.filter( + (t) => t.justRepopulated || activeFilter?.filter(t), + ); + /* next, filter out any untracked time if the trips that came before and after it are no longer displayed */ - entriesToDisplay = entriesAfterFilter.filter((tlEntry) => { - if (!tlEntry.origin_key.includes('untracked')) return true; - const prevTrip = allEntries[allEntries.indexOf(tlEntry) - 1]; - const nextTrip = allEntries[allEntries.indexOf(tlEntry) + 1]; - const prevTripDisplayed = entriesAfterFilter.includes(prevTrip); - const nextTripDisplayed = entriesAfterFilter.includes(nextTrip); - // if either the trip before or after is displayed, then keep the untracked time - return prevTripDisplayed || nextTripDisplayed; - }); + entriesToDisplay = entriesAfterFilter.filter((tlEntry) => { + if (!tlEntry.origin_key.includes('untracked')) return true; + const prevTrip = allEntries[allEntries.indexOf(tlEntry) - 1]; + const nextTrip = allEntries[allEntries.indexOf(tlEntry) + 1]; + const prevTripDisplayed = entriesAfterFilter.includes(prevTrip); + const nextTripDisplayed = entriesAfterFilter.includes(nextTrip); + // if either the trip before or after is displayed, then keep the untracked time + return prevTripDisplayed || nextTripDisplayed; + }); + logDebug('After filtering, entriesToDisplay = ' + JSON.stringify(entriesToDisplay)); + } else { + logDebug('No active filter, displaying all entries'); + } + setDisplayedEntries(entriesToDisplay); + } catch (e) { + displayError(e, t('errors.while-updating-timeline')); } - setDisplayedEntries(entriesToDisplay); }, [timelineMap, filterInputs]); async function loadTimelineEntries() { @@ -117,15 +128,12 @@ const LabelTab = () => { labelPopulateFactory, enbs, ); - Logger.log( - 'After reading unprocessedInputs, labelsResultMap =' + - JSON.stringify(labelsResultMap) + - '; notesResultMap = ' + - JSON.stringify(notesResultMap), - ); + logDebug(`LabelTab: After reading unprocessedInputs, + labelsResultMap = ${JSON.stringify(labelsResultMap)}; + notesResultMap = ${JSON.stringify(notesResultMap)}`); setPipelineRange(pipelineRange); - } catch (error) { - Logger.displayError('Error while loading pipeline range', error); + } catch (e) { + displayError(e, 'Error while loading pipeline range'); setIsLoading(false); } } @@ -138,15 +146,22 @@ const LabelTab = () => { }, [pipelineRange]); function refresh() { - setIsLoading('replace'); - resetNominatimLimiter(); - setQueriedRange(null); - setTimelineMap(null); - setRefreshTime(new Date()); + try { + logDebug('Refreshing LabelTab'); + setIsLoading('replace'); + resetNominatimLimiter(); + setQueriedRange(null); + setTimelineMap(null); + setRefreshTime(new Date()); + } catch (e) { + displayError(e, t('errors.while-refreshing-label')); + } } async function loadAnotherWeek(when: 'past' | 'future') { try { + logDebug('LabelTab: loadAnotherWeek into the ' + when); + const reachedPipelineStart = queriedRange?.start_ts && queriedRange.start_ts <= pipelineRange.start_ts; const reachedPipelineEnd = @@ -183,6 +198,7 @@ const LabelTab = () => { async function loadSpecificWeek(day: string) { try { + logDebug('LabelTab: loadSpecificWeek for day ' + day); if (!isLoading) setIsLoading('replace'); resetNominatimLimiter(); const threeDaysBefore = moment(day).subtract(3, 'days').unix(); @@ -197,6 +213,11 @@ const LabelTab = () => { } function handleFetchedTrips(ctList, utList, mode: 'prepend' | 'append' | 'replace') { + logDebug(`LabelTab: handleFetchedTrips with + mode = ${mode}; + ctList = ${JSON.stringify(ctList)}; + utList = ${JSON.stringify(utList)}`); + const tripsRead = ctList.concat(utList); populateCompositeTrips( tripsRead, @@ -214,6 +235,8 @@ const LabelTab = () => { fillLocationNamesOfTrip(trip); }); const readTimelineMap = compositeTrips2TimelineMap(tripsRead, showPlaces); + logDebug(`LabelTab: after composite trips converted, + readTimelineMap = ${JSON.stringify(readTimelineMap)}`); if (mode == 'append') { setTimelineMap(new Map([...timelineMap, ...readTimelineMap])); } else if (mode == 'prepend') { @@ -221,16 +244,13 @@ const LabelTab = () => { } else if (mode == 'replace') { setTimelineMap(readTimelineMap); } else { - return console.error('Unknown insertion mode ' + mode); + return displayErrorMsg('Unknown insertion mode ' + mode); } } async function fetchTripsInRange(startTs: number, endTs: number) { - if (!pipelineRange.start_ts) { - console.warn('trying to read data too early, early return'); - return; - } - + if (!pipelineRange.start_ts) return logWarn('No pipelineRange yet - early return'); + logDebug('LabelTab: fetchTripsInRange from ' + startTs + ' to ' + endTs); const readCompositePromise = Timeline.readAllCompositeTrips(startTs, endTs); let readUnprocessedPromise; if (endTs >= pipelineRange.end_ts) { @@ -249,6 +269,8 @@ const LabelTab = () => { readUnprocessedPromise = Promise.resolve([]); } const results = await Promise.all([readCompositePromise, readUnprocessedPromise]); + logDebug(`LabelTab: readCompositePromise resolved as: ${JSON.stringify(results[0])}; + readUnprocessedPromise resolved as: ${JSON.stringify(results[1])}`); return results; } @@ -260,34 +282,41 @@ const LabelTab = () => { const timelineMapRef = useRef(timelineMap); async function repopulateTimelineEntry(oid: string) { - if (!timelineMap.has(oid)) - return console.error('Item with oid: ' + oid + ' not found in timeline'); - const [newLabels, newNotes] = await getLocalUnprocessedInputs( - pipelineRange, - labelPopulateFactory, - enbs, - ); - const repopTime = new Date().getTime(); - const newEntry = { ...timelineMap.get(oid), justRepopulated: repopTime }; - labelPopulateFactory.populateInputsAndInferences(newEntry, newLabels); - enbs.populateInputsAndInferences(newEntry, newNotes); - const newTimelineMap = new Map(timelineMap).set(oid, newEntry); - setTimelineMap(newTimelineMap); + try { + logDebug('LabelTab: Repopulating timeline entry with oid ' + oid); + if (!timelineMap.has(oid)) + return displayErrorMsg('Item with oid: ' + oid + ' not found in timeline'); + const [newLabels, newNotes] = await getLocalUnprocessedInputs( + pipelineRange, + labelPopulateFactory, + enbs, + ); + const repopTime = new Date().getTime(); + logDebug('LabelTab: creating new entry for oid ' + oid + ' with repopTime ' + repopTime); + const newEntry = { ...timelineMap.get(oid), justRepopulated: repopTime }; + labelPopulateFactory.populateInputsAndInferences(newEntry, newLabels); + enbs.populateInputsAndInferences(newEntry, newNotes); + logDebug('LabelTab: after repopulating, newEntry = ' + JSON.stringify(newEntry)); + const newTimelineMap = new Map(timelineMap).set(oid, newEntry); + setTimelineMap(newTimelineMap); - // after 30 seconds, remove the justRepopulated flag unless it was repopulated again since then - /* ref is needed to avoid stale closure: + // after 30 seconds, remove the justRepopulated flag unless it was repopulated again since then + /* ref is needed to avoid stale closure: https://legacy.reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function */ - timelineMapRef.current = newTimelineMap; - setTimeout(() => { - const entry = { ...timelineMapRef.current.get(oid) }; - if (entry.justRepopulated != repopTime) - return console.log('Entry ' + oid + ' was repopulated again, skipping'); - const newTimelineMap = new Map(timelineMapRef.current).set(oid, { - ...entry, - justRepopulated: false, - }); - setTimelineMap(newTimelineMap); - }, 30000); + timelineMapRef.current = newTimelineMap; + setTimeout(() => { + const entry = { ...timelineMapRef.current.get(oid) }; + if (entry.justRepopulated != repopTime) + return logDebug('Entry ' + oid + ' was repopulated again, skipping'); + const newTimelineMap = new Map(timelineMapRef.current).set(oid, { + ...entry, + justRepopulated: false, + }); + setTimelineMap(newTimelineMap); + }, 30000); + } catch (e) { + displayError(e, t('errors.while-repopulating-entry')); + } } const contextVals = { From 94d893337c39a297b446d15939f6d92a9d6d01ac Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 9 Nov 2023 13:41:35 -0500 Subject: [PATCH 4/5] add logs to MetricsTab When metrics are fetched and stored as state, we can have more log statements and a try/catch. --- www/js/metrics/MetricsTab.tsx | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index d23cdd454..392a0ef3b 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -17,6 +17,7 @@ import DailyActiveMinutesCard from './DailyActiveMinutesCard'; import CarbonTextCard from './CarbonTextCard'; import ActiveMinutesTableCard from './ActiveMinutesTableCard'; import { getAggregateData, getMetrics } from '../commHelper'; +import { displayError, logDebug } from '../plugin/logger'; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; @@ -54,17 +55,24 @@ const MetricsTab = () => { }, [dateRange]); async function loadMetricsForPopulation(population: 'user' | 'aggregate', dateRange: DateTime[]) { - const serverResponse = await fetchMetricsFromServer(population, dateRange); - console.debug('Got metrics = ', serverResponse); - const metrics = {}; - const dataKey = population == 'user' ? 'user_metrics' : 'aggregate_metrics'; - METRIC_LIST.forEach((metricName, i) => { - metrics[metricName] = serverResponse[dataKey][i]; - }); - if (population == 'user') { - setUserMetrics(metrics as MetricsData); - } else { - setAggMetrics(metrics as MetricsData); + try { + logDebug(`MetricsTab: fetching metrics for population ${population}' + in date range ${JSON.stringify(dateRange)}`); + const serverResponse = await fetchMetricsFromServer(population, dateRange); + 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); + } + } catch (e) { + displayError(e, t('errors.while-loading-metrics')); } } From 98a9db64d0088dc7301d314e14d2fb8c2c2fdc25 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 9 Nov 2023 13:41:53 -0500 Subject: [PATCH 5/5] translations for error msgs just added --- www/i18n/en.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/i18n/en.json b/www/i18n/en.json index 7f3798f16..af549d05e 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -398,9 +398,14 @@ "errors": { "registration-check-token": "User registration error. Please check your token and try again.", "not-registered-cant-contact": "User is not registered, so the server cannot be contacted.", + "while-initializing-label": "While initializing Label tab: ", "while-populating-composite": "Error while populating composite trips", "while-loading-another-week": "Error while loading travel of {{when}} week", "while-loading-specific-week": "Error while loading travel for the week of {{day}}", + "while-updating-timeline": "While updating timeline: ", + "while-refreshing-label": "While refreshing Label tab: ", + "while-repopulating-entry": "While repopulating timeline entry: ", + "while-loading-metrics": "While loading metrics: ", "while-log-messages": "While getting messages from the log ", "while-max-index": "While getting max index " },