Skip to content

Commit

Permalink
navigation doesn't work on error pages (#1519)
Browse files Browse the repository at this point in the history
* navigation doesn't work on error pages

Fixes #1518

* unify API resource thrown errors and disable Sentry logging for them

* test fix
  • Loading branch information
tom2drum authored Jan 22, 2024
1 parent c7dc03b commit 55ac1ea
Show file tree
Hide file tree
Showing 22 changed files with 116 additions and 89 deletions.
1 change: 1 addition & 0 deletions lib/api/useApiFetch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default function useApiFetch() {
},
{
resource: resource.path,
omitSentryErrorLog: true, // disable logging of API errors to Sentry
},
);
}, [ fetch, csrfToken ]);
Expand Down
5 changes: 5 additions & 0 deletions lib/errors/throwOnAbsentParamError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function throwOnAbsentParamError(param: unknown) {
if (!param) {
throw new Error('Required param not provided', { cause: { status: 404 } });
}
}
19 changes: 19 additions & 0 deletions lib/errors/throwOnResourceLoadError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ResourceError, ResourceName } from 'lib/api/resources';

type Params = ({
isError: true;
error: ResourceError<unknown>;
} | {
isError: false;
error: null;
}) & {
resource?: ResourceName;
}

export const RESOURCE_LOAD_ERROR_MESSAGE = 'Resource load error';

export default function throwOnResourceLoadError({ isError, error, resource }: Params) {
if (isError) {
throw Error(RESOURCE_LOAD_ERROR_MESSAGE, { cause: { ...error, resource } as unknown as Error });
}
}
4 changes: 4 additions & 0 deletions lib/sentry/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';

import appConfig from 'configs/app';
import { RESOURCE_LOAD_ERROR_MESSAGE } from 'lib/errors/throwOnResourceLoadError';

const feature = appConfig.features.sentry;

Expand Down Expand Up @@ -59,6 +60,9 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
'The quota has been exceeded',
'Attempt to connect to relay via',
'WebSocket connection failed for URL: wss://relay.walletconnect.com',

// API errors
RESOURCE_LOAD_ERROR_MESSAGE,
],
denyUrls: [
// Facebook flakiness
Expand Down
3 changes: 2 additions & 1 deletion ui/address/AddressDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Address as TAddress } from 'types/api/address';

import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_COUNTERS } from 'stubs/address';
import AddressCounterItem from 'ui/address/details/AddressCounterItem';
Expand Down Expand Up @@ -68,7 +69,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const is422Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 422;

if (addressQuery.isError && is422Error) {
throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error });
throwOnResourceLoadError(addressQuery);
}

if (addressQuery.isError && !is404Error) {
Expand Down
9 changes: 3 additions & 6 deletions ui/block/BlockDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { ResourceError } from 'lib/api/resources';
import getBlockReward from 'lib/block/getBlockReward';
import { GWEI, WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import { space } from 'lib/html-entities';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getQueryParamString from 'lib/router/getQueryParamString';
Expand Down Expand Up @@ -68,12 +69,8 @@ const BlockDetails = ({ query }: Props) => {
}, [ data, router ]);

if (isError) {
if (error?.status === 404) {
throw Error('Block not found', { cause: error as unknown as Error });
}

if (error?.status === 422) {
throw Error('Invalid block number', { cause: error as unknown as Error });
if (error?.status === 404 || error?.status === 422) {
throwOnResourceLoadError({ isError, error });
}

return <DataFetchAlert/>;
Expand Down
13 changes: 5 additions & 8 deletions ui/pages/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { BLOCK } from 'stubs/block';
Expand Down Expand Up @@ -72,14 +74,6 @@ const BlockPageContent = () => {
},
});

if (!heightOrHash) {
throw new Error('Block not found', { cause: { status: 404 } });
}

if (blockQuery.isError) {
throw new Error(undefined, { cause: blockQuery.error });
}

const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> },
{ id: 'txs', title: 'Transactions', component: <TxsWithFrontendSorting query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> },
Expand Down Expand Up @@ -113,6 +107,9 @@ const BlockPageContent = () => {
};
}, [ appProps.referrer ]);

throwOnAbsentParamError(heightOrHash);
throwOnResourceLoadError(blockQuery);

const title = (() => {
switch (blockQuery.data?.type) {
case 'reorg':
Expand Down
5 changes: 2 additions & 3 deletions ui/pages/ContractVerificationForAddress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { SmartContractVerificationMethod } from 'types/api/contract';

import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm';
import useFormConfigQuery from 'ui/contractVerification/useFormConfigQuery';
Expand All @@ -27,9 +28,7 @@ const ContractVerificationForAddress = () => {
},
});

if (contractQuery.isError && contractQuery.error.status === 404) {
throw Error('Not found', { cause: contractQuery.error as unknown as Error });
}
throwOnResourceLoadError(contractQuery);

const configQuery = useFormConfigQuery(Boolean(hash));

Expand Down
29 changes: 16 additions & 13 deletions ui/pages/CsvExport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import type { CsvExportParams } from 'types/client/address';
import type { ResourceName } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import CsvExportForm from 'ui/csvExport/CsvExportForm';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import PageTitle from 'ui/shared/Page/PageTitle';

Expand Down Expand Up @@ -62,7 +63,8 @@ const CsvExport = () => {
const isMobile = useIsMobile();

const addressHash = router.query.address?.toString() || '';
const exportType = router.query.type?.toString() || '';
const exportTypeParam = router.query.type?.toString() || '';
const exportType = isCorrectExportType(exportTypeParam) ? EXPORT_TYPES[exportTypeParam] : null;
const filterTypeFromQuery = router.query.filterType?.toString() || null;
const filterValueFromQuery = router.query.filterValue?.toString();

Expand All @@ -86,27 +88,28 @@ const CsvExport = () => {
};
}, [ appProps.referrer ]);

if (!isCorrectExportType(exportType) || !addressHash || addressQuery.error?.status === 400) {
throw Error('Not found', { cause: { status: 404 } });
throwOnAbsentParamError(addressHash);
throwOnAbsentParamError(exportType);

if (!exportType) {
return null;
}

const filterType = filterTypeFromQuery === EXPORT_TYPES[exportType].filterType ? filterTypeFromQuery : null;
const filterType = filterTypeFromQuery === exportType.filterType ? filterTypeFromQuery : null;
const filterValue = (() => {
if (!filterType || !filterValueFromQuery) {
return null;
}

if (EXPORT_TYPES[exportType].filterValues && !EXPORT_TYPES[exportType].filterValues?.includes(filterValueFromQuery)) {
if (exportType.filterValues && !exportType.filterValues?.includes(filterValueFromQuery)) {
return null;
}

return filterValueFromQuery;
})();

const content = (() => {
if (addressQuery.isError) {
return <DataFetchAlert/>;
}
throwOnResourceLoadError(addressQuery);

if (addressQuery.isPending) {
return <ContentLoader/>;
Expand All @@ -115,10 +118,10 @@ const CsvExport = () => {
return (
<CsvExportForm
hash={ addressHash }
resource={ EXPORT_TYPES[exportType].resource }
resource={ exportType.resource }
filterType={ filterType }
filterValue={ filterValue }
fileNameTemplate={ EXPORT_TYPES[exportType].fileNameTemplate }
fileNameTemplate={ exportType.fileNameTemplate }
/>
);
})();
Expand All @@ -130,7 +133,7 @@ const CsvExport = () => {
backLink={ backLink }
/>
<Flex mb={ 10 } whiteSpace="pre-wrap" flexWrap="wrap">
<span>Export { EXPORT_TYPES[exportType].text } for address </span>
<span>Export { exportType.text } for address </span>
<AddressEntity
address={{ hash: addressHash, is_contract: true, implementation_name: null }}
truncation={ isMobile ? 'constant' : 'dynamic' }
Expand All @@ -139,7 +142,7 @@ const CsvExport = () => {
<span>{ nbsp }</span>
{ filterType && filterValue && <span>with applied filter by { filterType } ({ filterValue }) </span> }
<span>to CSV file. </span>
<span>Exports are limited to the last 10K { EXPORT_TYPES[exportType].text }.</span>
<span>Exports are limited to the last 10K { exportType.text }.</span>
</Flex>
{ content }
</>
Expand Down
5 changes: 2 additions & 3 deletions ui/pages/Marketplace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Box } from '@chakra-ui/react';
import React from 'react';

import config from 'configs/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu';
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
Expand Down Expand Up @@ -32,9 +33,7 @@ const Marketplace = () => {
showDisclaimer,
} = useMarketplace();

if (isError) {
throw new Error('Unable to get apps list', { cause: error });
}
throwOnResourceLoadError(isError && error ? { isError, error } : { isError: false, error: null });

if (!feature.isEnabled) {
return null;
Expand Down
8 changes: 4 additions & 4 deletions ui/pages/MarketplaceApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { route } from 'nextjs-routes';

import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useApiFetch from 'lib/hooks/useFetch';
import * as metadata from 'lib/metadata';
import getQueryParamString from 'lib/router/getQueryParamString';
Expand Down Expand Up @@ -100,7 +101,7 @@ const MarketplaceApp = () => {
const router = useRouter();
const id = getQueryParamString(router.query.id);

const { isPending, isError, error, data } = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>({
const query = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>({
queryKey: [ 'marketplace-apps', id ],
queryFn: async() => {
const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(configUrl, undefined, { resource: 'marketplace-apps' });
Expand All @@ -116,6 +117,7 @@ const MarketplaceApp = () => {
},
enabled: feature.isEnabled,
});
const { data, isPending } = query;

useEffect(() => {
if (data) {
Expand All @@ -126,9 +128,7 @@ const MarketplaceApp = () => {
}
}, [ data ]);

if (isError) {
throw new Error('Unable to load app', { cause: error });
}
throwOnResourceLoadError(query);

return (
<DappscoutIframeProvider
Expand Down
5 changes: 2 additions & 3 deletions ui/pages/NameDomain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { route } from 'nextjs-routes';

import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ENS_DOMAIN } from 'stubs/ENS';
Expand Down Expand Up @@ -42,9 +43,7 @@ const NameDomain = () => {

const tabIndex = useTabIndexFromQuery(tabs);

if (infoQuery.isError) {
throw new Error(undefined, { cause: infoQuery.error });
}
throwOnResourceLoadError(infoQuery);

const isLoading = infoQuery.isPlaceholderData;

Expand Down
5 changes: 2 additions & 3 deletions ui/pages/TokenInstance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { RoutedTab } from 'ui/shared/Tabs/types';

import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as metadata from 'lib/metadata';
import * as regexp from 'lib/regexp';
Expand Down Expand Up @@ -129,9 +130,7 @@ const TokenInstanceContent = () => {
) },
].filter(Boolean);

if (tokenInstanceQuery.isError) {
throw Error('Token instance fetch failed', { cause: tokenInstanceQuery.error });
}
throwOnResourceLoadError(tokenInstanceQuery);

const tokenTag = <Tag isLoading={ tokenInstanceQuery.isPlaceholderData }>{ tokenQuery.data?.type }</Tag>;

Expand Down
11 changes: 4 additions & 7 deletions ui/pages/ZkEvmL2TxnBatch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { RoutedTab } from 'ui/shared/Tabs/types';

import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString';
import { TX_ZKEVM_L2 } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
Expand Down Expand Up @@ -41,13 +43,8 @@ const ZkEvmL2TxnBatch = () => {
},
});

if (!number) {
throw new Error('Tx batch not found', { cause: { status: 404 } });
}

if (batchQuery.isError) {
throw new Error(undefined, { cause: batchQuery.error });
}
throwOnAbsentParamError(number);
throwOnResourceLoadError(batchQuery);

const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <ZkEvmL2TxnBatchDetails query={ batchQuery }/> },
Expand Down
2 changes: 1 addition & 1 deletion ui/shared/AppError/AppError.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ test('status code 500', async({ mount }) => {
});

test('invalid tx hash', async({ mount }) => {
const error = { message: 'Invalid tx hash', cause: { status: 404 } } as Error;
const error = { message: 'Invalid tx hash', cause: { status: 422, resource: 'tx' } } as Error;
const component = await mount(
<TestApp>
<AppError error={ error }/>
Expand Down
7 changes: 4 additions & 3 deletions ui/shared/AppError/AppError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';

import { route } from 'nextjs-routes';

import getErrorCause from 'lib/errors/getErrorCause';
import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode';
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
import getResourceErrorPayload from 'lib/errors/getResourceErrorPayload';
Expand Down Expand Up @@ -36,15 +37,17 @@ const ERROR_TEXTS: Record<string, { title: string; text: string }> = {
const AppError = ({ error, className }: Props) => {
const content = (() => {
const resourceErrorPayload = getResourceErrorPayload(error);
const cause = getErrorCause(error);
const messageInPayload =
resourceErrorPayload &&
typeof resourceErrorPayload === 'object' &&
'message' in resourceErrorPayload &&
typeof resourceErrorPayload.message === 'string' ?
resourceErrorPayload.message :
undefined;
const statusCode = getErrorCauseStatusCode(error) || getErrorObjStatusCode(error);

const isInvalidTxHash = error?.message?.includes('Invalid tx hash');
const isInvalidTxHash = cause && 'resource' in cause && cause.resource === 'tx' && statusCode === 422;
const isBlockConsensus = messageInPayload?.includes('Block lost consensus');

if (isInvalidTxHash) {
Expand All @@ -62,8 +65,6 @@ const AppError = ({ error, className }: Props) => {
return <AppErrorBlockConsensus hash={ hash }/>;
}

const statusCode = getErrorCauseStatusCode(error) || getErrorObjStatusCode(error);

switch (statusCode) {
case 429: {
return <AppErrorTooManyRequests/>;
Expand Down
Loading

0 comments on commit 55ac1ea

Please sign in to comment.