Skip to content

Commit

Permalink
chore: Improve js execution instrumentation (appsmithorg#24994)
Browse files Browse the repository at this point in the history
###Description

This PR enriches the data logged for the EXECUTE_ACTION event on trigger
fields.

Example schema
<img width="500" alt="Screenshot 2023-07-05 at 10 03 32"
src="https://github.com/appsmithorg/appsmith/assets/46670083/13b3ab48-6c19-453a-8eb8-c87129e8c8d5">


Fixes appsmithorg#24706 

#### 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
- [ ] 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
  • Loading branch information
ohansFavour authored Jul 6, 2023
1 parent f24ecc2 commit a7f818d
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 31 deletions.
38 changes: 26 additions & 12 deletions app/client/src/sagas/EvalWorkerActionSagas.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { all, call, put, spawn, take } from "redux-saga/effects";
import { all, call, fork, put, spawn, take } from "redux-saga/effects";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import { MAIN_THREAD_ACTION } from "@appsmith/workers/Evaluation/evalWorkerActions";
import log from "loglevel";
Expand Down Expand Up @@ -30,11 +30,16 @@ import isEmpty from "lodash/isEmpty";
import type { UnEvalTree } from "entities/DataTree/dataTreeFactory";
import { sortJSExecutionDataByCollectionId } from "workers/Evaluation/JSObject/utils";
import type { LintTreeSagaRequestData } from "plugins/Linting/types";
import AnalyticsUtil from "utils/AnalyticsUtil";
export type UpdateDataTreeMessageData = {
workerResponse: EvalTreeResponseData;
unevalTree: UnEvalTree;
};
import { logJSActionExecution } from "./analyticsSaga";
import { uniq } from "lodash";
import type {
TriggerKind,
TriggerSource,
} from "constants/AppsmithActionConstants/ActionConstants";

export function* handleEvalWorkerRequestSaga(listenerChannel: Channel<any>) {
while (true) {
Expand Down Expand Up @@ -106,18 +111,27 @@ export function* processTriggerHandler(message: any) {
if (messageType === MessageType.REQUEST)
yield call(evalWorker.respond, message.messageId, result);
}
export function* handleJSExecutionLog(data: TMessage<{ data: string[] }>) {
export function* handleJSExecutionLog(
data: TMessage<{
data: {
jsFnFullName: string;
isSuccess: boolean;
triggerMeta: {
source: TriggerSource;
triggerPropertyName: string | undefined;
triggerKind: TriggerKind | undefined;
};
}[];
}>,
) {
const {
body: { data: executedFns },
body: { data: executionData },
} = data;

for (const executedFn of executedFns) {
AnalyticsUtil.logEvent("EXECUTE_ACTION", {
type: "JS",
name: executedFn,
});
}
yield call(logJSFunctionExecution, data);
const executedFns = uniq(
executionData.map((execData) => execData.jsFnFullName),
);
yield fork(logJSActionExecution, executionData);
yield call(logJSFunctionExecution, executedFns);
}

export function* handleEvalWorkerMessage(message: TMessage<any>) {
Expand Down
7 changes: 6 additions & 1 deletion app/client/src/sagas/EvaluationsSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import { handleEvalWorkerRequestSaga } from "./EvalWorkerActionSagas";
import { getAppsmithConfigs } from "@appsmith/configs";
import { executeJSUpdates } from "actions/pluginActionActions";
import { setEvaluatedActionSelectorField } from "actions/actionSelectorActions";
import { logDynamicTriggerExecution } from "./analyticsSaga";

const APPSMITH_CONFIGS = getAppsmithConfigs();

Expand Down Expand Up @@ -312,7 +313,6 @@ export function* evaluateAndExecuteDynamicTrigger(
const unEvalTree: ReturnType<typeof getUnevaluatedDataTree> = yield select(
getUnevaluatedDataTree,
);
// const unEvalTree = unEvalAndConfigTree.unEvalTree;
log.debug({ execute: dynamicTrigger });
const response: { errors: EvaluationError[]; result: unknown } = yield call(
evalWorker.request,
Expand All @@ -328,6 +328,11 @@ export function* evaluateAndExecuteDynamicTrigger(
);
const { errors = [] } = response as any;
yield call(dynamicTriggerErrorHandler, errors);
yield fork(logDynamicTriggerExecution, {
dynamicTrigger,
errors,
triggerMeta,
});
return response;
}

Expand Down
224 changes: 224 additions & 0 deletions app/client/src/sagas/analyticsSaga.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { getCurrentUser } from "selectors/usersSelectors";
import { getInstanceId } from "@appsmith/selectors/tenantSelectors";
import { getAppsmithConfigs } from "@appsmith/configs";
import { call, select } from "redux-saga/effects";
import type { APP_MODE } from "entities/App";
import {
getCurrentApplication,
getCurrentPageId,
} from "selectors/editorSelectors";
import type { TriggerMeta } from "@appsmith/sagas/ActionExecution/ActionExecutionSagas";
import type { TriggerSource } from "constants/AppsmithActionConstants/ActionConstants";
import { TriggerKind } from "constants/AppsmithActionConstants/ActionConstants";
import { isArray } from "lodash";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { getEntityNameAndPropertyPath } from "@appsmith/workers/Evaluation/evaluationUtils";
import { getAppMode, getJSActionFromName } from "selectors/entitiesSelector";
import type { AppState } from "@appsmith/reducers";
import { getWidget } from "./selectors";

export interface UserAndAppDetails {
pageId: string;
appId: string;
appMode: APP_MODE | undefined;
appName: string;
isExampleApp: boolean;
userId: string;
email: string;
source: string;
instanceId: string;
}

export function* getUserAndAppDetails() {
const appMode: ReturnType<typeof getAppMode> = yield select(getAppMode);
const currentApp: ReturnType<typeof getCurrentApplication> = yield select(
getCurrentApplication,
);
const user: ReturnType<typeof getCurrentUser> = yield select(getCurrentUser);
const instanceId: ReturnType<typeof getInstanceId> = yield select(
getInstanceId,
);
const { cloudHosting } = getAppsmithConfigs();
const source = cloudHosting ? "cloud" : "ce";
const pageId: ReturnType<typeof getCurrentPageId> = yield select(
getCurrentPageId,
);
const userAndAppDetails: UserAndAppDetails = {
pageId,
appId: currentApp?.id || "",
appMode,
appName: currentApp?.name || "",
isExampleApp: currentApp?.appIsExample || false,
userId: user?.username || "",
email: user?.email || "",
source,
instanceId: instanceId,
};

return userAndAppDetails;
}
export function* logDynamicTriggerExecution({
dynamicTrigger,
errors,
triggerMeta,
}: {
dynamicTrigger: string;
errors: unknown;
triggerMeta: TriggerMeta;
}) {
if (triggerMeta.triggerKind !== TriggerKind.EVENT_EXECUTION) return;
const isUnsuccessfulExecution = isArray(errors) && errors.length > 0;
const {
appId,
appMode,
appName,
email,
instanceId,
isExampleApp,
pageId,
source,
userId,
}: UserAndAppDetails = yield call(getUserAndAppDetails);
const widget: ReturnType<typeof getWidget> | undefined = yield select(
(state: AppState) => getWidget(state, triggerMeta.source?.id || ""),
);

const dynamicPropertyPathList = widget?.dynamicPropertyPathList;
const isJSToggled = !!dynamicPropertyPathList?.find(
(property) => property.key === triggerMeta.triggerPropertyName,
);
AnalyticsUtil.logEvent("EXECUTE_ACTION", {
type: "JS_EXPRESSION",
unevalValue: dynamicTrigger,
pageId,
appId,
appMode,
appName,
isExampleApp,
userData: {
userId,
email,
appId,
source,
},
widgetName: widget?.widgetName,
widgetType: widget?.type,
propertyName: triggerMeta.triggerPropertyName,
instanceId,
isJSToggled,
});

AnalyticsUtil.logEvent(
isUnsuccessfulExecution
? "EXECUTE_ACTION_FAILURE"
: "EXECUTE_ACTION_SUCCESS",
{
type: "JS_EXPRESSION",
unevalValue: dynamicTrigger,
pageId,
appId,
appMode,
appName,
isExampleApp,
userData: {
userId,
email,
appId,
source,
},
widgetName: widget?.widgetName,
widgetType: widget?.type,
propertyName: triggerMeta.triggerPropertyName,
instanceId,
isJSToggled,
},
);
}

export function* logJSActionExecution(
executionData: {
jsFnFullName: string;
isSuccess: boolean;
triggerMeta: {
source: TriggerSource;
triggerPropertyName: string | undefined;
triggerKind: TriggerKind | undefined;
};
}[],
) {
const {
appId,
appMode,
appName,
email,
instanceId,
isExampleApp,
pageId,
source,
userId,
}: UserAndAppDetails = yield call(getUserAndAppDetails);
for (const { isSuccess, jsFnFullName, triggerMeta } of executionData) {
const { entityName: JSObjectName, propertyPath: functionName } =
getEntityNameAndPropertyPath(jsFnFullName);
const jsAction: ReturnType<typeof getJSActionFromName> = yield select(
(state: AppState) =>
getJSActionFromName(state, JSObjectName, functionName),
);
const triggeredWidget: ReturnType<typeof getWidget> | undefined =
yield select((state: AppState) =>
getWidget(state, triggerMeta.source?.id || ""),
);
const dynamicPropertyPathList = triggeredWidget?.dynamicPropertyPathList;
const isJSToggled = !!dynamicPropertyPathList?.find(
(property) => property.key === triggerMeta.triggerPropertyName,
);
AnalyticsUtil.logEvent("EXECUTE_ACTION", {
type: "JS",
name: functionName,
JSObjectName,
pageId,
appId,
appMode,
appName,
isExampleApp,
actionId: jsAction?.id,
userData: {
userId,
email,
appId,
source,
},
widgetName: triggeredWidget?.widgetName,
widgetType: triggeredWidget?.type,
propertyName: triggerMeta.triggerPropertyName,
isJSToggled,
instanceId,
});

AnalyticsUtil.logEvent(
isSuccess ? "EXECUTE_ACTION_SUCCESS" : "EXECUTE_ACTION_FAILURE",
{
type: "JS",
name: functionName,
JSObjectName,
pageId,
appId,
appMode,
appName,
isExampleApp,
actionId: jsAction?.id,
userData: {
userId,
email,
appId,
source,
},
widgetName: triggeredWidget?.widgetName,
widgetType: triggeredWidget?.type,
propertyName: triggerMeta.triggerPropertyName,
isJSToggled,
instanceId,
},
);
}
}
18 changes: 18 additions & 0 deletions app/client/src/selectors/entitiesSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,24 @@ export const getJSCollectionFromName = createSelector(
return currentJSCollection;
},
);
export const getJSActionFromName = createSelector(
[
(state: AppState, jsCollectionName: string) =>
getJSCollectionFromName(state, jsCollectionName),
(_state: AppState, jsCollectionName: string, functionName: string) => ({
jsCollectionName,
functionName,
}),
],
(JSCollectionData, { functionName }) => {
if (!JSCollectionData) return null;
const jsFunction = find(
JSCollectionData.config.actions,
(action) => action.name === functionName,
);
return jsFunction || null;
},
);

export const getJSActionFromJSCollection = (
JSCollection: JSCollectionData,
Expand Down
7 changes: 0 additions & 7 deletions app/client/src/workers/Evaluation/JSObject/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {
ConfigTree,
DataTree,
AppsmithEntity,
DataTreeEntity,
} from "entities/DataTree/dataTreeFactory";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
Expand All @@ -22,7 +21,6 @@ import {
isJSAction,
} from "@appsmith/workers/Evaluation/evaluationUtils";
import JSObjectCollection from "./Collection";
import type { APP_MODE } from "entities/App";
import type {
JSActionEntityConfig,
JSActionEntity,
Expand Down Expand Up @@ -272,11 +270,6 @@ export function isJSObjectVariable(
);
}

export function getAppMode(dataTree: DataTree) {
const appsmithObj = dataTree.appsmith as AppsmithEntity;
return appsmithObj.mode as APP_MODE;
}

export function isPromise(value: any): value is Promise<unknown> {
return Boolean(value && typeof value.then === "function");
}
Expand Down
Loading

0 comments on commit a7f818d

Please sign in to comment.