mirror of
https://github.com/status-im/safe-react.git
synced 2025-02-12 09:37:05 +00:00
Chore: coded exceptions registry and utils (#2283)
* Coded exceptions + registry * adding throwError function * Log to sentry * Chore: coded exceptions registry and utils (#2161) * Add error 600 for token fetching * Fix constants type error * Log only the error message on prod * Add error 601 * PR feedback from Nico * Add readonly for the public props * Add isTracked and set it to false for 601 * Add a comment on how to add new errors * Rename var * Replace the registry object with an enum * Add a reverse lookup map * Duplicate the code in the enum Co-authored-by: nicosampler <nf.dominguez.87@gmail.com> Co-authored-by: nicolas <nicosampler@users.noreply.github.com> Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
parent
6f2ae5af54
commit
b0fadee951
83
src/logic/exceptions/CodedException.test.ts
Normal file
83
src/logic/exceptions/CodedException.test.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { Errors, logError, CodedException } from './CodedException'
|
||||
import * as constants from 'src/utils/constants'
|
||||
import * as Sentry from '@sentry/react'
|
||||
|
||||
jest.mock('@sentry/react')
|
||||
jest.mock('src/utils/constants')
|
||||
|
||||
describe('CodedException', () => {
|
||||
it('throws an error if code is not found', () => {
|
||||
expect(Errors.___0).toBe('0: No such error code')
|
||||
|
||||
expect(() => {
|
||||
new CodedException('weird error' as any)
|
||||
}).toThrow('0: No such error code (weird error)')
|
||||
})
|
||||
|
||||
it('creates an error', () => {
|
||||
const err = new CodedException(Errors._100)
|
||||
expect(err.message).toBe('100: Invalid input in the address field')
|
||||
expect(err.code).toBe(100)
|
||||
})
|
||||
|
||||
it('creates an error with an extra message', () => {
|
||||
const err = new CodedException(Errors._100, '0x123')
|
||||
expect(err.message).toBe('100: Invalid input in the address field (0x123)')
|
||||
expect(err.code).toBe(100)
|
||||
})
|
||||
|
||||
describe('Logging', () => {
|
||||
beforeAll(() => {
|
||||
jest.mock('console')
|
||||
console.error = jest.fn()
|
||||
})
|
||||
afterEach(() => {
|
||||
jest.unmock('console')
|
||||
;(constants as any).IS_PRODUCTION = false
|
||||
})
|
||||
|
||||
it('logs to the console', () => {
|
||||
const err = logError(Errors._100, '123')
|
||||
expect(err.message).toBe('100: Invalid input in the address field (123)')
|
||||
expect(console.error).toHaveBeenCalledWith(err)
|
||||
})
|
||||
|
||||
it('logs to the console via the public log method', () => {
|
||||
const err = new CodedException(Errors._601)
|
||||
expect(err.message).toBe('601: Error fetching balances')
|
||||
expect(console.error).not.toHaveBeenCalled()
|
||||
err.log()
|
||||
expect(console.error).toHaveBeenCalledWith(err)
|
||||
})
|
||||
|
||||
it('logs only the error message on prod', () => {
|
||||
;(constants as any).IS_PRODUCTION = true
|
||||
logError(Errors._100)
|
||||
expect(console.error).toHaveBeenCalledWith('100: Invalid input in the address field')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tracking', () => {
|
||||
afterEach(() => {
|
||||
;(constants as any).IS_PRODUCTION = false
|
||||
})
|
||||
|
||||
it('tracks using Sentry on production', () => {
|
||||
;(constants as any).IS_PRODUCTION = true
|
||||
logError(Errors._100)
|
||||
expect(Sentry.captureException).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("doesn't track when isTracked is false", () => {
|
||||
;(constants as any).IS_PRODUCTION = true
|
||||
logError(Errors._100, '', false)
|
||||
expect(Sentry.captureException).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not track using Sentry in non-production envs', () => {
|
||||
;(constants as any).IS_PRODUCTION = false
|
||||
logError(Errors._100)
|
||||
expect(Sentry.captureException).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
42
src/logic/exceptions/CodedException.ts
Normal file
42
src/logic/exceptions/CodedException.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import * as Sentry from '@sentry/react'
|
||||
import ErrorCodes from './registry'
|
||||
import { IS_PRODUCTION } from 'src/utils/constants'
|
||||
|
||||
export class CodedException extends Error {
|
||||
public readonly message: string
|
||||
public readonly code: number
|
||||
|
||||
constructor(content: ErrorCodes, extraMessage?: string) {
|
||||
super()
|
||||
|
||||
const codePrefix = content.split(':')[0]
|
||||
const code = Number(codePrefix)
|
||||
if (isNaN(code)) {
|
||||
throw new CodedException(ErrorCodes.___0, codePrefix)
|
||||
}
|
||||
|
||||
const extraInfo = extraMessage ? ` (${extraMessage})` : ''
|
||||
this.message = `${content}${extraInfo}`
|
||||
this.code = code
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the error in the console and send to Sentry
|
||||
*/
|
||||
public log(isTracked = true): void {
|
||||
// Log only the message on prod, and the full error on dev
|
||||
console.error(IS_PRODUCTION ? this.message : this)
|
||||
|
||||
if (IS_PRODUCTION && isTracked) {
|
||||
Sentry.captureException(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logError(content: ErrorCodes, extraMessage?: string, isTracked?: boolean): CodedException {
|
||||
const error = new CodedException(content, extraMessage)
|
||||
error.log(isTracked)
|
||||
return error
|
||||
}
|
||||
|
||||
export const Errors = ErrorCodes
|
14
src/logic/exceptions/registry.ts
Normal file
14
src/logic/exceptions/registry.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* When creating a new error type, please try to group them semantically
|
||||
* with the existing errors in the same hundred. For example, if it's
|
||||
* related to fetching data from the backend, add it to the 6xx errors.
|
||||
* This is not a hard requirement, just a useful convention.
|
||||
*/
|
||||
enum ErrorCodes {
|
||||
___0 = '0: No such error code',
|
||||
_100 = '100: Invalid input in the address field',
|
||||
_600 = '600: Error fetching token list',
|
||||
_601 = '601: Error fetching balances',
|
||||
}
|
||||
|
||||
export default ErrorCodes
|
@ -1,7 +1,11 @@
|
||||
import { List } from 'immutable'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
import { fetchTokenCurrenciesBalances, TokenBalance } from 'src/logic/safe/api/fetchTokenCurrenciesBalances'
|
||||
import {
|
||||
BalanceEndpoint,
|
||||
fetchTokenCurrenciesBalances,
|
||||
TokenBalance,
|
||||
} from 'src/logic/safe/api/fetchTokenCurrenciesBalances'
|
||||
import { addTokens } from 'src/logic/tokens/store/actions/addTokens'
|
||||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||
import { updateSafe } from 'src/logic/safe/store/actions/updateSafe'
|
||||
@ -11,6 +15,7 @@ import { safeSelector } from 'src/logic/safe/store/selectors'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
|
||||
import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { Errors, logError } from 'src/logic/exceptions/CodedException'
|
||||
|
||||
export type BalanceRecord = {
|
||||
tokenAddress?: string
|
||||
@ -50,39 +55,38 @@ export const fetchSafeTokens = (safeAddress: string, currencySelected?: string)
|
||||
dispatch: Dispatch,
|
||||
getState: () => AppReduxState,
|
||||
): Promise<void> => {
|
||||
const state = getState()
|
||||
const safe = safeSelector(state)
|
||||
|
||||
if (!safe) {
|
||||
return
|
||||
}
|
||||
const selectedCurrency = currentCurrencySelector(state)
|
||||
|
||||
let tokenCurrenciesBalances: BalanceEndpoint
|
||||
try {
|
||||
const state = getState()
|
||||
const safe = safeSelector(state)
|
||||
|
||||
if (!safe) {
|
||||
return
|
||||
}
|
||||
const selectedCurrency = currentCurrencySelector(state)
|
||||
|
||||
const tokenCurrenciesBalances = await fetchTokenCurrenciesBalances({
|
||||
tokenCurrenciesBalances = await fetchTokenCurrenciesBalances({
|
||||
safeAddress,
|
||||
selectedCurrency: currencySelected ?? selectedCurrency,
|
||||
})
|
||||
|
||||
const { balances, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce<ExtractedData>(
|
||||
extractDataFromResult,
|
||||
{
|
||||
balances: [],
|
||||
ethBalance: '0',
|
||||
tokens: List(),
|
||||
},
|
||||
)
|
||||
|
||||
dispatch(
|
||||
updateSafe({
|
||||
address: safeAddress,
|
||||
balances,
|
||||
ethBalance,
|
||||
totalFiatBalance: new BigNumber(tokenCurrenciesBalances.fiatTotal).toFixed(2),
|
||||
}),
|
||||
)
|
||||
dispatch(addTokens(tokens))
|
||||
} catch (err) {
|
||||
console.error('Error fetching active token list', err)
|
||||
} catch (e) {
|
||||
logError(Errors._601, e.message, false)
|
||||
return
|
||||
}
|
||||
|
||||
const { balances, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce<ExtractedData>(extractDataFromResult, {
|
||||
balances: [],
|
||||
ethBalance: '0',
|
||||
tokens: List(),
|
||||
})
|
||||
|
||||
dispatch(
|
||||
updateSafe({
|
||||
address: safeAddress,
|
||||
balances,
|
||||
ethBalance,
|
||||
totalFiatBalance: new BigNumber(tokenCurrenciesBalances.fiatTotal).toFixed(2),
|
||||
}),
|
||||
)
|
||||
dispatch(addTokens(tokens))
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ import { AppReduxState } from 'src/store'
|
||||
import { ensureOnce } from 'src/utils/singleton'
|
||||
import { ThunkDispatch } from 'redux-thunk'
|
||||
import { AnyAction } from 'redux'
|
||||
import { Errors, logError } from 'src/logic/exceptions/CodedException'
|
||||
import { TokenResult } from '../../api/fetchErc20AndErc721AssetsList'
|
||||
|
||||
const createStandardTokenContract = async () => {
|
||||
const web3 = getWeb3()
|
||||
@ -52,23 +54,24 @@ export const fetchTokens = () => async (
|
||||
dispatch: ThunkDispatch<AppReduxState, undefined, AnyAction>,
|
||||
getState: () => AppReduxState,
|
||||
): Promise<void> => {
|
||||
const currentSavedTokens = tokensSelector(getState())
|
||||
|
||||
let tokenList: TokenResult[]
|
||||
try {
|
||||
const currentSavedTokens = tokensSelector(getState())
|
||||
|
||||
const {
|
||||
data: { results: tokenList },
|
||||
} = await fetchErc20AndErc721AssetsList()
|
||||
|
||||
const erc20Tokens = tokenList.filter((token) => token.type.toLowerCase() === 'erc20')
|
||||
|
||||
if (currentSavedTokens?.size === erc20Tokens.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const tokens = List(erc20Tokens.map((token) => makeToken(token)))
|
||||
|
||||
dispatch(addTokens(tokens))
|
||||
} catch (err) {
|
||||
console.error('Error fetching token list', err)
|
||||
const resp = await fetchErc20AndErc721AssetsList()
|
||||
tokenList = resp.data.results
|
||||
} catch (e) {
|
||||
logError(Errors._600, e.message, false)
|
||||
return
|
||||
}
|
||||
|
||||
const erc20Tokens = tokenList.filter((token) => token.type.toLowerCase() === 'erc20')
|
||||
|
||||
if (currentSavedTokens?.size === erc20Tokens.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const tokens = List(erc20Tokens.map((token) => makeToken(token)))
|
||||
|
||||
dispatch(addTokens(tokens))
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user