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:
katspaugh 2021-05-20 10:49:50 +02:00 committed by GitHub
parent 6f2ae5af54
commit b0fadee951
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 194 additions and 48 deletions

View 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()
})
})
})

View 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

View 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

View File

@ -1,7 +1,11 @@
import { List } from 'immutable' import { List } from 'immutable'
import { Dispatch } from 'redux' 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 { addTokens } from 'src/logic/tokens/store/actions/addTokens'
import { makeToken, Token } from 'src/logic/tokens/store/model/token' import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { updateSafe } from 'src/logic/safe/store/actions/updateSafe' 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 BigNumber from 'bignumber.js'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses' import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses'
import { Errors, logError } from 'src/logic/exceptions/CodedException'
export type BalanceRecord = { export type BalanceRecord = {
tokenAddress?: string tokenAddress?: string
@ -50,39 +55,38 @@ export const fetchSafeTokens = (safeAddress: string, currencySelected?: string)
dispatch: Dispatch, dispatch: Dispatch,
getState: () => AppReduxState, getState: () => AppReduxState,
): Promise<void> => { ): Promise<void> => {
const state = getState()
const safe = safeSelector(state)
if (!safe) {
return
}
const selectedCurrency = currentCurrencySelector(state)
let tokenCurrenciesBalances: BalanceEndpoint
try { try {
const state = getState() tokenCurrenciesBalances = await fetchTokenCurrenciesBalances({
const safe = safeSelector(state)
if (!safe) {
return
}
const selectedCurrency = currentCurrencySelector(state)
const tokenCurrenciesBalances = await fetchTokenCurrenciesBalances({
safeAddress, safeAddress,
selectedCurrency: currencySelected ?? selectedCurrency, selectedCurrency: currencySelected ?? selectedCurrency,
}) })
} catch (e) {
const { balances, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce<ExtractedData>( logError(Errors._601, e.message, false)
extractDataFromResult, return
{
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)
} }
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))
} }

View File

@ -12,6 +12,8 @@ import { AppReduxState } from 'src/store'
import { ensureOnce } from 'src/utils/singleton' import { ensureOnce } from 'src/utils/singleton'
import { ThunkDispatch } from 'redux-thunk' import { ThunkDispatch } from 'redux-thunk'
import { AnyAction } from 'redux' import { AnyAction } from 'redux'
import { Errors, logError } from 'src/logic/exceptions/CodedException'
import { TokenResult } from '../../api/fetchErc20AndErc721AssetsList'
const createStandardTokenContract = async () => { const createStandardTokenContract = async () => {
const web3 = getWeb3() const web3 = getWeb3()
@ -52,23 +54,24 @@ export const fetchTokens = () => async (
dispatch: ThunkDispatch<AppReduxState, undefined, AnyAction>, dispatch: ThunkDispatch<AppReduxState, undefined, AnyAction>,
getState: () => AppReduxState, getState: () => AppReduxState,
): Promise<void> => { ): Promise<void> => {
const currentSavedTokens = tokensSelector(getState())
let tokenList: TokenResult[]
try { try {
const currentSavedTokens = tokensSelector(getState()) const resp = await fetchErc20AndErc721AssetsList()
tokenList = resp.data.results
const { } catch (e) {
data: { results: tokenList }, logError(Errors._600, e.message, false)
} = await fetchErc20AndErc721AssetsList() 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))
} catch (err) {
console.error('Error fetching token list', err)
} }
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))
} }