Skip to content
This repository has been archived by the owner on Jun 24, 2022. It is now read-only.

[SENTRY] - Report order submission errors and update ignore list #2531

Merged
merged 5 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 76 additions & 40 deletions src/custom/api/gnosisProtocol/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { SupportedChainId as ChainId, SupportedChainId } from 'constants/chains'
import { OrderKind, QuoteQuery } from '@gnosis.pm/gp-v2-contracts'
import { stringify } from 'qs'
import { getSigningSchemeApiValue, OrderCancellation, OrderCreation, SigningSchemeValue } from 'utils/signatures'
import {
getSigningSchemeApiValue,
OrderCancellation,
OrderCreation,
SigningSchemeValue,
UnsignedOrder,
} from 'utils/signatures'
import { APP_DATA_HASH, GAS_FEE_ENDPOINTS } from 'constants/index'
import { registerOnWindow } from 'utils/misc'
import { isBarn, isDev, isLocal, isPr } from '../../utils/environments'
Expand All @@ -21,10 +27,11 @@ import { FeeQuoteParams, PriceInformation, PriceQuoteParams, SimpleGetQuoteRespo

import { DEFAULT_NETWORK_FOR_LISTS } from 'constants/lists'
import * as Sentry from '@sentry/browser'
import { constructSentryError } from 'utils/logging'
import { checkAndThrowIfJsonSerialisableError, constructSentryError } from 'utils/logging'
import { ZERO_ADDRESS } from 'constants/misc'
import { getAppDataHash } from 'constants/appDataHash'
import { GpPriceStrategy } from 'hooks/useGetGpPriceStrategy'
import { Context } from '@sentry/types'

function getGnosisProtocolUrl(): Partial<Record<ChainId, string>> {
if (isLocal || isDev || isPr || isBarn) {
Expand Down Expand Up @@ -220,23 +227,15 @@ export async function sendOrder(params: { chainId: ChainId; order: OrderCreation
const { chainId, order, owner } = params
console.log(`[api:${API_NAME}] Post signed order for network`, chainId, order)

// Call API
const response = await _post(chainId, `/orders`, {
const orderParams = {
...order,
signingScheme: getSigningSchemeApiValue(order.signingScheme),
from: owner,
})

// Handle response
if (!response.ok) {
// Raise an exception
const errorMessage = await OperatorError.getErrorFromStatusCode(response, 'create')
throw new Error(errorMessage)
}
// Call API
const response = await _post(chainId, `/orders`, orderParams)

const uid = (await response.json()) as string
console.log(`[api:${API_NAME}] Success posting the signed order`, uid)
return uid
return _handleOrderResponse<string, typeof orderParams>(response, orderParams)
}

type OrderCancellationParams = {
Expand Down Expand Up @@ -275,52 +274,89 @@ const UNHANDLED_ORDER_ERROR: ApiErrorObject = {
description: ApiErrorCodeDetails.UNHANDLED_CREATE_ERROR,
}

function _handleError<P extends Context>(error: any, response: Response, params: P, operation: 'ORDER' | 'QUOTE') {
// Create a new sentry error OR
// use the previously created and rethrown error from the try block
const sentryError =
error?.sentryError ||
constructSentryError(error, response, {
message: error?.message || error,
name: `[${operation}-ERROR] - Unmapped ${operation} Error`,
})
// Create the error tags or use the previously constructed ones from the try block
const tags = error?.tags || { errorType: operation, backendErrorCode: response.status }

// report to sentry
Sentry.captureException(sentryError, {
tags,
// TODO: change/remove this in context update pr
contexts: { params: { ...params } },
})

return error?.baseError || error
}

async function _handleOrderResponse<T = any, P extends UnsignedOrder = UnsignedOrder>(
response: Response,
params: P
): Promise<T> {
try {
// Handle response
if (!response.ok) {
// Raise an exception
const [errorObject, description] = await Promise.all<[Promise<ApiErrorObject>, Promise<string>]>([
response.json(),
OperatorError.getErrorFromStatusCode(response, 'create'),
])
// create the OperatorError from the constructed error message and the original error
const error = new OperatorError(Object.assign({}, errorObject, { description }))

// we need to create a sentry error and keep the original mapped quote error
throw constructSentryError(error, response, {
message: `${error.description}`,
name: `[${error.name}] - ${error.type}`,
optionalTags: {
orderErrorType: error.type,
},
})
} else {
const uid = await response.json()
console.log(`[api:${API_NAME}] Success posting the signed order`, JSON.stringify(uid))
return uid
}
} catch (error) {
throw _handleError(error, response, params, 'ORDER')
}
}

async function _handleQuoteResponse<T = any, P extends FeeQuoteParams = FeeQuoteParams>(
response: Response,
params: P
): Promise<T> {
try {
if (!response.ok) {
// don't attempt json parse if not json response...
if (response.headers.get('Content-Type') !== 'application/json') {
throw new Error(`${response.status} error occurred. ${response.statusText}`)
}
checkAndThrowIfJsonSerialisableError(response)

const errorObj: ApiErrorObject = await response.json()

// we need to map the backend error codes to match our own for quotes
const mappedError = mapOperatorErrorToQuoteError(errorObj)
const quoteError = new QuoteError(mappedError)
const error = new QuoteError(mappedError)

// we need to create a sentry error and keep the original mapped quote error
throw constructSentryError(quoteError, response, {
message: `${quoteError.description} [sellToken: ${params.sellToken}]//[buyToken: ${params.buyToken}]`,
name: `[${quoteError.name}] - ${quoteError.type}`,
throw constructSentryError(error, response, {
message: `${error.description}`,
name: `[${error.name}] - ${error.type}`,
optionalTags: {
quoteErrorType: quoteError.type,
quoteErrorType: error.type,
},
})
} else {
return response.json()
}
} catch (error) {
// Create a new sentry error OR
// use the previously created and rethrown error from the try block
const sentryError =
error?.sentryError ||
constructSentryError(error, response, {
message: `Potential backend error detected - status code: ${response.status}`,
name: '[HandleQuoteResponse] - Unmapped Quote Error',
})
// Create the error tags or use the previously constructed ones from the try block
const tags = error?.tags || { errorType: 'handleQuoteResponse', backendErrorCode: response.status }

// report to sentry
Sentry.captureException(sentryError, {
tags,
contexts: { params: { ...params } },
})

throw error?.baseError || error
throw _handleError(error, response, params, 'QUOTE')
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/custom/api/gnosisProtocol/errors/OperatorError.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { checkAndThrowIfJsonSerialisableError } from 'utils/logging'

type ApiActionType = 'get' | 'create' | 'delete'

export interface ApiErrorObject {
Expand Down Expand Up @@ -97,7 +99,11 @@ export default class OperatorError extends Error {

public static async getErrorMessage(response: Response, action: ApiActionType) {
try {
const orderPostError: ApiErrorObject = await response.json()
// don't attempt json parse if not json response...
checkAndThrowIfJsonSerialisableError(response)
// clone response body
const clonedResponse = response.clone()
const orderPostError: ApiErrorObject = await clonedResponse.json()

if (orderPostError.errorType) {
const errorMessage = OperatorError.apiErrorDetails[orderPostError.errorType]
Expand All @@ -112,7 +118,7 @@ export default class OperatorError extends Error {
return _mapActionToErrorDetail(action)
}
}
static async getErrorFromStatusCode(response: Response, action: 'create' | 'delete') {
static async getErrorFromStatusCode(response: Response, action: 'create' | 'delete'): Promise<string> {
switch (response.status) {
case 400:
case 404:
Expand Down
2 changes: 2 additions & 0 deletions src/custom/api/gnosisProtocol/errors/QuoteError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export enum GpQuoteErrorCodes {
UNHANDLED_ERROR = 'UNHANDLED_ERROR',
}

export const SENTRY_IGNORED_GP_QUOTE_ERRORS = [GpQuoteErrorCodes.FeeExceedsFrom]

export enum GpQuoteErrorDetails {
UnsupportedToken = 'One of the tokens you are trading is unsupported. Please read the FAQ for more info.',
InsufficientLiquidity = 'Token pair selected has insufficient liquidity.',
Expand Down
2 changes: 2 additions & 0 deletions src/custom/pages/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { environmentName } from 'utils/environments'
import { useFilterEmptyQueryParams } from 'hooks/useFilterEmptyQueryParams'
import RedirectAnySwapAffectedUsers from 'pages/error/AnySwapAffectedUsers/RedirectAnySwapAffectedUsers'
import { IS_CLAIMING_ENABLED } from 'pages/Claim/const'
import { SENTRY_IGNORED_GP_QUOTE_ERRORS } from 'api/gnosisProtocol/errors/QuoteError'

const SENTRY_DSN = process.env.REACT_APP_SENTRY_DSN
const SENTRY_TRACES_SAMPLE_RATE = process.env.REACT_APP_SENTRY_TRACES_SAMPLE_RATE
Expand All @@ -31,6 +32,7 @@ if (SENTRY_DSN) {
integrations: [new Integrations.BrowserTracing()],
release: 'CowSwap@v' + version,
environment: environmentName,
ignoreErrors: [...SENTRY_IGNORED_GP_QUOTE_ERRORS],

// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
Expand Down
8 changes: 8 additions & 0 deletions src/custom/utils/logging/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,11 @@ export function constructSentryError(

return { baseError, sentryError: constructedError, tags }
}

// checks response for non json/application return type and throw appropriate error
export function checkAndThrowIfJsonSerialisableError(response: Response) {
// don't attempt json parse if not json response...
if (response.headers.get('Content-Type') !== 'application/json') {
throw new Error(`Error code ${response.status} occurred`)
}
}