2025-05-28 15:36:51 -07:00

228 lines
5.9 KiB
TypeScript

import axios from "axios";
import { merge } from "lodash";
import {
ApiAdapterDataException,
ApiAdapterException,
ApiAdapterUrlException,
ApiException,
AuthException,
AuthMissingException,
Constants,
} from "@dndbeyond/character-rules-engine/es";
import { appEnvActions } from "../../actions";
import { appInfoActions } from "../../actions/appInfo";
import AppErrorTypeEnum from "../../constants/AppErrorTypeEnum";
import * as HttpStatusCodes from "../../constants/HttpStatusCodes";
import { appEnvSelectors } from "../../selectors";
import { StateStoreUtils } from "../../stores";
import { AppNotificationUtils, ErrorCustomTags, ErrorUtils } from "../../utils";
import { COALESCED_ERROR_CODES } from "./constants";
let hasDispatchedAuthException: boolean = false;
/**
*
* @param url
*/
export function processApiUrl(url: string): string {
let match = url.match(/^(.*\/character\/v\d+\/character)\/\d+$/);
if (match) {
return [match[1], "{id}"].join("/");
}
return url;
}
/**
*
* @param error
* @param extraContextData
*/
export function getApiAdapterUrlExceptionContextData(
error: ApiAdapterUrlException,
extraContextData: Record<string, any>
): Record<string, any> {
let errorContextData: Record<string, any> = error.contextData ?? {};
let errorMessageContextData: Record<string, any> = {
errorMessage: error.message,
errorUrl: error.url,
errorMethod: error.method,
};
return merge({}, errorContextData, errorMessageContextData, extraContextData);
}
/**
*
* @param error
*/
export function getApiAdapterUrlExceptionCustomTags(
error: ApiAdapterUrlException
): ErrorCustomTags {
return {
serverErrorId: error.contextData?.serverErrorData?.errorCode ?? null,
};
}
/**
*
* @param error
* @param messagePrefix
*/
export function getApiAdapterUrlExceptionMessage(
error: ApiAdapterUrlException,
messagePrefix: string
): string {
if (error.errorCode === null) {
return "Unknown Client Request Failure";
}
if (COALESCED_ERROR_CODES.includes(error.errorCode)) {
return [messagePrefix, error.errorCode].join(" - ");
}
return [
messagePrefix,
error.errorCode,
error.method ?? "UNKNOWN Method",
error.url ? processApiUrl(error.url) : "UNKNOWN URL",
].join(" - ");
}
/**
*
* @param error
*/
export function getApiAdapterUrlExceptionAppErrorType(
error: ApiAdapterUrlException
): AppErrorTypeEnum {
if (error.errorCode === HttpStatusCodes.FORBIDDEN) {
return AppErrorTypeEnum.ACCESS_DENIED;
}
if (error.errorCode === HttpStatusCodes.NOT_FOUND) {
return AppErrorTypeEnum.NOT_FOUND;
}
return AppErrorTypeEnum.API_FAIL;
}
/**
*
* @param error
* @param contextData
*/
export function logError(
error: Error,
contextData: Record<string, any> = {}
): null {
// This function returns null because the Rules Engine config expects a log method
// that returns string | null. See packages/rules-engine/src/config/typings.ts
if (process.env.NODE_ENV !== "production") {
console.log("***************");
console.log("The following error happened:");
console.log(error);
console.log("***************");
}
if (axios.isCancel(error)) {
// only do something if not canceled, cancellation should be fine
return null;
}
let appErrorType: AppErrorTypeEnum = AppErrorTypeEnum.GENERIC;
let message: string | null = null;
let shouldLogError: boolean = true;
let customTags: ErrorCustomTags = {};
if (error instanceof ApiException) {
appErrorType = getApiAdapterUrlExceptionAppErrorType(error);
contextData = getApiAdapterUrlExceptionContextData(error, contextData);
customTags = getApiAdapterUrlExceptionCustomTags(error);
message = getApiAdapterUrlExceptionMessage(error, "API");
} else if (error instanceof ApiAdapterDataException) {
appErrorType = getApiAdapterUrlExceptionAppErrorType(error);
contextData = getApiAdapterUrlExceptionContextData(error, contextData);
customTags = getApiAdapterUrlExceptionCustomTags(error);
message = getApiAdapterUrlExceptionMessage(error, "API Data");
} else if (error instanceof AuthException) {
appErrorType = AppErrorTypeEnum.AUTH_FAIL;
message = "Auth Failure";
// We could emit multiple auth exceptions when we make parallel requests
// AuthException isn't recoverable, so it should be fine to only log the first one
if (hasDispatchedAuthException) {
shouldLogError = false;
} else {
hasDispatchedAuthException = true;
}
} else if (error instanceof AuthMissingException) {
appErrorType = AppErrorTypeEnum.AUTH_MISSING;
shouldLogError = false;
}
if (shouldLogError) {
if (message === null) {
ErrorUtils.dispatchException(error, null, customTags);
} else {
ErrorUtils.dispatchError(
message,
contextData,
customTags,
Constants.LogMessageType.ERROR
);
}
}
const store = StateStoreUtils.getAppStore();
if (store !== null) {
const diceUrl = appEnvSelectors.getDiceFeatureConfiguration(
store.getState()
).apiEndpoint;
if (error instanceof ApiException && error?.url?.includes(diceUrl)) {
store.dispatch(
appEnvActions.dataSet({
diceEnabled: false,
})
);
AppNotificationUtils.dispatchError(
"Dice Service Error",
"The dice service is currently experiencing issues. We have disabled it temporarily as a result."
);
} else {
store.dispatch(appInfoActions.errorSet(appErrorType));
}
}
return null;
}
/**
*
* @param message
* @param type
* @param contextData
*/
export function logMessage(
message: string,
type: Constants.LogMessageType,
contextData: Record<string, any> = {}
): null {
ErrorUtils.dispatchError(message, contextData, null, type);
return null;
}
/**
*
* @param error
*/
export function handleAdhocApiError(error: Error): void {
// ApiAdapterException errors are handled by the AppApiAdapter
if (!(error instanceof ApiAdapterException)) {
logError(error);
}
}