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 { 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))
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user