diff --git a/app/client/src/actions/evaluationActions.ts b/app/client/src/actions/evaluationActions.ts index 2066ee8bb8..60089604f5 100644 --- a/app/client/src/actions/evaluationActions.ts +++ b/app/client/src/actions/evaluationActions.ts @@ -33,6 +33,13 @@ export const LINT_REDUX_ACTIONS = { [ReduxActionTypes.META_UPDATE_DEBOUNCED_EVAL]: true, }; +export const LOG_REDUX_ACTIONS = [ + ReduxActionTypes.UPDATE_LAYOUT, + ReduxActionTypes.UPDATE_WIDGET_PROPERTY, + ReduxActionTypes.UPDATE_WIDGET_NAME_SUCCESS, + ReduxActionTypes.CREATE_ACTION_SUCCESS, +]; + export const EVALUATE_REDUX_ACTIONS = [ ...FIRST_EVAL_REDUX_ACTIONS, // Actions @@ -121,6 +128,10 @@ export function shouldLint(action: ReduxAction) { return LINT_REDUX_ACTIONS[action.type]; } +export function shouldLog(action: ReduxAction) { + return LOG_REDUX_ACTIONS.includes(action.type); +} + export const setEvaluatedTree = ( updates: Diff[], ): ReduxAction<{ updates: Diff[] }> => { diff --git a/app/client/src/ce/workers/Evaluation/evaluationUtils.ts b/app/client/src/ce/workers/Evaluation/evaluationUtils.ts index f7fb743774..1b6e9b3e98 100644 --- a/app/client/src/ce/workers/Evaluation/evaluationUtils.ts +++ b/app/client/src/ce/workers/Evaluation/evaluationUtils.ts @@ -43,7 +43,7 @@ export enum DataTreeDiffEvent { NEW = "NEW", DELETE = "DELETE", EDIT = "EDIT", - NOOP = "NOOP", + NOOP = "NOOP", // No Operation (don’t do anything) } export type DataTreeDiff = { diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts index 424be3c098..dcf16a9050 100644 --- a/app/client/src/sagas/EvaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -42,6 +42,7 @@ import { setDependencyMap, setEvaluatedTree, shouldLint, + shouldLog, shouldProcessBatchedAction, } from "actions/evaluationActions"; import ConfigTreeActions from "utils/configTree"; @@ -136,6 +137,7 @@ export function* evaluateTreeSaga( shouldReplay = true, requiresLinting = false, forceEvaluation = false, + requiresLogging = false, ) { const allActionValidationConfig: { [actionId: string]: ActionValidationConfigMap; @@ -184,6 +186,7 @@ export function* evaluateTreeSaga( configTree, staleMetaIds, pathsToClearErrorsFor, + isNewWidgetAdded, } = workerResponse; PerformanceTracker.stopAsyncTracking( @@ -230,14 +233,18 @@ export function* evaluateTreeSaga( if (appMode !== APP_MODE.PUBLISHED) { const jsData: Record = yield select(getAllJSActionsData); yield call(makeUpdateJSCollection, jsUpdates); - yield fork( - logSuccessfulBindings, - unevalTree, - updatedDataTree, - evaluationOrder, - isCreateFirstTree, - configTree, - ); + + if (requiresLogging) { + yield fork( + logSuccessfulBindings, + unevalTree, + updatedDataTree, + evaluationOrder, + isCreateFirstTree, + isNewWidgetAdded, + configTree, + ); + } yield fork( updateTernDefinitions, @@ -578,7 +585,7 @@ function* evaluationChangeListenerSaga(): any { type: ReduxActionType; postEvalActions: Array>; } = yield take(FIRST_EVAL_REDUX_ACTIONS); - yield fork(evaluateTreeSaga, initAction.postEvalActions, false, true); + yield fork(evaluateTreeSaga, initAction.postEvalActions, false, true, false); const evtActionChannel: ActionPattern> = yield actionChannel( EVALUATE_REDUX_ACTIONS, evalQueueBuffer(), @@ -596,6 +603,8 @@ function* evaluationChangeListenerSaga(): any { postEvalActions, get(action, "payload.shouldReplay"), shouldLint(action), + false, + shouldLog(action), ); } } diff --git a/app/client/src/sagas/PostEvaluationSagas.ts b/app/client/src/sagas/PostEvaluationSagas.ts index fdc852b4a3..a10324a371 100644 --- a/app/client/src/sagas/PostEvaluationSagas.ts +++ b/app/client/src/sagas/PostEvaluationSagas.ts @@ -47,6 +47,10 @@ import type FeatureFlags from "entities/FeatureFlags"; import type { JSAction } from "entities/JSCollection"; import { isWidgetPropertyNamePath } from "utils/widgetEvalUtils"; import type { ActionEntityConfig } from "entities/DataTree/types"; +import SuccessfulBindingMap from "utils/SuccessfulBindingsMap"; +import type { SuccessfulBindings } from "utils/SuccessfulBindingsMap"; + +let successfulBindingsMap: SuccessfulBindingMap | undefined; const getDebuggerErrors = (state: AppState) => state.ui.debugger.errors; @@ -317,17 +321,17 @@ export function* logSuccessfulBindings( dataTree: DataTree, evaluationOrder: string[], isCreateFirstTree: boolean, + isNewWidgetAdded: boolean, configTree: ConfigTree, ) { const appMode: APP_MODE | undefined = yield select(getAppMode); if (appMode === APP_MODE.PUBLISHED) return; if (!evaluationOrder) return; - if (isCreateFirstTree) { - // we only aim to log binding success which were added by user - // for first evaluation, bindings are not added by user hence skipping it. - return; - } + const successfulBindingPaths: SuccessfulBindings = !successfulBindingsMap + ? {} + : { ...successfulBindingsMap.get() }; + evaluationOrder.forEach((evaluatedPath) => { const { entityName, propertyPath } = getEntityNameAndPropertyPath(evaluatedPath); @@ -345,6 +349,23 @@ export function* logSuccessfulBindings( }); const logBlackList = entityConfig.logBlackList; + + if (!isABinding || propertyPath in logBlackList) { + /**Remove the binding from the map so that in case it is added again, we log it*/ + if (successfulBindingPaths[evaluatedPath]) { + delete successfulBindingPaths[evaluatedPath]; + } + return; + } + + /** All the paths that are added when a new widget is added needs to be added to the map so that + * we don't log them again unless they are changed by the user. + */ + if (isNewWidgetAdded) { + successfulBindingPaths[evaluatedPath] = unevalValue; + return; + } + const errors: EvaluationError[] = get( dataTree, getEvalErrorPath(evaluatedPath), @@ -352,16 +373,44 @@ export function* logSuccessfulBindings( ) as EvaluationError[]; const hasErrors = errors.length > 0; + if (!hasErrors) { + if (!isCreateFirstTree) { + // we only aim to log binding success which were added by user + // for first evaluation, bindings are not added by user hence skipping it. + AnalyticsUtil.logEvent("BINDING_SUCCESS", { + unevalValue, + entityType, + propertyPath, + }); - if (isABinding && !hasErrors && !(propertyPath in logBlackList)) { - AnalyticsUtil.logEvent("BINDING_SUCCESS", { - unevalValue, - entityType, - propertyPath, - }); + /**Log the binding only if it doesn't already exist */ + if ( + !successfulBindingPaths[evaluatedPath] || + (successfulBindingPaths[evaluatedPath] && + successfulBindingPaths[evaluatedPath] !== unevalValue) + ) { + AnalyticsUtil.logEvent("ENTITY_BINDING_SUCCESS", { + unevalValue, + entityType, + propertyPath, + }); + } + } + successfulBindingPaths[evaluatedPath] = unevalValue; + } else { + /**Remove the binding from the map so that in case it is added again, we log it*/ + if (successfulBindingPaths[evaluatedPath]) { + delete successfulBindingPaths[evaluatedPath]; + } } } }); + + if (!successfulBindingsMap) { + successfulBindingsMap = new SuccessfulBindingMap(successfulBindingPaths); + } else { + successfulBindingsMap.set(successfulBindingPaths); + } } export function* postEvalActionDispatcher(actions: Array) { diff --git a/app/client/src/utils/AnalyticsUtil.tsx b/app/client/src/utils/AnalyticsUtil.tsx index fa3ab5e75a..fc98b21d65 100644 --- a/app/client/src/utils/AnalyticsUtil.tsx +++ b/app/client/src/utils/AnalyticsUtil.tsx @@ -134,6 +134,7 @@ export type EventName = | "DISCORD_LINK_CLICK" | "INTERCOM_CLICK" | "BINDING_SUCCESS" + | "ENTITY_BINDING_SUCCESS" | "APP_MENU_OPTION_CLICK" | "SLASH_COMMAND" | "DEBUGGER_NEW_ERROR" diff --git a/app/client/src/utils/SuccessfulBindingsMap.ts b/app/client/src/utils/SuccessfulBindingsMap.ts new file mode 100644 index 0000000000..3ffdb2a82d --- /dev/null +++ b/app/client/src/utils/SuccessfulBindingsMap.ts @@ -0,0 +1,20 @@ +import type { UnEvalTreeEntity } from "entities/DataTree/dataTreeFactory"; + +export type SuccessfulBindings = { + [entityName: string]: UnEvalTreeEntity; +}; +export default class SuccessfulBindingMap { + successfulBindings: SuccessfulBindings; + + constructor(successfulBindings: SuccessfulBindings) { + this.successfulBindings = successfulBindings; + } + + set(successfulBindings: SuccessfulBindings) { + this.successfulBindings = successfulBindings; + } + + get() { + return this.successfulBindings; + } +} diff --git a/app/client/src/workers/Evaluation/handlers/evalTree.ts b/app/client/src/workers/Evaluation/handlers/evalTree.ts index 57f9ee00a4..b48a077c37 100644 --- a/app/client/src/workers/Evaluation/handlers/evalTree.ts +++ b/app/client/src/workers/Evaluation/handlers/evalTree.ts @@ -40,6 +40,7 @@ export default function (request: EvalWorkerSyncRequest) { let configTree: ConfigTree = {}; let staleMetaIds: string[] = []; let pathsToClearErrorsFor: any[] = []; + let isNewWidgetAdded = false; const { allActionValidationConfig, @@ -149,6 +150,7 @@ export default function (request: EvalWorkerSyncRequest) { jsUpdates = setupUpdateTreeResponse.jsUpdates; unEvalUpdates = setupUpdateTreeResponse.unEvalUpdates; pathsToClearErrorsFor = setupUpdateTreeResponse.pathsToClearErrorsFor; + isNewWidgetAdded = setupUpdateTreeResponse.isNewWidgetAdded; initiateLinting( lintOrder, @@ -225,6 +227,7 @@ export default function (request: EvalWorkerSyncRequest) { configTree, staleMetaIds, pathsToClearErrorsFor, + isNewWidgetAdded, }; return evalTreeResponse; diff --git a/app/client/src/workers/Evaluation/types.ts b/app/client/src/workers/Evaluation/types.ts index 57b2bd7169..f841d890ec 100644 --- a/app/client/src/workers/Evaluation/types.ts +++ b/app/client/src/workers/Evaluation/types.ts @@ -53,4 +53,5 @@ export interface EvalTreeResponseData { configTree: ConfigTree; staleMetaIds: string[]; pathsToClearErrorsFor: any[]; + isNewWidgetAdded: boolean; } diff --git a/app/client/src/workers/Linting/lint.worker.ts b/app/client/src/workers/Linting/lint.worker.ts index 76cb648c84..56ebefbb24 100644 --- a/app/client/src/workers/Linting/lint.worker.ts +++ b/app/client/src/workers/Linting/lint.worker.ts @@ -74,6 +74,7 @@ function eventRequestHandler({ configTree, cloudHosting, ); + lintTreeResponse.errors = lintErrors; } catch (e) {} return lintTreeResponse; diff --git a/app/client/src/workers/common/DataTreeEvaluator/index.ts b/app/client/src/workers/common/DataTreeEvaluator/index.ts index 0e0f7a9440..f716be6b8a 100644 --- a/app/client/src/workers/common/DataTreeEvaluator/index.ts +++ b/app/client/src/workers/common/DataTreeEvaluator/index.ts @@ -52,6 +52,7 @@ import { isNewEntity, getStaleMetaStateIds, convertJSFunctionsToString, + DataTreeDiffEvent, } from "@appsmith/workers/Evaluation/evaluationUtils"; import { difference, @@ -378,6 +379,7 @@ export default class DataTreeEvaluator { jsUpdates: Record; nonDynamicFieldValidationOrder: string[]; pathsToClearErrorsFor: any[]; + isNewWidgetAdded: boolean; } { const totalUpdateTreeSetupStartTime = performance.now(); @@ -451,14 +453,32 @@ export default class DataTreeEvaluator { lintOrder: [], jsUpdates: {}, nonDynamicFieldValidationOrder: [], + isNewWidgetAdded: false, }; } + let isNewWidgetAdded = false; + //find all differences which can lead to updating of dependency map const translatedDiffs = flatten( differences.map((diff) => translateDiffEventToDataTreeDiffEvent(diff, localUnEvalTree), ), ); + + /** We need to know if a new widget was added so that we do not fire ENTITY_BINDING_SUCCESS event */ + for (let i = 0; i < translatedDiffs.length; i++) { + const diffEvent = translatedDiffs[i]; + if (diffEvent.event === DataTreeDiffEvent.NEW) { + const entity = localUnEvalTree[diffEvent.payload.propertyPath]; + + if (isWidget(entity)) { + isNewWidgetAdded = true; + + break; + } + } + } + const diffCheckTimeStopTime = performance.now(); this.logs.push({ differences, @@ -571,6 +591,7 @@ export default class DataTreeEvaluator { nonDynamicFieldValidationOrderSet, ), pathsToClearErrorsFor, + isNewWidgetAdded, }; }