PromucFlow_constructor/app/client/src/sagas/EvaluationsSaga.ts

565 lines
16 KiB
TypeScript
Raw Normal View History

import {
actionChannel,
call,
fork,
put,
select,
take,
} from "redux-saga/effects";
import {
EvaluationReduxAction,
ReduxAction,
ReduxActionErrorTypes,
ReduxActionTypes,
ReduxActionWithoutPayload,
} from "constants/ReduxActionConstants";
2021-01-14 14:37:21 +00:00
import { getUnevaluatedDataTree } from "selectors/dataTreeSelectors";
import WidgetFactory, { WidgetTypeConfigMap } from "../utils/WidgetFactory";
2021-02-22 05:00:16 +00:00
import { GracefulWorkerService } from "utils/WorkerUtil";
import Worker from "worker-loader!../workers/evaluation.worker";
import {
EVAL_WORKER_ACTIONS,
EvalError,
EvalErrorTypes,
EvaluationError,
getEvalErrorPath,
getEvalValuePath,
PropertyEvalErrorTypeDebugMessage,
PropertyEvaluationErrorType,
2021-02-22 05:00:16 +00:00
} from "utils/DynamicBindingUtils";
import log from "loglevel";
2021-02-22 05:00:16 +00:00
import { WidgetProps } from "widgets/BaseWidget";
import PerformanceTracker, {
PerformanceTransactionName,
} from "../utils/PerformanceTracker";
import * as Sentry from "@sentry/react";
2020-12-30 13:26:44 +00:00
import { Action } from "redux";
import _ from "lodash";
import { ENTITY_TYPE, Message } from "entities/AppsmithConsole";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { AppState } from "reducers";
import {
getEntityNameAndPropertyPath,
isAction,
isWidget,
} from "workers/evaluationUtils";
import moment from "moment/moment";
import { Toaster } from "components/ads/Toast";
import { Variant } from "components/ads/common";
import AppsmithConsole from "utils/AppsmithConsole";
import AnalyticsUtil from "utils/AnalyticsUtil";
import {
createMessage,
ERROR_EVAL_ERROR_GENERIC,
ERROR_EVAL_TRIGGER,
} from "constants/messages";
import { getAppMode } from "selectors/applicationSelectors";
import { APP_MODE } from "reducers/entityReducers/appReducer";
2021-07-08 05:31:08 +00:00
import store from "store";
import { logDebuggerErrorAnalytics } from "actions/debuggerActions";
let widgetTypeConfigMap: WidgetTypeConfigMap;
const worker = new GracefulWorkerService(Worker);
const getDebuggerErrors = (state: AppState) => state.ui.debugger.errors;
function getLatestEvalPropertyErrors(
currentDebuggerErrors: Record<string, Message>,
dataTree: DataTree,
evaluationOrder: Array<string>,
) {
const updatedDebuggerErrors: Record<string, Message> = {
...currentDebuggerErrors,
};
for (const evaluatedPath of evaluationOrder) {
const { entityName, propertyPath } = getEntityNameAndPropertyPath(
evaluatedPath,
);
const entity = dataTree[entityName];
if (isWidget(entity) || isAction(entity)) {
if (propertyPath in entity.logBlackList) {
continue;
}
const allEvalErrors: EvaluationError[] = _.get(
entity,
getEvalErrorPath(evaluatedPath, false),
[],
);
const evaluatedValue = _.get(
entity,
getEvalValuePath(evaluatedPath, false),
);
const evalErrors = allEvalErrors.filter(
(error) => error.errorType !== PropertyEvaluationErrorType.LINT,
);
const idField = isWidget(entity) ? entity.widgetId : entity.actionId;
const nameField = isWidget(entity) ? entity.widgetName : entity.name;
const entityType = isWidget(entity)
? ENTITY_TYPE.WIDGET
: ENTITY_TYPE.ACTION;
const debuggerKey = idField + "-" + propertyPath;
// if dataTree has error but debugger does not -> add
// if debugger has error and data tree has error -> update error
// if debugger has error but data tree does not -> remove
// if debugger or data tree does not have an error -> no change
if (evalErrors.length) {
// TODO Rank and set the most critical error
const error = evalErrors[0];
const errorMessages = evalErrors.map((e) => ({
message: e.errorMessage,
}));
2021-07-08 05:31:08 +00:00
if (!(debuggerKey in updatedDebuggerErrors)) {
store.dispatch(
logDebuggerErrorAnalytics({
eventName: "DEBUGGER_NEW_ERROR",
entityId: idField,
entityName: nameField,
entityType,
propertyPath,
errorMessages,
}),
);
}
// Add or update
updatedDebuggerErrors[debuggerKey] = {
logType: LOG_TYPE.EVAL_ERROR,
text: PropertyEvalErrorTypeDebugMessage[error.errorType](
propertyPath,
),
messages: errorMessages,
severity: error.severity,
timestamp: moment().format("hh:mm:ss"),
source: {
id: idField,
name: nameField,
type: entityType,
propertyPath: propertyPath,
},
state: {
[propertyPath]: evaluatedValue,
},
};
} else if (debuggerKey in updatedDebuggerErrors) {
2021-07-08 05:31:08 +00:00
store.dispatch(
logDebuggerErrorAnalytics({
eventName: "DEBUGGER_RESOLVED_ERROR",
entityId: idField,
entityName: nameField,
entityType,
propertyPath:
updatedDebuggerErrors[debuggerKey].source?.propertyPath ?? "",
errorMessages: updatedDebuggerErrors[debuggerKey].messages ?? [],
}),
);
// Remove
delete updatedDebuggerErrors[debuggerKey];
}
}
}
return updatedDebuggerErrors;
}
function* evalErrorHandler(
errors: EvalError[],
dataTree?: DataTree,
evaluationOrder?: Array<string>,
): any {
if (dataTree && evaluationOrder) {
const currentDebuggerErrors: Record<string, Message> = yield select(
getDebuggerErrors,
);
const evalPropertyErrors = getLatestEvalPropertyErrors(
currentDebuggerErrors,
dataTree,
evaluationOrder,
);
yield put({
type: ReduxActionTypes.DEBUGGER_UPDATE_ERROR_LOGS,
payload: evalPropertyErrors,
});
}
2020-12-24 04:32:25 +00:00
errors.forEach((error) => {
2021-02-22 05:00:16 +00:00
switch (error.type) {
case EvalErrorTypes.CYCLICAL_DEPENDENCY_ERROR: {
if (error.context) {
// Add more info about node for the toast
const { entityType, node } = error.context;
Toaster.show({
text: `${error.message} Node was: ${node}`,
variant: Variant.danger,
});
AppsmithConsole.error({
text: `${error.message} Node was: ${node}`,
});
// Send the generic error message to sentry for better grouping
Sentry.captureException(new Error(error.message), {
tags: {
node,
entityType,
},
// Level is warning because it could be a user error
level: Sentry.Severity.Warning,
});
// Log an analytics event for cyclical dep errors
AnalyticsUtil.logEvent("CYCLICAL_DEPENDENCY_ERROR", {
node,
entityType,
// Level is warning because it could be a user error
level: Sentry.Severity.Warning,
});
}
2021-02-22 05:00:16 +00:00
break;
}
case EvalErrorTypes.EVAL_TREE_ERROR: {
Toaster.show({
text: createMessage(ERROR_EVAL_ERROR_GENERIC),
2021-02-22 05:00:16 +00:00
variant: Variant.danger,
});
break;
}
case EvalErrorTypes.BAD_UNEVAL_TREE_ERROR: {
Sentry.captureException(error);
break;
}
case EvalErrorTypes.EVAL_TRIGGER_ERROR: {
log.debug(error);
2021-02-22 05:00:16 +00:00
Toaster.show({
text: createMessage(ERROR_EVAL_TRIGGER, error.message),
2021-02-22 05:00:16 +00:00
variant: Variant.danger,
showDebugButton: true,
});
AppsmithConsole.error({
text: createMessage(ERROR_EVAL_TRIGGER, error.message),
2021-02-22 05:00:16 +00:00
});
break;
}
case EvalErrorTypes.EVAL_PROPERTY_ERROR: {
log.debug(error);
break;
}
2021-02-22 05:00:16 +00:00
default: {
Sentry.captureException(error);
log.debug(error);
2021-02-22 05:00:16 +00:00
}
}
});
}
function* logSuccessfulBindings(
unEvalTree: DataTree,
dataTree: DataTree,
evaluationOrder: string[],
) {
const appMode = yield select(getAppMode);
if (appMode === APP_MODE.PUBLISHED) return;
if (!evaluationOrder) return;
evaluationOrder.forEach((evaluatedPath) => {
const { entityName, propertyPath } = getEntityNameAndPropertyPath(
evaluatedPath,
);
const entity = dataTree[entityName];
if (isAction(entity) || isWidget(entity)) {
const unevalValue = _.get(unEvalTree, evaluatedPath);
const entityType = isAction(entity) ? entity.pluginType : entity.type;
const isABinding = _.find(entity.dynamicBindingPathList, {
key: propertyPath,
});
const logBlackList = entity.logBlackList;
const errors: EvaluationError[] = _.get(
dataTree,
getEvalErrorPath(evaluatedPath),
[],
) as EvaluationError[];
const criticalErrors = errors.filter(
(error) => error.errorType !== PropertyEvaluationErrorType.LINT,
);
const hasErrors = criticalErrors.length > 0;
if (isABinding && !hasErrors && !(propertyPath in logBlackList)) {
AnalyticsUtil.logEvent("BINDING_SUCCESS", {
unevalValue,
entityType,
propertyPath,
});
}
}
});
}
function* postEvalActionDispatcher(
actions: Array<ReduxAction<unknown> | ReduxActionWithoutPayload>,
) {
for (const action of actions) {
yield put(action);
}
}
function* evaluateTreeSaga(
postEvalActions?: Array<ReduxAction<unknown> | ReduxActionWithoutPayload>,
) {
2021-03-31 07:40:59 +00:00
const unevalTree = yield select(getUnevaluatedDataTree);
log.debug({ unevalTree });
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.DATA_TREE_EVALUATION,
);
const workerResponse = yield call(
worker.request,
EVAL_WORKER_ACTIONS.EVAL_TREE,
{
unevalTree,
widgetTypeConfigMap,
},
);
const {
dataTree,
dependencies,
errors,
evaluationOrder,
logs,
} = workerResponse;
2021-03-31 07:40:59 +00:00
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.DATA_TREE_EVALUATION,
);
log.debug({ dataTree: dataTree });
logs.forEach((evalLog: any) => log.debug(evalLog));
yield call(evalErrorHandler, errors, dataTree, evaluationOrder);
yield fork(logSuccessfulBindings, unevalTree, dataTree, evaluationOrder);
2021-03-31 07:40:59 +00:00
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.SET_EVALUATED_TREE,
);
yield put({
type: ReduxActionTypes.SET_EVALUATED_TREE,
payload: dataTree,
});
PerformanceTracker.stopAsyncTracking(
2021-03-31 07:40:59 +00:00
PerformanceTransactionName.SET_EVALUATED_TREE,
);
yield put({
2021-01-14 14:37:21 +00:00
type: ReduxActionTypes.SET_EVALUATION_INVERSE_DEPENDENCY_MAP,
payload: { inverseDependencyMap: dependencies },
});
if (postEvalActions && postEvalActions.length) {
yield call(postEvalActionDispatcher, postEvalActions);
}
}
2021-01-14 14:37:21 +00:00
export function* evaluateActionBindings(
bindings: string[],
executionParams: Record<string, any> | string = {},
2020-12-14 18:48:13 +00:00
) {
const workerResponse = yield call(
worker.request,
2021-01-14 14:37:21 +00:00
EVAL_WORKER_ACTIONS.EVAL_ACTION_BINDINGS,
{
2021-01-14 14:37:21 +00:00
bindings,
executionParams,
},
);
2021-01-14 14:37:21 +00:00
const { errors, values } = workerResponse;
yield call(evalErrorHandler, errors);
2021-01-14 14:37:21 +00:00
return values;
}
export function* evaluateDynamicTrigger(
dynamicTrigger: string,
2020-11-20 09:30:50 +00:00
callbackData?: Array<any>,
) {
const unEvalTree = yield select(getUnevaluatedDataTree);
const workerResponse = yield call(
worker.request,
EVAL_WORKER_ACTIONS.EVAL_TRIGGER,
{ dataTree: unEvalTree, dynamicTrigger, callbackData },
);
const { errors, triggers } = workerResponse;
yield call(evalErrorHandler, errors);
return triggers;
}
export function* clearEvalCache() {
yield call(worker.request, EVAL_WORKER_ACTIONS.CLEAR_CACHE);
return true;
}
export function* clearEvalPropertyCache(propertyPath: string) {
yield call(worker.request, EVAL_WORKER_ACTIONS.CLEAR_PROPERTY_CACHE, {
propertyPath,
});
}
/**
* clears all cache keys of a widget
*
* @param widgetName
*/
export function* clearEvalPropertyCacheOfWidget(widgetName: string) {
yield call(
worker.request,
EVAL_WORKER_ACTIONS.CLEAR_PROPERTY_CACHE_OF_WIDGET,
{
widgetName,
},
);
}
export function* validateProperty(
property: string,
value: any,
props: WidgetProps,
) {
const unevalTree = yield select(getUnevaluatedDataTree);
const validation = unevalTree[props.widgetName].validationPaths[property];
return yield call(worker.request, EVAL_WORKER_ACTIONS.VALIDATE_PROPERTY, {
property,
value,
props,
validation,
});
}
2020-12-30 13:26:44 +00:00
const FIRST_EVAL_REDUX_ACTIONS = [
// Pages
ReduxActionTypes.FETCH_PAGE_SUCCESS,
ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS,
];
const EVALUATE_REDUX_ACTIONS = [
2020-12-30 13:26:44 +00:00
...FIRST_EVAL_REDUX_ACTIONS,
// Actions
ReduxActionTypes.FETCH_ACTIONS_SUCCESS,
ReduxActionTypes.FETCH_PLUGIN_FORM_CONFIGS_SUCCESS,
ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS,
ReduxActionErrorTypes.FETCH_ACTIONS_ERROR,
ReduxActionErrorTypes.FETCH_ACTIONS_VIEW_MODE_ERROR,
ReduxActionTypes.FETCH_ACTIONS_FOR_PAGE_SUCCESS,
ReduxActionTypes.SUBMIT_CURL_FORM_SUCCESS,
ReduxActionTypes.CREATE_ACTION_SUCCESS,
ReduxActionTypes.UPDATE_ACTION_PROPERTY,
ReduxActionTypes.DELETE_ACTION_SUCCESS,
ReduxActionTypes.COPY_ACTION_SUCCESS,
ReduxActionTypes.MOVE_ACTION_SUCCESS,
ReduxActionTypes.RUN_ACTION_SUCCESS,
ReduxActionErrorTypes.RUN_ACTION_ERROR,
ReduxActionTypes.EXECUTE_API_ACTION_SUCCESS,
ReduxActionErrorTypes.EXECUTE_ACTION_ERROR,
// App Data
ReduxActionTypes.SET_APP_MODE,
ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS,
2021-03-24 05:09:47 +00:00
ReduxActionTypes.UPDATE_APP_PERSISTENT_STORE,
ReduxActionTypes.UPDATE_APP_TRANSIENT_STORE,
// Widgets
ReduxActionTypes.UPDATE_LAYOUT,
ReduxActionTypes.UPDATE_WIDGET_PROPERTY,
ReduxActionTypes.UPDATE_WIDGET_NAME_SUCCESS,
// Widget Meta
ReduxActionTypes.SET_META_PROP,
ReduxActionTypes.RESET_WIDGET_META,
// Batches
ReduxActionTypes.BATCH_UPDATES_SUCCESS,
];
const shouldProcessAction = (action: ReduxAction<unknown>) => {
if (
action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS &&
Array.isArray(action.payload)
) {
const batchedActionTypes = action.payload.map(
(batchedAction) => batchedAction.type,
);
return (
_.intersection(EVALUATE_REDUX_ACTIONS, batchedActionTypes).length > 0
);
}
return true;
};
2020-12-30 13:26:44 +00:00
function evalQueueBuffer() {
let canTake = false;
2020-12-30 13:26:44 +00:00
let postEvalActions: any = [];
const take = () => {
if (canTake) {
2020-12-30 13:26:44 +00:00
const resp = postEvalActions;
postEvalActions = [];
canTake = false;
return { postEvalActions: resp, type: "BUFFERED_ACTION" };
2020-12-30 13:26:44 +00:00
}
};
const flush = () => {
if (canTake) {
2020-12-30 13:26:44 +00:00
return [take() as Action];
}
return [];
};
const put = (action: EvaluationReduxAction<unknown | unknown[]>) => {
if (!shouldProcessAction(action)) {
return;
}
canTake = true;
2020-12-30 13:26:44 +00:00
// TODO: If the action is the same as before, we can send only one and ignore duplicates.
if (action.postEvalActions) {
postEvalActions.push(...action.postEvalActions);
}
};
return {
take,
put,
isEmpty: () => {
return !canTake;
2020-12-30 13:26:44 +00:00
},
flush,
};
}
function* evaluationChangeListenerSaga() {
// Explicitly shutdown old worker if present
yield call(worker.shutdown);
yield call(worker.start);
widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap();
const initAction = yield take(FIRST_EVAL_REDUX_ACTIONS);
yield fork(evaluateTreeSaga, initAction.postEvalActions);
2020-12-30 13:26:44 +00:00
const evtActionChannel = yield actionChannel(
EVALUATE_REDUX_ACTIONS,
evalQueueBuffer(),
);
while (true) {
const action: EvaluationReduxAction<unknown | unknown[]> = yield take(
evtActionChannel,
);
if (shouldProcessAction(action)) {
yield call(evaluateTreeSaga, action.postEvalActions);
}
}
// TODO(hetu) need an action to stop listening and evaluate (exit app)
}
export default function* evaluationSagaListeners() {
2020-12-30 13:26:44 +00:00
yield take(ReduxActionTypes.START_EVALUATION);
while (true) {
try {
yield call(evaluationChangeListenerSaga);
} catch (e) {
log.error(e);
Sentry.captureException(e);
}
}
}