chore: Refactor debugger analytics for active fields (#28036)

## Description
This PR fires debugger analytics for active fields only after the editor
onblur event is triggered.
#### PR fixes following issue(s)
Fixes #27679 
> if no issue exists, please create an issue and ask the maintainers
about this first
>
>
#### Media
> A video or a GIF is preferred. when using Loom, don’t embed because it
looks like it’s a GIF. instead, just link to the video
>
>
#### Type of change
> Please delete options that are not relevant.
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- Chore (housekeeping or task changes that don't impact user perception)
- This change requires a documentation update
>
>
>
## 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
This commit is contained in:
Favour Ohanekwu 2023-10-16 04:52:11 +01:00 committed by GitHub
parent b04548b915
commit 3d1640e0ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 283 additions and 56 deletions

View File

@ -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,
});

View File

@ -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,

View File

@ -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 = {

View File

@ -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;

View File

@ -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,
};

View File

@ -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<typeof mapStateToProps>;
type ReduxDispatchProps = ReturnType<typeof mapDispatchToProps>;
@ -1063,6 +1067,7 @@ class CodeEditor extends Component<Props, State> {
};
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<Props, State> {
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(

View File

@ -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;

View File

@ -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<Log[]>) {
// This saga is intended for analytics only
function* logDebuggerErrorAnalyticsSaga(
action: ReduxAction<LogDebuggerErrorAnalyticsPayload>,
) {
analyticsPayload: LogDebuggerErrorAnalyticsPayload,
currentDebuggerErrors: Record<string, Log>,
): unknown {
try {
const { payload } = action;
const payload = analyticsPayload;
const currentPageId: string | undefined = yield select(getCurrentPageId);
const { source } = payload;
const activeEditorField: ReturnType<typeof getActiveEditorField> =
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<Log[]>) {
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<Log[]>) {
);
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<Log[]>) {
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<Log[]>) {
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<string, Log>,
) {
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<typeof getAppMode> = 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<string, Log> =
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,

View File

@ -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}`,
},

View File

@ -0,0 +1,5 @@
import type { AppState } from "@appsmith/reducers";
export function getActiveEditorField(state: AppState) {
return state.ui.activeField;
}