diff --git a/app/client/src/actions/activeFieldActions.ts b/app/client/src/actions/activeFieldActions.ts new file mode 100644 index 0000000000..60f5a4bbcb --- /dev/null +++ b/app/client/src/actions/activeFieldActions.ts @@ -0,0 +1,9 @@ +import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; + +export const setActiveEditorField = (field: string) => ({ + type: ReduxActionTypes.SET_ACTIVE_EDITOR_FIELD, + payload: { field }, +}); +export const resetActiveEditorField = () => ({ + type: ReduxActionTypes.RESET_ACTIVE_EDITOR_FIELD, +}); diff --git a/app/client/src/actions/debuggerActions.ts b/app/client/src/actions/debuggerActions.ts index 59baecc197..557e80e4a6 100644 --- a/app/client/src/actions/debuggerActions.ts +++ b/app/client/src/actions/debuggerActions.ts @@ -1,5 +1,10 @@ import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; -import type { ENTITY_TYPE, Log, Message } from "entities/AppsmithConsole"; +import type { + ENTITY_TYPE, + Log, + Message, + SourceEntity, +} from "entities/AppsmithConsole"; import type { DebuggerContext } from "reducers/uiReducers/debuggerReducer"; import type { EventName } from "@appsmith/utils/analyticsUtilTypes"; import type { APP_MODE } from "entities/App"; @@ -17,6 +22,10 @@ export interface LogDebuggerErrorAnalyticsPayload { errorSubType?: Message["subType"]; analytics?: Log["analytics"]; appMode: APP_MODE; + source: SourceEntity; + logId: string; + environmentId?: string; + environmentName?: string; } export const debuggerLogInit = (payload: Log[]) => ({ @@ -61,14 +70,6 @@ export const deleteErrorLog = (ids: string[]) => ({ payload: ids, }); -// Only used for analytics -export const logDebuggerErrorAnalytics = ( - payload: LogDebuggerErrorAnalyticsPayload, -) => ({ - type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS, - payload, -}); - export const hideDebuggerErrors = (payload: boolean) => ({ type: ReduxActionTypes.HIDE_DEBUGGER_ERRORS, payload, diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index 8f3b498836..cc6798605e 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -866,6 +866,8 @@ const ActionTypes = { DELETE_MULTIPLE_APPLICATION_CANCEL: "DELETE_MULTIPLE_APPLICATION_CANCEL", TRIGGER_EVAL: "TRIGGER_EVAL", UPDATE_ACTION_DATA: "UPDATE_ACTION_DATA", + SET_ACTIVE_EDITOR_FIELD: "SET_ACTIVE_EDITOR_FIELD", + RESET_ACTIVE_EDITOR_FIELD: "RESET_ACTIVE_EDITOR_FIELD", }; export const ReduxActionTypes = { diff --git a/app/client/src/ce/reducers/index.tsx b/app/client/src/ce/reducers/index.tsx index 9b594ebd3a..2e66eaf0a6 100644 --- a/app/client/src/ce/reducers/index.tsx +++ b/app/client/src/ce/reducers/index.tsx @@ -79,6 +79,7 @@ import type { OneClickBindingState } from "reducers/uiReducers/oneClickBindingRe /* Reducers which are integrated into the core system when registering a pluggable module or done so by a module that is designed to be eventually pluggable */ import type { WidgetPositionsReduxState } from "layoutSystems/anvil/integrations/reducers/widgetPositionsReducer"; +import type { ActiveField } from "reducers/uiReducers/activeFieldEditorReducer"; export const reducerObject = { entities: entityReducer, @@ -142,6 +143,7 @@ export interface AppState { layoutConversion: layoutConversionReduxState; actionSelector: ActionSelectorReduxState; oneClickBinding: OneClickBindingState; + activeField: ActiveField; }; entities: { canvasWidgetsStructure: CanvasWidgetStructure; diff --git a/app/client/src/ce/reducers/uiReducers/index.tsx b/app/client/src/ce/reducers/uiReducers/index.tsx index 263e9d8058..31f4db3bdb 100644 --- a/app/client/src/ce/reducers/uiReducers/index.tsx +++ b/app/client/src/ce/reducers/uiReducers/index.tsx @@ -48,6 +48,7 @@ import autoHeightUIReducer from "reducers/uiReducers/autoHeightReducer"; import analyticsReducer from "reducers/uiReducers/analyticsReducer"; import layoutConversionReducer from "reducers/uiReducers/layoutConversionReducer"; import oneClickBindingReducer from "reducers/uiReducers/oneClickBindingReducer"; +import activeFieldReducer from "reducers/uiReducers/activeFieldEditorReducer"; export const uiReducerObject = { analytics: analyticsReducer, @@ -100,4 +101,5 @@ export const uiReducerObject = { layoutConversion: layoutConversionReducer, actionSelector: actionSelectorReducer, oneClickBinding: oneClickBindingReducer, + activeField: activeFieldReducer, }; diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx index a1c5598d04..38f31c4351 100644 --- a/app/client/src/components/editorComponents/CodeEditor/index.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx @@ -157,6 +157,10 @@ import { CodeEditorSignPosting } from "@appsmith/components/editorComponents/Cod import { getFocusablePropertyPaneField } from "selectors/propertyPaneSelectors"; import resizeObserver from "utils/resizeObserver"; import { EMPTY_BINDING } from "../ActionCreator/constants"; +import { + resetActiveEditorField, + setActiveEditorField, +} from "actions/activeFieldActions"; type ReduxStateProps = ReturnType; type ReduxDispatchProps = ReturnType; @@ -1063,6 +1067,7 @@ class CodeEditor extends Component { }; handleEditorFocus = (cm: CodeMirror.Editor) => { + this.props.setActiveField(this.props.dataTreePath || ""); this.setState({ isFocused: true }); const { sticky } = cm.getCursor(); const isUserFocus = sticky !== null; @@ -1118,6 +1123,7 @@ class CodeEditor extends Component { return; } } + this.props.resetActiveField(); this.handleChange(); this.setState({ isFocused: false }); this.editor.setOption("matchBrackets", false); @@ -1724,6 +1730,8 @@ const mapDispatchToProps = (dispatch: any) => ({ startingEntityUpdate: () => dispatch(startingEntityUpdate()), setCodeEditorLastFocus: (payload: CodeEditorFocusState) => dispatch(setEditorFieldFocusAction(payload)), + setActiveField: (path: string) => dispatch(setActiveEditorField(path)), + resetActiveField: () => dispatch(resetActiveEditorField()), }); export default Sentry.withProfiler( diff --git a/app/client/src/reducers/uiReducers/activeFieldEditorReducer.ts b/app/client/src/reducers/uiReducers/activeFieldEditorReducer.ts new file mode 100644 index 0000000000..9b53dfed19 --- /dev/null +++ b/app/client/src/reducers/uiReducers/activeFieldEditorReducer.ts @@ -0,0 +1,20 @@ +import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants"; +import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; +import { createReducer } from "utils/ReducerUtils"; + +export type ActiveField = null | string; +const initialState: ActiveField = null; + +const activeFieldReducer = createReducer(initialState, { + [ReduxActionTypes.SET_ACTIVE_EDITOR_FIELD]: ( + state: ActiveField, + action: ReduxAction<{ field: string }>, + ) => { + return action.payload.field; + }, + [ReduxActionTypes.RESET_ACTIVE_EDITOR_FIELD]: () => { + return initialState; + }, +}); + +export default activeFieldReducer; diff --git a/app/client/src/sagas/DebuggerSagas.ts b/app/client/src/sagas/DebuggerSagas.ts index 4bf9b77c6f..a8d370a88b 100644 --- a/app/client/src/sagas/DebuggerSagas.ts +++ b/app/client/src/sagas/DebuggerSagas.ts @@ -64,6 +64,13 @@ import { isWidget, } from "@appsmith/workers/Evaluation/evaluationUtils"; import { getCurrentEnvironmentDetails } from "@appsmith/selectors/environmentSelectors"; +import { getActiveEditorField } from "selectors/activeEditorFieldSelectors"; + +let blockedSource: string | null = null; + +function generateErrorId(error: Log) { + return error.id + "_" + error.timestamp; +} // Saga to format action request values to be shown in the debugger function* formatActionRequestSaga( @@ -376,12 +383,29 @@ function* debuggerLogSaga(action: ReduxAction) { // This saga is intended for analytics only function* logDebuggerErrorAnalyticsSaga( - action: ReduxAction, -) { + analyticsPayload: LogDebuggerErrorAnalyticsPayload, + currentDebuggerErrors: Record, +): unknown { try { - const { payload } = action; + const payload = analyticsPayload; const currentPageId: string | undefined = yield select(getCurrentPageId); - + const { source } = payload; + const activeEditorField: ReturnType = + yield select(getActiveEditorField); + const sourceFullPath = source.name + "." + source.propertyPath || ""; + // To prevent redundant logs for active editor fields + // We dispatch log events only after the onBlur event of the editor field is fired + if (sourceFullPath === activeEditorField) { + if (!blockedSource) { + blockedSource = sourceFullPath; + yield fork( + activeFieldDebuggerErrorHandler, + analyticsPayload, + currentDebuggerErrors, + ); + } + return; + } if (payload.entityType === ENTITY_TYPE.WIDGET) { const widget: WidgetProps | undefined = yield select( getWidget, @@ -474,15 +498,18 @@ function* addDebuggerErrorLogsSaga(action: ReduxAction) { if (!currentDebuggerErrors.hasOwnProperty(id)) { const errorMessages = errorLog.messages ?? []; - yield put({ - type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS, - payload: { + yield fork( + logDebuggerErrorAnalyticsSaga, + { ...analyticsPayload, eventName: "DEBUGGER_NEW_ERROR", errorMessages, appMode, - }, - }); + source, + logId: id, + } as LogDebuggerErrorAnalyticsPayload, + currentDebuggerErrors, + ); // Log analytics for new error messages //errorID has timestamp for 1:1 mapping with new and resolved errors @@ -492,20 +519,23 @@ function* addDebuggerErrorLogsSaga(action: ReduxAction) { ); yield all( errorMessages.map((errorMessage) => - put({ - type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS, - payload: { + fork( + logDebuggerErrorAnalyticsSaga, + { ...analyticsPayload, environmentId: currentEnvDetails.id, environmentName: currentEnvDetails.name, eventName: "DEBUGGER_NEW_ERROR_MESSAGE", - errorId: errorLog.id + "_" + errorLog.timestamp, + errorId: generateErrorId(errorLog), errorMessage: errorMessage.message, errorType: errorMessage.type, errorSubType: errorMessage.subType, appMode, - }, - }), + source, + logId: id, + } as LogDebuggerErrorAnalyticsPayload, + currentDebuggerErrors, + ), ), ); } @@ -527,20 +557,23 @@ function* addDebuggerErrorLogsSaga(action: ReduxAction) { if (exists < 0) { //errorID has timestamp for 1:1 mapping with new and resolved errors - return put({ - type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS, - payload: { + return fork( + logDebuggerErrorAnalyticsSaga, + { ...analyticsPayload, environmentId: currentEnvDetails.id, environmentName: currentEnvDetails.name, eventName: "DEBUGGER_NEW_ERROR_MESSAGE", - errorId: errorLog.id + "_" + errorLog.timestamp, + errorId: generateErrorId(errorLog), errorMessage: updatedErrorMessage.message, errorType: updatedErrorMessage.type, errorSubType: updatedErrorMessage.subType, appMode, - }, - }); + source, + logId: id, + } as LogDebuggerErrorAnalyticsPayload, + currentDebuggerErrors, + ); } }), ); @@ -556,23 +589,23 @@ function* addDebuggerErrorLogsSaga(action: ReduxAction) { if (exists < 0) { //errorID has timestamp for 1:1 mapping with new and resolved errors - return put({ - type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS, - payload: { + return fork( + logDebuggerErrorAnalyticsSaga, + { ...analyticsPayload, environmentId: currentEnvDetails.id, environmentName: currentEnvDetails.name, eventName: "DEBUGGER_RESOLVED_ERROR_MESSAGE", - errorId: - currentDebuggerErrors[id].id + - "_" + - currentDebuggerErrors[id].timestamp, + errorId: generateErrorId(currentDebuggerErrors[id]), errorMessage: existingErrorMessage.message, errorType: existingErrorMessage.type, errorSubType: existingErrorMessage.subType, appMode, - }, - }); + source, + logId: id, + } as LogDebuggerErrorAnalyticsPayload, + currentDebuggerErrors, + ); } }), ); @@ -611,15 +644,18 @@ function* deleteDebuggerErrorLogsSaga( }; const errorMessages = error.messages; - yield put({ - type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS, - payload: { + yield fork( + logDebuggerErrorAnalyticsSaga, + { ...analyticsPayload, eventName: "DEBUGGER_RESOLVED_ERROR", errorMessages, appMode, - }, - }); + source: error.source, + logId: error.id, + } as LogDebuggerErrorAnalyticsPayload, + currentDebuggerErrors, + ); if (errorMessages) { const currentEnvDetails: { id: string; name: string } = yield select( @@ -628,20 +664,23 @@ function* deleteDebuggerErrorLogsSaga( //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: { + return fork( + logDebuggerErrorAnalyticsSaga, + { ...analyticsPayload, environmentId: currentEnvDetails.id, environmentName: currentEnvDetails.name, eventName: "DEBUGGER_RESOLVED_ERROR_MESSAGE", - errorId: error.id + "_" + error.timestamp, + errorId: generateErrorId(error), errorMessage: errorMessage.message, errorType: errorMessage.type, errorSubType: errorMessage.subType, appMode, - }, - }); + source: error.source, + logId: error.id, + } as LogDebuggerErrorAnalyticsPayload, + currentDebuggerErrors, + ); }), ); } @@ -689,15 +728,154 @@ export function* updateTriggerMeta( triggerMeta["triggerPropertyName"] = name; } } +// This function handles logging of debugger error events for active editor fields +// Error logs are fired only after the editor gets blur +function* activeFieldDebuggerErrorHandler( + analyticsPayload: LogDebuggerErrorAnalyticsPayload, + currentDebuggerErrors: Record, +) { + const { logId, source } = analyticsPayload; + const initialSourceDebuggerError: Log = currentDebuggerErrors[logId]; + const sourceMetaData = { + entityName: source.name, + entityType: source.type, + entityId: source.id, + propertyPath: source.propertyPath ?? "", + source: source, + }; + const appMode: ReturnType = yield select(getAppMode); + const currentEnvDetails: { id: string; name: string } = yield select( + getCurrentEnvironmentDetails, + ); + const envMetaData = { + appMode, + environmentId: currentEnvDetails.id, + environmentName: currentEnvDetails.name, + }; + yield take(ReduxActionTypes.RESET_ACTIVE_EDITOR_FIELD); + + const latestDebuggerErrors: Record = + yield select(getDebuggerErrors); + const latestSourceDebuggerError: Log = latestDebuggerErrors[logId]; + blockedSource = null; + + if (!initialSourceDebuggerError && latestSourceDebuggerError) { + yield fork( + logDebuggerErrorAnalyticsSaga, + { + ...sourceMetaData, + ...envMetaData, + eventName: "DEBUGGER_NEW_ERROR", + errorMessages: latestSourceDebuggerError.messages, + errorId: generateErrorId(latestSourceDebuggerError), + } as LogDebuggerErrorAnalyticsPayload, + latestDebuggerErrors, + ); + + yield all( + latestSourceDebuggerError.messages?.map((errorMessage) => + fork( + logDebuggerErrorAnalyticsSaga, + { + ...sourceMetaData, + ...envMetaData, + eventName: "DEBUGGER_NEW_ERROR_MESSAGE", + errorId: generateErrorId(latestSourceDebuggerError), + errorMessage: errorMessage.message, + errorType: errorMessage.type, + errorSubType: errorMessage.subType, + } as LogDebuggerErrorAnalyticsPayload, + currentDebuggerErrors, + ), + ) || [], + ); + } + + if (!latestSourceDebuggerError && initialSourceDebuggerError) { + yield fork( + logDebuggerErrorAnalyticsSaga, + { + ...sourceMetaData, + ...envMetaData, + eventName: "DEBUGGER_RESOLVED_ERROR", + errorMessages: initialSourceDebuggerError.messages, + errorId: generateErrorId(initialSourceDebuggerError), + } as LogDebuggerErrorAnalyticsPayload, + latestDebuggerErrors, + ); + + yield all( + initialSourceDebuggerError.messages?.map((errorMessage) => { + return fork( + logDebuggerErrorAnalyticsSaga, + { + ...sourceMetaData, + ...envMetaData, + eventName: "DEBUGGER_RESOLVED_ERROR_MESSAGE", + errorMessage: errorMessage.message, + errorId: generateErrorId(initialSourceDebuggerError), + } as LogDebuggerErrorAnalyticsPayload, + latestDebuggerErrors, + ); + }) || [], + ); + } + + if (latestSourceDebuggerError && initialSourceDebuggerError) { + const latestErrorMessages = latestSourceDebuggerError.messages || []; + const initialErrorMessages = initialSourceDebuggerError.messages || []; + yield all( + initialErrorMessages.map((initialErrorMessage) => { + const exists = findIndex(latestErrorMessages, (latestErrorMessage) => { + return isMatch(latestErrorMessage, initialErrorMessage); + }); + + if (exists < 0) { + return put({ + type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS, + payload: { + ...sourceMetaData, + ...envMetaData, + eventName: "DEBUGGER_RESOLVED_ERROR_MESSAGE", + errorMessage: initialErrorMessage.message, + errorId: generateErrorId(initialSourceDebuggerError), + }, + }); + } + }), + ); + yield all( + latestErrorMessages.map((latestErrorMessage) => { + const exists = findIndex( + initialErrorMessages, + (initialErrorMessage) => { + return isMatch(initialErrorMessage, latestErrorMessage); + }, + ); + + if (exists < 0) { + return fork( + logDebuggerErrorAnalyticsSaga, + { + ...sourceMetaData, + ...envMetaData, + eventName: "DEBUGGER_NEW_ERROR_MESSAGE", + errorMessage: latestErrorMessage.message, + errorType: latestErrorMessage.type, + errorSubType: latestErrorMessage.subType, + errorId: generateErrorId(latestSourceDebuggerError), + } as LogDebuggerErrorAnalyticsPayload, + currentDebuggerErrors, + ); + } + }), + ); + } +} 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, diff --git a/app/client/src/sagas/PostEvaluationSagas.ts b/app/client/src/sagas/PostEvaluationSagas.ts index dfb89089f8..19c305877f 100644 --- a/app/client/src/sagas/PostEvaluationSagas.ts +++ b/app/client/src/sagas/PostEvaluationSagas.ts @@ -550,7 +550,7 @@ export function* handleJSFunctionExecutionErrorLog( }), source: { id: action.collectionId ? action.collectionId : action.id, - name: `${collectionName}.${action.name}`, + name: collectionName, type: ENTITY_TYPE.JSACTION, propertyPath: `${action.name}`, }, diff --git a/app/client/src/selectors/activeEditorFieldSelectors.ts b/app/client/src/selectors/activeEditorFieldSelectors.ts new file mode 100644 index 0000000000..7e9e5ff6fb --- /dev/null +++ b/app/client/src/selectors/activeEditorFieldSelectors.ts @@ -0,0 +1,5 @@ +import type { AppState } from "@appsmith/reducers"; + +export function getActiveEditorField(state: AppState) { + return state.ui.activeField; +}