PromucFlow_constructor/app/client/src/sagas/DebuggerSagas.ts
Apeksha Bhosale 4dc6df0013
chore: query module evaluation (#27660)
> Pull Request Template
>
> Use this template to quickly create a well written pull request.
Delete all quotes before creating the pull request.
>
## Description
There are multiple refactors and split for query module's creator flow
changes which involves module input -- it's a new entity introduced as
part of modules project

#### PR fixes following issue(s)
Fixes # (issue number)
Part of
https://app.zenhub.com/workspaces/modules-pod-63e0d668a7fea03850c89c6f/issues/gh/appsmithorg/appsmith/27352

#### Type of change

- Chore (housekeeping or task changes that don't impact user perception)

>
>
## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [ ] Manual
- [ ] JUnit
- [ ] Jest
- [ ] Cypress
>
>
#### Test Plan
> Add Testsmith test cases links that relate to this PR
>
>
#### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
>
>
>
## Checklist:
#### Dev activity
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
2023-09-29 16:12:14 +05:30

713 lines
22 KiB
TypeScript

import type { LogDebuggerErrorAnalyticsPayload } from "actions/debuggerActions";
import {
addErrorLogs,
debuggerLog,
debuggerLogInit,
deleteErrorLog,
} from "actions/debuggerActions";
import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import type {
Log,
LogActionPayload,
LogObject,
} from "entities/AppsmithConsole";
import { ENTITY_TYPE, LOG_CATEGORY } from "entities/AppsmithConsole";
import {
all,
call,
fork,
put,
select,
take,
takeEvery,
} from "redux-saga/effects";
import { findIndex, flatten, get, isEmpty, isMatch, set } from "lodash";
import { getDebuggerErrors } from "selectors/debuggerSelectors";
import {
getAction,
getPlugin,
getJSCollection,
getAppMode,
} from "@appsmith/selectors/entitiesSelector";
import type { Action } from "entities/Action";
import { PluginType } from "entities/Action";
import type { JSCollection } from "entities/JSCollection";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import type { ConfigTree, DataTree } from "@appsmith/entities/DataTree/types";
import {
getConfigTree,
getDataTree,
getEvaluationInverseDependencyMap,
} from "selectors/dataTreeSelectors";
import {
createLogTitleString,
getDependencyChain,
} from "components/editorComponents/Debugger/helpers";
import {
ACTION_CONFIGURATION_UPDATED,
createMessage,
WIDGET_PROPERTIES_UPDATED,
} from "@appsmith/constants/messages";
import AppsmithConsole from "utils/AppsmithConsole";
import { getWidget } from "./selectors";
import AnalyticsUtil from "utils/AnalyticsUtil";
import type { Plugin } from "api/PluginApi";
import { getCurrentPageId } from "selectors/editorSelectors";
import type { WidgetProps } from "widgets/BaseWidget";
import * as log from "loglevel";
import type { DependencyMap } from "utils/DynamicBindingUtils";
import type { TriggerMeta } from "@appsmith/sagas/ActionExecution/ActionExecutionSagas";
import {
getEntityNameAndPropertyPath,
isAction,
isWidget,
} from "@appsmith/workers/Evaluation/evaluationUtils";
import { getCurrentEnvironmentDetails } from "@appsmith/selectors/environmentSelectors";
// Saga to format action request values to be shown in the debugger
function* formatActionRequestSaga(
payload: LogActionPayload,
requestPath?: any,
) {
// If there are no headers or body we don't format anything.
if (!payload.source || !payload.state || !requestPath) {
return payload;
}
const request = get(payload, requestPath);
const source = payload.source;
const action: Action | undefined = yield select(getAction, source.id);
// Only formatting for apis and not queries
if (action && action.pluginType === PluginType.API) {
// Formatting api headers here
if (request.headers) {
let formattedHeaders = [];
// Convert headers from Record<string, array>[] to Record<string, string>[]
// for showing in the logs
formattedHeaders = Object.keys(request.headers).map((key: string) => {
const value = request.headers[key];
return {
[key]: value[0],
};
});
set(payload, `${requestPath}.headers`, formattedHeaders);
}
// Formatting api body
if (request.body) {
let body = request.body;
try {
body = JSON.parse(body);
set(payload, `${requestPath}.body`, body);
} catch (e) {
// Nothing to do here, we show the api body as it is if it cannot be shown as
// an object
}
}
// Return the final payload to be logged
return payload;
} else {
return payload;
}
}
function* onEntityDeleteSaga(payload: Log[]) {
const sortedLogs = payload.reduce(
(
sortedLogs: {
withSource: Log[];
withoutSource: Log[];
},
log,
) => {
return log.source
? { ...sortedLogs, withSource: [...sortedLogs.withSource, log] }
: { ...sortedLogs, withSource: [...sortedLogs.withoutSource, log] };
},
{
withSource: [],
withoutSource: [],
},
);
if (!isEmpty(sortedLogs.withoutSource)) {
yield put(debuggerLog(sortedLogs.withoutSource));
}
if (isEmpty(sortedLogs.withSource)) return;
const errors: Record<string, Log> = yield select(getDebuggerErrors);
const errorIds = Object.keys(errors);
const logSourceIds = sortedLogs.withSource.map((log) => log.source?.id);
const errorsToDelete = errorIds.reduce((errorList: Log[], currentId) => {
const isPresent = logSourceIds.some((id) => id && currentId.includes(id));
return isPresent ? [...errorList, errors[currentId]] : errorList;
}, []);
sortedLogs.withSource.filter(
(log) => log.source && errorIds.includes(log.source.id),
);
if (!isEmpty(errorsToDelete)) {
const errorPayload = errorsToDelete.map((log) => ({
id: log.id as string,
analytics: log.analytics,
}));
AppsmithConsole.deleteErrors(errorPayload);
}
yield put(debuggerLog(sortedLogs.withSource));
}
function getLogsFromDependencyChain(
dependencyChain: string[],
payload: Log,
dataTree: DataTree,
) {
return dependencyChain.map((path) => {
const entityInfo = getEntityNameAndPropertyPath(path);
const entity = dataTree[entityInfo.entityName];
let log = {
...payload,
state: {
[entityInfo.propertyPath]: get(dataTree, path),
},
};
if (isAction(entity)) {
log = {
...log,
text: createMessage(ACTION_CONFIGURATION_UPDATED),
source: {
type: ENTITY_TYPE.ACTION,
name: entityInfo.entityName,
id: entity.actionId,
},
};
} else if (isWidget(entity)) {
log = {
...log,
text: createMessage(WIDGET_PROPERTIES_UPDATED),
source: {
type: ENTITY_TYPE.WIDGET,
name: entityInfo.entityName,
id: entity.widgetId,
},
};
}
return log;
});
}
function* logDependentEntityProperties(payload: Log[]) {
const validLogs = payload.filter((log) => log.state && log.source);
if (isEmpty(validLogs)) return;
yield take(ReduxActionTypes.SET_EVALUATED_TREE);
const dataTree: DataTree = yield select(getDataTree);
const inverseDependencyMap: DependencyMap = yield select(
getEvaluationInverseDependencyMap,
);
const finalPayload: Log[][] = [];
for (const log of validLogs) {
const propertyPath = `${log.source?.name}.` + log.source?.propertyPath;
const dependencyChain = getDependencyChain(
propertyPath,
inverseDependencyMap,
);
const payloadValue = getLogsFromDependencyChain(
dependencyChain,
log,
dataTree,
);
finalPayload.push(payloadValue);
}
//logging them all at once rather than updating them individually
yield put(debuggerLog(flatten(finalPayload)));
}
function* onTriggerPropertyUpdates(payload: Log[]) {
const configTree: ConfigTree = yield select(getConfigTree);
const validLogs = payload.filter(
(log) => log.source && log.source.propertyPath,
);
if (isEmpty(validLogs)) return;
const errorsPathsToDeleteFromConsole = new Set<string>();
for (const log of validLogs) {
const { source } = log;
if (!source || !source.propertyPath) continue;
const widget = configTree[source.name];
// If property is not a trigger property we ignore
if (!isWidget(widget) || !(source.propertyPath in widget.triggerPaths))
return false;
// If the value of the property is empty(or set to 'No Action')
if (widget[source.propertyPath] === "") {
errorsPathsToDeleteFromConsole.add(`${source.id}-${source.propertyPath}`);
}
}
const errorIdsToDelete = Array.from(errorsPathsToDeleteFromConsole).map(
(path) => ({ id: path }),
);
AppsmithConsole.deleteErrors(errorIdsToDelete);
}
function* debuggerLogSaga(action: ReduxAction<Log[]>) {
const { payload: logs } = action;
// array of logs without LOG_TYPE and logs which are not handled in switch statement below.
let otherLogs: Log[] = [];
// Group logs by LOG_TYPE
const sortedLogs = logs.reduce(
(sortedLogs: Record<string, Log[]>, currentLog: Log) => {
if (currentLog.logType) {
return sortedLogs.hasOwnProperty(currentLog.logType)
? {
...sortedLogs,
[currentLog.logType]: [
...sortedLogs[currentLog.logType],
currentLog,
],
}
: {
...sortedLogs,
[currentLog.logType]: [currentLog],
};
} else {
otherLogs.push(currentLog);
return sortedLogs;
}
},
{},
);
for (const item in sortedLogs) {
const logType = Number(item);
const payload = sortedLogs[item];
switch (logType) {
case LOG_TYPE.WIDGET_UPDATE:
yield put(debuggerLog(payload));
yield call(logDependentEntityProperties, payload);
yield call(onTriggerPropertyUpdates, payload);
return;
case LOG_TYPE.ACTION_UPDATE:
yield put(debuggerLog(payload));
yield call(logDependentEntityProperties, payload);
return;
case LOG_TYPE.JS_ACTION_UPDATE:
yield put(debuggerLog(payload));
return;
case LOG_TYPE.JS_PARSE_ERROR:
yield put(addErrorLogs(payload));
break;
case LOG_TYPE.JS_PARSE_SUCCESS: {
const errorIds = payload.map((log) => ({ id: log.source?.id ?? "" }));
AppsmithConsole.deleteErrors(errorIds);
break;
}
// @ts-expect-error: Types are not available
case LOG_TYPE.TRIGGER_EVAL_ERROR:
yield put(debuggerLog(payload));
case LOG_TYPE.EVAL_ERROR:
case LOG_TYPE.LINT_ERROR:
case LOG_TYPE.EVAL_WARNING:
case LOG_TYPE.WIDGET_PROPERTY_VALIDATION_ERROR: {
const filteredLogs = payload.filter(
(log) => log.source && log.source.propertyPath && log.text,
);
yield put(addErrorLogs(filteredLogs));
break;
}
case LOG_TYPE.ACTION_EXECUTION_ERROR:
{
const allFormatedLogs: Log[] = [];
for (const log of payload) {
const formattedLog: Log = yield call(
formatActionRequestSaga,
log,
"state",
);
allFormatedLogs.push(formattedLog);
}
yield put(addErrorLogs(allFormatedLogs));
yield put(debuggerLog(allFormatedLogs));
}
break;
case LOG_TYPE.ACTION_EXECUTION_SUCCESS:
{
const allFormatedLogs: Log[] = [];
for (const log of payload) {
const formattedLog: Log = yield call(
formatActionRequestSaga,
log,
"state.request",
);
allFormatedLogs.push(formattedLog);
}
const payloadIds = payload.map((log) => ({
id: log.source?.id ?? "",
}));
AppsmithConsole.deleteErrors(payloadIds);
yield put(debuggerLog(allFormatedLogs));
}
break;
case LOG_TYPE.ENTITY_DELETED:
yield fork(onEntityDeleteSaga, payload);
break;
default:
otherLogs = otherLogs.concat(payload);
}
}
if (!isEmpty(otherLogs)) {
yield put(debuggerLog(otherLogs));
}
}
// This saga is intended for analytics only
function* logDebuggerErrorAnalyticsSaga(
action: ReduxAction<LogDebuggerErrorAnalyticsPayload>,
) {
try {
const { payload } = action;
const currentPageId: string | undefined = yield select(getCurrentPageId);
if (payload.entityType === ENTITY_TYPE.WIDGET) {
const widget: WidgetProps | undefined = yield select(
getWidget,
payload.entityId,
);
const widgetType = widget?.type || payload?.analytics?.widgetType || "";
const propertyPath = `${widgetType}.${payload.propertyPath}`;
// Sending widget type for widgets
AnalyticsUtil.logEvent(payload.eventName, {
entityType: widgetType,
propertyPath,
errorId: payload.errorId,
errorMessages: payload.errorMessages,
pageId: currentPageId,
errorMessage: payload.errorMessage,
errorType: payload.errorType,
appMode: payload.appMode,
});
} else if (payload.entityType === ENTITY_TYPE.ACTION) {
const action: Action | undefined = yield select(
getAction,
payload.entityId,
);
const pluginId = action?.pluginId || payload?.analytics?.pluginId || "";
const plugin: Plugin = yield select(getPlugin, pluginId);
const pluginName = plugin?.name.replace(/ /g, "");
let propertyPath = `${pluginName}`;
if (payload.propertyPath) {
propertyPath += `.${payload.propertyPath}`;
}
// Sending plugin name for actions
AnalyticsUtil.logEvent(payload.eventName, {
entityType: pluginName,
propertyPath,
errorId: payload.errorId,
errorMessages: payload.errorMessages,
pageId: currentPageId,
errorMessage: payload.errorMessage,
errorType: payload.errorType,
errorSubType: payload.errorSubType,
appMode: payload.appMode,
});
} else if (payload.entityType === ENTITY_TYPE.JSACTION) {
const action: JSCollection = yield select(
getJSCollection,
payload.entityId,
);
if (!action) return;
const plugin: Plugin = yield select(getPlugin, action.pluginId);
const pluginName = plugin?.name?.replace(/ /g, "");
// Sending plugin name for actions
AnalyticsUtil.logEvent(payload.eventName, {
entityType: pluginName,
errorId: payload.errorId,
propertyPath: payload.propertyPath,
errorMessages: payload.errorMessages,
pageId: currentPageId,
appMode: payload.appMode,
});
}
} catch (e) {
log.error(e);
}
}
function* addDebuggerErrorLogsSaga(action: ReduxAction<Log[]>) {
const errorLogs = action.payload;
const currentDebuggerErrors: Record<string, Log> = yield select(
getDebuggerErrors,
);
const appMode: ReturnType<typeof getAppMode> = yield select(getAppMode);
yield put(debuggerLogInit(errorLogs));
const validErrorLogs = errorLogs.filter((log) => log.source && log.id);
if (isEmpty(validErrorLogs)) return;
for (const errorLog of validErrorLogs) {
const { id, messages, source } = errorLog;
if (!source || !id) continue;
const analyticsPayload = {
entityName: source.name,
entityType: source.type,
entityId: source.id,
propertyPath: source.propertyPath ?? "",
};
// If this is a new error
if (!currentDebuggerErrors.hasOwnProperty(id)) {
const errorMessages = errorLog.messages ?? [];
yield put({
type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
payload: {
...analyticsPayload,
eventName: "DEBUGGER_NEW_ERROR",
errorMessages,
appMode,
},
});
// Log analytics for new error messages
//errorID has timestamp for 1:1 mapping with new and resolved errors
if (errorMessages.length && errorLog) {
const currentEnvDetails: { id: string; name: string } = yield select(
getCurrentEnvironmentDetails,
);
yield all(
errorMessages.map((errorMessage) =>
put({
type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
payload: {
...analyticsPayload,
environmentId: currentEnvDetails.id,
environmentName: currentEnvDetails.name,
eventName: "DEBUGGER_NEW_ERROR_MESSAGE",
errorId: errorLog.id + "_" + errorLog.timestamp,
errorMessage: errorMessage.message,
errorType: errorMessage.type,
errorSubType: errorMessage.subType,
appMode,
},
}),
),
);
}
} else {
const updatedErrorMessages = messages ?? [];
const existingErrorMessages = currentDebuggerErrors[id].messages ?? [];
const currentEnvDetails: { id: string; name: string } = yield select(
getCurrentEnvironmentDetails,
);
// Log new error messages
yield all(
updatedErrorMessages.map((updatedErrorMessage) => {
const exists = findIndex(
existingErrorMessages,
(existingErrorMessage) => {
return isMatch(existingErrorMessage, updatedErrorMessage);
},
);
if (exists < 0) {
//errorID has timestamp for 1:1 mapping with new and resolved errors
return put({
type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
payload: {
...analyticsPayload,
environmentId: currentEnvDetails.id,
environmentName: currentEnvDetails.name,
eventName: "DEBUGGER_NEW_ERROR_MESSAGE",
errorId: errorLog.id + "_" + errorLog.timestamp,
errorMessage: updatedErrorMessage.message,
errorType: updatedErrorMessage.type,
errorSubType: updatedErrorMessage.subType,
appMode,
},
});
}
}),
);
// Log resolved error messages
yield all(
existingErrorMessages.map((existingErrorMessage) => {
const exists = findIndex(
updatedErrorMessages,
(updatedErrorMessage) => {
return isMatch(updatedErrorMessage, existingErrorMessage);
},
);
if (exists < 0) {
//errorID has timestamp for 1:1 mapping with new and resolved errors
return put({
type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
payload: {
...analyticsPayload,
environmentId: currentEnvDetails.id,
environmentName: currentEnvDetails.name,
eventName: "DEBUGGER_RESOLVED_ERROR_MESSAGE",
errorId:
currentDebuggerErrors[id].id +
"_" +
currentDebuggerErrors[id].timestamp,
errorMessage: existingErrorMessage.message,
errorType: existingErrorMessage.type,
errorSubType: existingErrorMessage.subType,
appMode,
},
});
}
}),
);
}
}
}
function* deleteDebuggerErrorLogsSaga(
action: ReduxAction<{ id: string; analytics: Log["analytics"] }[]>,
) {
const { payload } = action;
const currentDebuggerErrors: Record<string, Log> = yield select(
getDebuggerErrors,
);
const appMode: ReturnType<typeof getAppMode> = yield select(getAppMode);
const existingErrorPayloads = payload.filter((item) =>
currentDebuggerErrors.hasOwnProperty(item.id),
);
if (isEmpty(existingErrorPayloads)) return;
const validErrorPayloadsToDelete = existingErrorPayloads.filter((payload) => {
const existingError = currentDebuggerErrors[payload.id];
return existingError && existingError.source;
});
if (isEmpty(validErrorPayloadsToDelete)) return;
for (const validErrorPayload of validErrorPayloadsToDelete) {
const error = currentDebuggerErrors[validErrorPayload.id];
if (!error || !error.source) continue;
const analyticsPayload = {
entityName: error.source.name,
entityType: error.source.type,
entityId: error.source.id,
propertyPath: error.source.propertyPath ?? "",
analytics: validErrorPayload.analytics,
};
const errorMessages = error.messages;
yield put({
type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
payload: {
...analyticsPayload,
eventName: "DEBUGGER_RESOLVED_ERROR",
errorMessages,
appMode,
},
});
if (errorMessages) {
const currentEnvDetails: { id: string; name: string } = yield select(
getCurrentEnvironmentDetails,
);
//errorID has timestamp for 1:1 mapping with new and resolved errors
yield all(
errorMessages.map((errorMessage) => {
return put({
type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
payload: {
...analyticsPayload,
environmentId: currentEnvDetails.id,
environmentName: currentEnvDetails.name,
eventName: "DEBUGGER_RESOLVED_ERROR_MESSAGE",
errorId: error.id + "_" + error.timestamp,
errorMessage: errorMessage.message,
errorType: errorMessage.type,
errorSubType: errorMessage.subType,
appMode,
},
});
}),
);
}
}
const validErrorIds = validErrorPayloadsToDelete.map((payload) => payload.id);
yield put(deleteErrorLog(validErrorIds));
}
// takes a log object array and stores it in the redux store
export function* storeLogs(logs: LogObject[]) {
AppsmithConsole.addLogs(
logs.map((log: LogObject) => {
return {
text: createLogTitleString(log.data),
logData: log.data,
source: log.source,
severity: log.severity,
timestamp: log.timestamp,
category: LOG_CATEGORY.USER_GENERATED,
isExpanded: false,
};
}),
);
}
export function* updateTriggerMeta(
triggerMeta: TriggerMeta,
dynamicTrigger: string,
) {
let name = "";
if (!!triggerMeta.source && triggerMeta.source.hasOwnProperty("name")) {
name = triggerMeta.source.name;
} else if (!!triggerMeta.triggerPropertyName) {
name = triggerMeta.triggerPropertyName;
}
if (
name.length === 0 &&
!!dynamicTrigger &&
!(dynamicTrigger.includes("{") || dynamicTrigger.includes("}"))
) {
// We use the dynamic trigger as the name if it is not a binding
name = dynamicTrigger.replace("()", "");
triggerMeta["triggerPropertyName"] = name;
}
}
export default function* debuggerSagasListeners() {
yield all([
takeEvery(ReduxActionTypes.DEBUGGER_LOG_INIT, debuggerLogSaga),
takeEvery(
ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
logDebuggerErrorAnalyticsSaga,
),
takeEvery(
ReduxActionTypes.DEBUGGER_ADD_ERROR_LOG_INIT,
addDebuggerErrorLogsSaga,
),
takeEvery(
ReduxActionTypes.DEBUGGER_DELETE_ERROR_LOG_INIT,
deleteDebuggerErrorLogsSaga,
),
]);
}