PromucFlow_constructor/app/client/src/sagas/ErrorSagas.tsx
Apeksha Bhosale 6b6e348203
chore: Move sentry to faro for client side (#40220)
## Description
> [!TIP]  
> _Add a TL;DR when the description is longer than 500 words or
extremely technical (helps the content, marketing, and DevRel team)._
>
> _Please also include relevant motivation and context. List any
dependencies that are required for this change. Add links to Notion,
Figma or any other documents that might be relevant to the PR._


Fixes #`Issue Number`  
_or_  
Fixes `Issue URL`
> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/14401534892>
> Commit: cad55550d2e517bec0031fe4043ec76cfb4d67e3
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=14401534892&attempt=2"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Fri, 11 Apr 2025 12:38:06 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Refactor**
- Revamped the error reporting system across the platform with enhanced
contextual logging for faster issue diagnosis.

- **Chore**
- Removed legacy error tracking integrations, streamlining overall error
handling and system reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-16 11:45:47 +05:30

395 lines
10 KiB
TypeScript

import { get } from "lodash";
import { type ReduxAction } from "actions/ReduxActionTypes";
import {
ReduxActionErrorTypes,
ReduxActionTypes,
toastMessageErrorTypes,
} from "ee/constants/ReduxActionConstants";
import log from "loglevel";
import history from "utils/history";
import type { ApiResponse } from "api/ApiResponses";
import {
type ErrorPayloadType,
flushErrors,
safeCrashApp,
} from "actions/errorActions";
import { AUTH_LOGIN_URL } from "constants/routes";
import type { User } from "constants/userConstants";
import { ANONYMOUS_USERNAME } from "constants/userConstants";
import {
AXIOS_CONNECTION_ABORTED_CODE,
ERROR_CODES,
SERVER_ERROR_CODES,
} from "ee/constants/ApiConstants";
import { getSafeCrash } from "selectors/errorSelectors";
import { getCurrentUser } from "selectors/usersSelectors";
import { call, put, select, takeLatest } from "redux-saga/effects";
import {
createMessage,
DEFAULT_ERROR_MESSAGE,
ERROR_0,
ERROR_401,
ERROR_403,
ERROR_500,
} from "ee/constants/messages";
import store from "store";
import { getLoginUrl } from "ee/utils/adminSettingsHelpers";
import type { PluginErrorDetails } from "api/ActionAPI";
import showToast from "sagas/ToastSagas";
import AppsmithConsole from "../utils/AppsmithConsole";
import type { SourceEntity } from "../entities/AppsmithConsole";
import { getAppMode } from "ee/selectors/applicationSelectors";
import { APP_MODE } from "../entities/App";
import captureException from "instrumentation/sendFaroErrors";
const shouldShowToast = (action: string) => {
return action in toastMessageErrorTypes;
};
/**
* making with error message with action name
*
* @param action
*/
export const getDefaultActionError = (action: string) =>
`Incurred an error when ${action}`;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function* callAPI(apiCall: any, requestPayload: any) {
try {
const response: ApiResponse = yield call(apiCall, requestPayload);
return response;
} catch (error) {
return error;
}
}
/**
* transform server errors to client error codes
*
* @param code
* @param resourceType
*/
const getErrorMessage = (code: number, resourceType = "") => {
switch (code) {
case 401:
return createMessage(ERROR_401);
case 500:
return createMessage(ERROR_500);
case 403:
return createMessage(() =>
ERROR_403(resourceType, getCurrentUser(store.getState())?.email || ""),
);
case 0:
return createMessage(ERROR_0);
}
};
export class IncorrectBindingError extends Error {}
/**
* validates if response does have any errors
* @throws {Error}
* @param response
* @param show
* @param logToSentry
*/
export function* validateResponse(
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
response: ApiResponse | any,
show = true,
logToSentry = false,
) {
if (!response) {
throw Error("");
}
// letting `apiFailureResponseInterceptor` handle it this case
if (response?.code === AXIOS_CONNECTION_ABORTED_CODE) {
return false;
}
if (!response.responseMeta && !response.status) {
throw Error(getErrorMessage(0));
}
if (!response.responseMeta && response.status) {
yield put({
type: ReduxActionErrorTypes.API_ERROR,
payload: {
error: new Error(
getErrorMessage(response.status, response.resourceType),
),
logToSentry,
show,
},
});
}
if (response.responseMeta.success) {
return true;
}
if (
SERVER_ERROR_CODES.INCORRECT_BINDING_LIST_OF_WIDGET.includes(
response.responseMeta?.error?.code,
)
) {
throw new IncorrectBindingError(response.responseMeta?.error?.message);
}
yield put({
type: ReduxActionErrorTypes.API_ERROR,
payload: {
error: new Error(response.responseMeta?.error?.message),
logToSentry,
show,
},
});
throw Error(response.responseMeta?.error?.message);
}
export function getResponseErrorMessage(response: ApiResponse) {
return response.responseMeta.error
? response.responseMeta.error.message
: undefined;
}
interface ClientDefinedErrorMetadata {
clientDefinedError: boolean;
statusCode: string;
message: string;
pluginErrorDetails: PluginErrorDetails;
}
export function extractClientDefinedErrorMetadata(
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
err: any,
): ClientDefinedErrorMetadata | undefined {
if (err?.clientDefinedError && err?.response) {
return {
clientDefinedError: err?.clientDefinedError,
statusCode: err?.statusCode,
message: err?.message,
pluginErrorDetails: err?.pluginErrorDetails,
};
} else {
return undefined;
}
}
const ActionErrorDisplayMap: {
[key: string]: (error: ErrorPayloadType) => string;
} = {
[ReduxActionErrorTypes.API_ERROR]: (error) =>
get(error, "message", createMessage(DEFAULT_ERROR_MESSAGE)),
[ReduxActionErrorTypes.FETCH_PAGE_ERROR]: () =>
getDefaultActionError("fetching the page"),
[ReduxActionErrorTypes.SAVE_PAGE_ERROR]: () =>
getDefaultActionError("saving the page"),
};
const getErrorMessageFromActionType = (
type: string,
error: ErrorPayloadType,
): string => {
const actionErrorMessage = get(error, "message");
if (actionErrorMessage === undefined) {
if (type in ActionErrorDisplayMap) {
return ActionErrorDisplayMap[type](error);
}
return createMessage(DEFAULT_ERROR_MESSAGE);
}
return actionErrorMessage;
};
enum ErrorEffectTypes {
SHOW_ALERT = "SHOW_ALERT",
SAFE_CRASH = "SAFE_CRASH",
LOG_TO_CONSOLE = "LOG_TO_CONSOLE",
LOG_TO_SENTRY = "LOG_TO_SENTRY",
LOG_TO_DEBUGGER = "LOG_TO_DEBUGGER",
}
export interface ErrorActionPayload {
error: ErrorPayloadType;
show?: boolean;
crash?: boolean;
logToSentry?: boolean;
logToDebugger?: boolean;
sourceEntity?: SourceEntity;
}
export function* errorSaga(errorAction: ReduxAction<ErrorActionPayload>) {
const effects = [ErrorEffectTypes.LOG_TO_CONSOLE];
const { payload, type } = errorAction;
const { error, logToDebugger, logToSentry, show, sourceEntity } =
payload || {};
const appMode: APP_MODE = yield select(getAppMode);
// "show" means show a toast. We check if the error has been asked to not been shown
// By checking undefined, undecided actions still pass through this check
if (show === undefined) {
// We want to show toasts for certain actions only so we avoid issues or if it is outside edit mode
if (shouldShowToast(type) || appMode !== APP_MODE.EDIT) {
effects.push(ErrorEffectTypes.SHOW_ALERT);
}
// If true is passed, show the error no matter what
} else if (show) {
effects.push(ErrorEffectTypes.SHOW_ALERT);
}
if (logToDebugger) {
effects.push(ErrorEffectTypes.LOG_TO_DEBUGGER);
}
if (error && error.crash) {
effects.push(ErrorEffectTypes.LOG_TO_SENTRY);
effects.push(ErrorEffectTypes.SAFE_CRASH);
}
if (error && logToSentry) {
effects.push(ErrorEffectTypes.LOG_TO_SENTRY);
}
const message = getErrorMessageFromActionType(type, error);
for (const effect of effects) {
switch (effect) {
case ErrorEffectTypes.LOG_TO_CONSOLE: {
logErrorSaga(errorAction);
break;
}
case ErrorEffectTypes.LOG_TO_DEBUGGER: {
AppsmithConsole.error({
text: message,
source: sourceEntity,
});
break;
}
case ErrorEffectTypes.SHOW_ALERT: {
// This is the toast that is rendered when any page load API fails.
yield call(showToast, message, { kind: "error" });
if ("Cypress" in window) {
if (message === "" || message === null) {
yield put(
safeCrashApp({
...error,
code: ERROR_CODES.CYPRESS_DEBUG,
}),
);
}
}
break;
}
case ErrorEffectTypes.SAFE_CRASH: {
yield put(safeCrashApp(error));
break;
}
case ErrorEffectTypes.LOG_TO_SENTRY: {
yield call(captureException, error, { errorName: "ErrorSagaError" });
break;
}
}
}
yield put({
type: ReduxActionTypes.REPORT_ERROR,
payload: {
source: errorAction.type,
message,
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
stackTrace: (error as any)?.stack,
},
});
}
function logErrorSaga(action: ReduxAction<{ error: ErrorPayloadType }>) {
log.debug(`Error in action ${action.type}`);
if (action.payload) log.error(action.payload.error, action);
}
export function embedRedirectURL() {
const queryParams = new URLSearchParams(window.location.search);
const ssoTriggerQueryParam = queryParams.get("ssoTrigger");
const ssoLoginUrl = ssoTriggerQueryParam
? getLoginUrl(ssoTriggerQueryParam || "")
: null;
if (ssoLoginUrl) {
window.location.href = `${ssoLoginUrl}?redirectUrl=${encodeURIComponent(
window.location.href,
)}`;
} else {
window.location.href = `${AUTH_LOGIN_URL}?redirectUrl=${encodeURIComponent(
window.location.href,
)}`;
}
}
/**
* this saga do some logic before actually setting safeCrash to true
*/
function* safeCrashSagaRequest(action: ReduxAction<{ code?: ERROR_CODES }>) {
const user: User | undefined = yield select(getCurrentUser);
const code = get(action, "payload.code");
// if user is not logged and the error is "PAGE_NOT_FOUND",
// redirecting user to login page with redirecTo param
if (
get(user, "email") === ANONYMOUS_USERNAME &&
code === ERROR_CODES.PAGE_NOT_FOUND
) {
embedRedirectURL();
return false;
}
// if there is no action to be done, just calling the safe crash action
yield put(safeCrashApp({ code }));
}
/**
* flush errors and redirect users to a url
*
* @param action
*/
export function* flushErrorsAndRedirectSaga(
action: ReduxAction<{ url?: string }>,
) {
const safeCrash: boolean = yield select(getSafeCrash);
if (safeCrash) {
yield put(flushErrors());
}
if (!action.payload.url) return;
history.push(action.payload.url);
}
export default function* errorSagas() {
yield takeLatest(Object.values(ReduxActionErrorTypes), errorSaga);
yield takeLatest(
ReduxActionTypes.FLUSH_AND_REDIRECT,
flushErrorsAndRedirectSaga,
);
yield takeLatest(
ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST,
safeCrashSagaRequest,
);
}