Commit 4ebc467c authored by Mike Grabowski's avatar Mike Grabowski Committed by GitHub

feat: implement Sentry for state tracking (#6015)

* feat: add sentry middleware

* chore: remove file

* chore: reorg

* chore: update txt

* chore: fix types

* chore

* chore: normalize

* add todo

* Update src/state/logging.ts
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>

* Update src/state/logging.ts
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>

* chore: update type

* nits

---------
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>
parent 7b7e4e61
import { ApolloClient, ApolloLink, concat, HttpLink, InMemoryCache } from '@apollo/client' import { ApolloClient, ApolloLink, concat, HttpLink, InMemoryCache } from '@apollo/client'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import store, { AppState } from '../../state/index' import store from '../../state/index'
const CHAIN_SUBGRAPH_URL: Record<number, string> = { const CHAIN_SUBGRAPH_URL: Record<number, string> = {
[SupportedChainId.MAINNET]: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3', [SupportedChainId.MAINNET]: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',
...@@ -21,7 +21,7 @@ const httpLink = new HttpLink({ uri: CHAIN_SUBGRAPH_URL[SupportedChainId.MAINNET ...@@ -21,7 +21,7 @@ const httpLink = new HttpLink({ uri: CHAIN_SUBGRAPH_URL[SupportedChainId.MAINNET
// For more information: https://www.apollographql.com/docs/react/networking/advanced-http-networking/ // For more information: https://www.apollographql.com/docs/react/networking/advanced-http-networking/
const authMiddleware = new ApolloLink((operation, forward) => { const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers // add the authorization to the headers
const chainId = (store.getState() as AppState).application.chainId const chainId = store.getState().application.chainId
operation.setContext(() => ({ operation.setContext(() => ({
uri: uri:
......
...@@ -37,6 +37,13 @@ if (isSentryEnabled()) { ...@@ -37,6 +37,13 @@ if (isSentryEnabled()) {
Sentry.init({ Sentry.init({
dsn: process.env.REACT_APP_SENTRY_DSN, dsn: process.env.REACT_APP_SENTRY_DSN,
release: process.env.REACT_APP_GIT_COMMIT_HASH, release: process.env.REACT_APP_GIT_COMMIT_HASH,
/**
* TODO(INFRA-143)
* According to Sentry, this shouldn't be necessary, as they default to `3` when not set.
* Unfortunately, that doesn't work right now, so we workaround it by explicitly setting
* the `normalizeDepth` to `10`. This should be removed once the issue is fixed.
*/
normalizeDepth: 10,
}) })
} }
......
...@@ -4,7 +4,7 @@ import { DEFAULT_TXN_DISMISS_MS } from 'constants/misc' ...@@ -4,7 +4,7 @@ import { DEFAULT_TXN_DISMISS_MS } from 'constants/misc'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useAppDispatch, useAppSelector } from 'state/hooks' import { useAppDispatch, useAppSelector } from 'state/hooks'
import { AppState } from '../index' import { AppState } from '../types'
import { import {
addPopup, addPopup,
ApplicationModal, ApplicationModal,
......
...@@ -10,7 +10,7 @@ import { useAppDispatch, useAppSelector } from 'state/hooks' ...@@ -10,7 +10,7 @@ import { useAppDispatch, useAppSelector } from 'state/hooks'
import { useTotalSupply } from '../../hooks/useTotalSupply' import { useTotalSupply } from '../../hooks/useTotalSupply'
import { useV2Pair } from '../../hooks/useV2Pairs' import { useV2Pair } from '../../hooks/useV2Pairs'
import { useTokenBalances } from '../connection/hooks' import { useTokenBalances } from '../connection/hooks'
import { AppState } from '../index' import { AppState } from '../types'
import { Field, typeInput } from './actions' import { Field, typeInput } from './actions'
export function useBurnState(): AppState['burn'] { export function useBurnState(): AppState['burn'] {
......
...@@ -10,7 +10,7 @@ import { useAppDispatch, useAppSelector } from 'state/hooks' ...@@ -10,7 +10,7 @@ import { useAppDispatch, useAppSelector } from 'state/hooks'
import { PositionDetails } from 'types/position' import { PositionDetails } from 'types/position'
import { unwrappedToken } from 'utils/unwrappedToken' import { unwrappedToken } from 'utils/unwrappedToken'
import { AppState } from '../../index' import { AppState } from '../../types'
import { selectPercent } from './actions' import { selectPercent } from './actions'
export function useBurnV3State(): AppState['burnV3'] { export function useBurnV3State(): AppState['burnV3'] {
......
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import { AppDispatch, AppState } from 'state'
export const useAppDispatch = () => useDispatch<AppDispatch>() import store from './index'
export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector
export const useAppDispatch = () => useDispatch<typeof store.dispatch>()
export const useAppSelector: TypedUseSelectorHook<ReturnType<typeof store.getState>> = useSelector
import { configureStore } from '@reduxjs/toolkit' import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query/react' import { setupListeners } from '@reduxjs/toolkit/query/react'
import multicall from 'lib/state/multicall'
import { load, save } from 'redux-localstorage-simple' import { load, save } from 'redux-localstorage-simple'
import { isTestEnv } from 'utils/env' import { isTestEnv } from 'utils/env'
import application from './application/reducer'
import burn from './burn/reducer'
import burnV3 from './burn/v3/reducer'
import connection from './connection/reducer'
import { updateVersion } from './global/actions' import { updateVersion } from './global/actions'
import lists from './lists/reducer' import { sentryEnhancer } from './logging'
import logs from './logs/slice' import reducer from './reducer'
import mint from './mint/reducer'
import mintV3 from './mint/v3/reducer'
import { routingApi } from './routing/slice' import { routingApi } from './routing/slice'
import swap from './swap/reducer'
import transactions from './transactions/reducer'
import user from './user/reducer'
import wallets from './wallets/reducer'
const PERSISTED_KEYS: string[] = ['user', 'transactions', 'lists'] const PERSISTED_KEYS: string[] = ['user', 'transactions', 'lists']
const store = configureStore({ const store = configureStore({
reducer: { reducer,
application, enhancers: (defaultEnhancers) => defaultEnhancers.concat(sentryEnhancer),
user,
connection,
transactions,
wallets,
swap,
mint,
mintV3,
burn,
burnV3,
multicall: multicall.reducer,
lists,
logs,
[routingApi.reducerPath]: routingApi.reducer,
},
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ thunk: true }) getDefaultMiddleware({ thunk: true })
.concat(routingApi.middleware) .concat(routingApi.middleware)
...@@ -50,6 +25,3 @@ store.dispatch(updateVersion()) ...@@ -50,6 +25,3 @@ store.dispatch(updateVersion())
setupListeners(store.dispatch) setupListeners(store.dispatch)
export default store export default store
export type AppState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
...@@ -4,7 +4,7 @@ import { useAppSelector } from 'state/hooks' ...@@ -4,7 +4,7 @@ import { useAppSelector } from 'state/hooks'
import sortByListPriority from 'utils/listSort' import sortByListPriority from 'utils/listSort'
import BROKEN_LIST from '../../constants/tokenLists/broken.tokenlist.json' import BROKEN_LIST from '../../constants/tokenLists/broken.tokenlist.json'
import { AppState } from '../index' import { AppState } from '../types'
import { DEFAULT_ACTIVE_LIST_URLS, UNSUPPORTED_LIST_URLS } from './../../constants/lists' import { DEFAULT_ACTIVE_LIST_URLS, UNSUPPORTED_LIST_URLS } from './../../constants/lists'
export type TokenAddressMap = ChainTokenMap export type TokenAddressMap = ChainTokenMap
......
import * as Sentry from '@sentry/react'
import { AppState } from './types'
/* Utility type to mark all properties of a type as optional */
type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>
}
: T
/**
* This enhancer will automatically store the latest state in Sentry's scope, so that it will be available
* in the Sentry dashboard when an exception happens.
*/
export const sentryEnhancer = Sentry.createReduxEnhancer({
/**
* We don't want to store actions as breadcrumbs in Sentry, so we return null to disable the default behavior.
*/
actionTransformer: () => null,
/**
* We only want to store a subset of the state in Sentry, containing only the relevant parts for debugging.
* Note: This function runs on every state update, so we're keeping it as fast as possible by avoiding any function
* calls and deep object traversals.
*/
stateTransformer: (state: AppState): DeepPartial<AppState> => {
const { application, user, connection, transactions } = state
return {
application: {
fiatOnramp: application.fiatOnramp,
chainId: application.chainId,
openModal: application.openModal,
popupList: application.popupList,
},
user: {
fiatOnrampAcknowledgments: user.fiatOnrampAcknowledgments,
selectedWallet: user.selectedWallet,
lastUpdateVersionTimestamp: user.lastUpdateVersionTimestamp,
matchesDarkMode: user.matchesDarkMode,
userDarkMode: user.userDarkMode,
userLocale: user.userLocale,
userExpertMode: user.userExpertMode,
userClientSideRouter: user.userClientSideRouter,
userHideClosedPositions: user.userHideClosedPositions,
userSlippageTolerance: user.userSlippageTolerance,
userSlippageToleranceHasBeenMigratedToAuto: user.userSlippageToleranceHasBeenMigratedToAuto,
userDeadline: user.userDeadline,
timestamp: user.timestamp,
URLWarningVisible: user.URLWarningVisible,
showSurveyPopup: user.showSurveyPopup,
},
connection: {
errorByConnectionType: connection.errorByConnectionType,
},
transactions,
}
},
})
...@@ -10,7 +10,7 @@ import { useAppDispatch, useAppSelector } from 'state/hooks' ...@@ -10,7 +10,7 @@ import { useAppDispatch, useAppSelector } from 'state/hooks'
import { useTotalSupply } from '../../hooks/useTotalSupply' import { useTotalSupply } from '../../hooks/useTotalSupply'
import { PairState, useV2Pair } from '../../hooks/useV2Pairs' import { PairState, useV2Pair } from '../../hooks/useV2Pairs'
import { useCurrencyBalances } from '../connection/hooks' import { useCurrencyBalances } from '../connection/hooks'
import { AppState } from '../index' import { AppState } from '../types'
import { Field, typeInput } from './actions' import { Field, typeInput } from './actions'
const ZERO = JSBI.BigInt(0) const ZERO = JSBI.BigInt(0)
......
...@@ -24,7 +24,7 @@ import { replaceURLParam } from 'utils/routes' ...@@ -24,7 +24,7 @@ import { replaceURLParam } from 'utils/routes'
import { BIG_INT_ZERO } from '../../../constants/misc' import { BIG_INT_ZERO } from '../../../constants/misc'
import { PoolState } from '../../../hooks/usePools' import { PoolState } from '../../../hooks/usePools'
import { useCurrencyBalances } from '../../connection/hooks' import { useCurrencyBalances } from '../../connection/hooks'
import { AppState } from '../../index' import { AppState } from '../../types'
import { import {
Bound, Bound,
Field, Field,
......
import multicall from 'lib/state/multicall'
import application from './application/reducer'
import burn from './burn/reducer'
import burnV3 from './burn/v3/reducer'
import connection from './connection/reducer'
import lists from './lists/reducer'
import logs from './logs/slice'
import mint from './mint/reducer'
import mintV3 from './mint/v3/reducer'
import { routingApi } from './routing/slice'
import swap from './swap/reducer'
import transactions from './transactions/reducer'
import user from './user/reducer'
import wallets from './wallets/reducer'
export default {
application,
user,
connection,
transactions,
wallets,
swap,
mint,
mintV3,
burn,
burnV3,
multicall: multicall.reducer,
lists,
logs,
[routingApi.reducerPath]: routingApi.reducer,
}
...@@ -16,7 +16,7 @@ import useENS from '../../hooks/useENS' ...@@ -16,7 +16,7 @@ import useENS from '../../hooks/useENS'
import useParsedQueryString from '../../hooks/useParsedQueryString' import useParsedQueryString from '../../hooks/useParsedQueryString'
import { isAddress } from '../../utils' import { isAddress } from '../../utils'
import { useCurrencyBalances } from '../connection/hooks' import { useCurrencyBalances } from '../connection/hooks'
import { AppState } from '../index' import { AppState } from '../types'
import { Field, replaceSwapState, selectCurrency, setRecipient, switchCurrencies, typeInput } from './actions' import { Field, replaceSwapState, selectCurrency, setRecipient, switchCurrencies, typeInput } from './actions'
import { SwapState } from './reducer' import { SwapState } from './reducer'
......
import { Reducer } from '@reduxjs/toolkit'
import reducer from './reducer'
/* Utility type to extract state type out of a @reduxjs/toolkit Reducer type */
type GetState<T> = T extends Reducer<infer State> ? State : never
export type AppState = {
[K in keyof typeof reducer]: GetState<typeof reducer[K]>
}
...@@ -13,7 +13,7 @@ import { UserAddedToken } from 'types/tokens' ...@@ -13,7 +13,7 @@ import { UserAddedToken } from 'types/tokens'
import { V2_FACTORY_ADDRESSES } from '../../constants/addresses' import { V2_FACTORY_ADDRESSES } from '../../constants/addresses'
import { BASES_TO_TRACK_LIQUIDITY_FOR, PINNED_PAIRS } from '../../constants/routing' import { BASES_TO_TRACK_LIQUIDITY_FOR, PINNED_PAIRS } from '../../constants/routing'
import { useAllTokens } from '../../hooks/Tokens' import { useAllTokens } from '../../hooks/Tokens'
import { AppState } from '../index' import { AppState } from '../types'
import { import {
addSerializedPair, addSerializedPair,
addSerializedToken, addSerializedToken,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment