SyncFatalError: show nag instead of hard-crashing. (#331)

* SyncFatalError: show nag instead of hard-crashing.

## Issue
When sync fails, we crash the app.

## Ticket
Maybe closes 39 "Better handle both internal and web backend interruptions / downtime"

## Approach
I'm tackling this from the standpoint that (1) sync errors are not that fatal -- we'll just lost a few recent changes (2) network disconnection is the common cause.

## Changes
- If we are offline:
    - Inform user through a nag. All other status is meaningless if we are offline.
- If we are online:
    - If api is STATUS_DOWN, show the existing crash page.
    - If there is a sync error, show a nag saying settings are now potentially unsynchronized, and add a button to retry sync.
    - If there is a chunk error, nag to reload.

* Attempt to detect `status=DOWN`

Previous code resolves the status to either "ok" or "not", which makes the app unable to differentiate between the "degraded" (nag) and "down" (crash) states.
This commit is contained in:
infinite-persistence 2021-11-22 06:30:43 -08:00 committed by GitHub
parent 13cbbc8342
commit 87c3dcc057
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 63 additions and 14 deletions

View file

@ -2221,5 +2221,8 @@
"Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8": "Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8", "Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8": "Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8",
"Choose %asset%": "Choose %asset%", "Choose %asset%": "Choose %asset%",
"Showing %filtered% results of %total%": "Showing %filtered% results of %total%", "Showing %filtered% results of %total%": "Showing %filtered% results of %total%",
"Failed to synchronize settings. Wait a while before retrying.": "Failed to synchronize settings. Wait a while before retrying.",
"You are offline. Check your internet connection.": "You are offline. Check your internet connection.",
"A new version of Odysee is available.": "A new version of Odysee is available.",
"--end--": "--end--" "--end--": "--end--"
} }

View file

@ -134,6 +134,7 @@ function App(props: Props) {
const { pathname, hash, search } = props.location; const { pathname, hash, search } = props.location;
const [upgradeNagClosed, setUpgradeNagClosed] = useState(false); const [upgradeNagClosed, setUpgradeNagClosed] = useState(false);
const [resolvedSubscriptions, setResolvedSubscriptions] = useState(false); const [resolvedSubscriptions, setResolvedSubscriptions] = useState(false);
const [retryingSync, setRetryingSync] = useState(false);
const [sidebarOpen] = usePersistedState('sidebar', true); const [sidebarOpen] = usePersistedState('sidebar', true);
const [seenSunsestMessage, setSeenSunsetMessage] = usePersistedState('lbrytv-sunset', false); const [seenSunsestMessage, setSeenSunsetMessage] = usePersistedState('lbrytv-sunset', false);
const showUpgradeButton = const showUpgradeButton =
@ -153,6 +154,7 @@ function App(props: Props) {
const hasActiveChannelClaim = activeChannelClaim !== undefined; const hasActiveChannelClaim = activeChannelClaim !== undefined;
const isPersonalized = !IS_WEB || hasVerifiedEmail; const isPersonalized = !IS_WEB || hasVerifiedEmail;
const renderFiledrop = !IS_WEB || isAuthenticated; const renderFiledrop = !IS_WEB || isAuthenticated;
const isOnline = navigator.onLine;
let uri; let uri;
try { try {
@ -164,6 +166,50 @@ function App(props: Props) {
setShowAnalyticsNag(false); setShowAnalyticsNag(false);
} }
function getStatusNag() {
// Handle "offline" first. Everything else is meaningless if it's offline.
if (!isOnline) {
return <Nag type="helpful" message={__('You are offline. Check your internet connection.')} />;
}
// Only 1 nag is possible, so show the most important:
if (user === null) {
return <NagNoUser />;
}
if (lbryTvApiStatus === STATUS_DEGRADED || lbryTvApiStatus === STATUS_FAILING) {
if (!shouldHideNag) {
return <NagDegradedPerformance onClose={() => setLbryTvApiStatus(STATUS_OK)} />;
}
}
if (syncFatalError) {
if (!retryingSync) {
return (
<Nag
type="error"
message={__('Failed to synchronize settings. Wait a while before retrying.')}
actionText={__('Retry')}
onClick={() => {
syncLoop(true);
setRetryingSync(true);
setTimeout(() => setRetryingSync(false), 4000);
}}
/>
);
}
} else if (isReloadRequired) {
return (
<Nag
message={__('A new version of Odysee is available.')}
actionText={__('Refresh')}
onClick={() => window.location.reload()}
/>
);
}
}
useEffect(() => { useEffect(() => {
if (userId) { if (userId) {
analytics.setUser(userId); analytics.setUser(userId);
@ -333,7 +379,8 @@ function App(props: Props) {
); );
} }
if (syncFatalError) { if (isOnline && lbryTvApiStatus === STATUS_DOWN) {
// TODO: Rename `SyncFatalError` since it has nothing to do with syncing.
return ( return (
<React.Suspense fallback={null}> <React.Suspense fallback={null}>
<SyncFatalError lbryTvApiStatus={lbryTvApiStatus} /> <SyncFatalError lbryTvApiStatus={lbryTvApiStatus} />
@ -384,22 +431,11 @@ function App(props: Props) {
{fromLbrytvParam && !seenSunsestMessage && !shouldHideNag && ( {fromLbrytvParam && !seenSunsestMessage && !shouldHideNag && (
<NagSunset email={hasVerifiedEmail} onClose={() => setSeenSunsetMessage(true)} /> <NagSunset email={hasVerifiedEmail} onClose={() => setSeenSunsetMessage(true)} />
)} )}
{(lbryTvApiStatus === STATUS_DEGRADED || lbryTvApiStatus === STATUS_FAILING) && !shouldHideNag && (
<NagDegradedPerformance onClose={() => setLbryTvApiStatus(STATUS_OK)} />
)}
{!SIMPLE_SITE && lbryTvApiStatus === STATUS_OK && showAnalyticsNag && !shouldHideNag && ( {!SIMPLE_SITE && lbryTvApiStatus === STATUS_OK && showAnalyticsNag && !shouldHideNag && (
<NagDataCollection onClose={handleAnalyticsDismiss} /> <NagDataCollection onClose={handleAnalyticsDismiss} />
)} )}
{user === null && <NagNoUser />} {getStatusNag()}
</React.Suspense> </React.Suspense>
{isReloadRequired && (
<Nag
message={__('A new version of Odysee is available.')}
actionText={__('Refresh')}
onClick={() => window.location.reload()}
/>
)}
</React.Fragment> </React.Fragment>
)} )}
</div> </div>

View file

@ -5,6 +5,14 @@ import { X_LBRY_AUTH_TOKEN } from 'constants/token';
import fetchWithTimeout from 'util/fetch'; import fetchWithTimeout from 'util/fetch';
const STATUS_GENERAL_STATE = {
// internal/status/status.go#L44
OK: 'ok',
NOT_READY: 'not_ready',
OFFLINE: 'offline',
FAILING: 'failing',
};
const STATUS_TIMEOUT_LIMIT = 10000; const STATUS_TIMEOUT_LIMIT = 10000;
export const STATUS_OK = 'ok'; export const STATUS_OK = 'ok';
export const STATUS_DEGRADED = 'degraded'; export const STATUS_DEGRADED = 'degraded';
@ -33,7 +41,9 @@ export function useDegradedPerformance(onDegradedPerformanceCallback, user) {
fetchWithTimeout(STATUS_TIMEOUT_LIMIT, fetch(STATUS_ENDPOINT, getParams(user))) fetchWithTimeout(STATUS_TIMEOUT_LIMIT, fetch(STATUS_ENDPOINT, getParams(user)))
.then((response) => response.json()) .then((response) => response.json())
.then((status) => { .then((status) => {
if (status.general_state !== STATUS_OK) { if (status.general_state === STATUS_GENERAL_STATE.OFFLINE) {
onDegradedPerformanceCallback(STATUS_DOWN);
} else if (status.general_state !== STATUS_GENERAL_STATE.OK) {
onDegradedPerformanceCallback(STATUS_FAILING); onDegradedPerformanceCallback(STATUS_FAILING);
} }
}) })