Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🪵 Add more logs and error boundaries in Label and Dashboard #1100

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions www/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
},
Expand Down
7 changes: 4 additions & 3 deletions www/js/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => [
{
Expand Down Expand Up @@ -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),
Comment on lines +59 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very cool!

});

const refreshOnboardingState = () => getPendingOnboardingState().then(setOnboardingState);
Expand Down
8 changes: 7 additions & 1 deletion www/js/components/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down
201 changes: 115 additions & 86 deletions www/js/diary/LabelTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<any>(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<any>(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() {
Expand All @@ -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);
}
}
Expand All @@ -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 =
Expand Down Expand Up @@ -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();
Expand All @@ -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,
Expand All @@ -214,23 +235,22 @@ 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') {
setTimelineMap(new Map([...readTimelineMap, ...timelineMap]));
} else if (mode == 'replace') {
setTimelineMap(readTimelineMap);
} else {
return console.error('Unknown insertion mode ' + mode);
return displayErrorMsg('Unknown insertion mode ' + mode);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: as I was looking through the logger code, I noticed that displayError displays the full stacktrace, but displayErrorMsg does not. Is there a reason why we would not want to include the backtrace?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JGreenlee I'm moving this to the "cleanup" column after the merge so that we can revisit how the logger should display errors.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

displayError accepts an Error object as a parameter. displayErrorMsg is used when there is no error and we just want to popup with some text.

Maybe in cases where there is no error, we should be throwing one ourselves and then print out that stacktrace?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in cases where there is no error, we should be throwing one ourselves and then print out that stacktrace?

If we can do so easily, I think that would be helpful. Otherwise, we have to work backwards from the message to the code, and don't really have the full trace

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stack traces are not very useful anyway because all the JS is bundled.

Maybe we could do something like https://stackoverflow.com/questions/58448000/how-to-get-a-readable-stack-trace-for-a-webpack-production-environment-for-a-min

}
}

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) {
Expand All @@ -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;
}

Expand All @@ -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 = {
Expand Down
30 changes: 19 additions & 11 deletions www/js/metrics/MetricsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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'));
}
}

Expand Down
Loading
Loading