chore: Instrument JS execution (#25613)

This PR adds more details to the "EXECUTE_ACTION" event on trigger
fields

<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">

#### PR fixes following issue(s)
Fixes #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

---------

Co-authored-by: Aishwarya UR <aishwarya@appsmith.com>
This commit is contained in:
Favour Ohanekwu 2023-07-27 12:07:56 +01:00 committed by GitHub
parent de66a50f6c
commit 60fa6e352d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 325 additions and 52 deletions

View File

@ -1,3 +1,30 @@
export function* logJSFunctionExecution(data: any) {
return data;
import { TriggerKind } from "constants/AppsmithActionConstants/ActionConstants";
import type { TriggerSource } from "constants/AppsmithActionConstants/ActionConstants";
import { call } from "redux-saga/effects";
import type { TMessage } from "utils/MessageUtil";
import { logJSActionExecution } from "./analyticsSaga";
export function* logJSFunctionExecution(
data: TMessage<{
data: {
jsFnFullName: string;
isSuccess: boolean;
triggerMeta: {
source: TriggerSource;
triggerPropertyName: string | undefined;
triggerKind: TriggerKind | undefined;
};
}[];
}>,
) {
const {
body: { data: executionData },
} = data;
// We only care about EVENT_EXECUTION
const triggerExecutionData = executionData.filter(
(execData) =>
execData.triggerMeta.triggerKind === TriggerKind.EVENT_EXECUTION,
);
yield call(logJSActionExecution, triggerExecutionData);
}

View File

@ -0,0 +1,228 @@
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 "sagas/selectors";
export function getUserSource() {
const { cloudHosting } = getAppsmithConfigs();
const source = cloudHosting ? "cloud" : "ce";
return source;
}
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 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: getUserSource(),
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,
},
);
}
}

View File

@ -1,4 +1,16 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function postJSFunctionExecutionLog(fullName: string) {
//
import TriggerEmitter, {
BatchKey,
} from "workers/Evaluation/fns/utils/TriggerEmitter";
import type { PostProcessorArg } from "workers/Evaluation/fns/utils/jsObjectFnFactory";
export function postJSFunctionExecutionLog({
executionMetaData,
isSuccess,
jsFnFullName,
}: PostProcessorArg) {
TriggerEmitter.emit(BatchKey.process_batched_fn_invoke_log, {
jsFnFullName,
isSuccess,
triggerMeta: executionMetaData.triggerMeta,
});
}

View File

@ -0,0 +1 @@
export * from "../../ce/sagas/analyticsSaga";

View File

@ -30,7 +30,6 @@ 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;
@ -106,19 +105,6 @@ export function* processTriggerHandler(message: any) {
if (messageType === MessageType.REQUEST)
yield call(evalWorker.respond, message.messageId, result);
}
export function* handleJSExecutionLog(data: TMessage<{ data: string[] }>) {
const {
body: { data: executedFns },
} = data;
for (const executedFn of executedFns) {
AnalyticsUtil.logEvent("EXECUTE_ACTION", {
type: "JS",
name: executedFn,
});
}
yield call(logJSFunctionExecution, data);
}
export function* handleEvalWorkerMessage(message: TMessage<any>) {
const { body } = message;
@ -145,7 +131,7 @@ export function* handleEvalWorkerMessage(message: TMessage<any>) {
break;
}
case MAIN_THREAD_ACTION.LOG_JS_FUNCTION_EXECUTION: {
yield call(handleJSExecutionLog, message);
yield call(logJSFunctionExecution, message);
break;
}
case MAIN_THREAD_ACTION.PROCESS_BATCHED_TRIGGERS: {

View File

@ -100,6 +100,7 @@ import { getAppsmithConfigs } from "@appsmith/configs";
import { executeJSUpdates } from "actions/pluginActionActions";
import { setEvaluatedActionSelectorField } from "actions/actionSelectorActions";
import { waitForWidgetConfigBuild } from "./InitSagas";
import { logDynamicTriggerExecution } from "@appsmith/sagas/analyticsSaga";
const APPSMITH_CONFIGS = getAppsmithConfigs();
@ -315,7 +316,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,
@ -331,6 +331,11 @@ export function* evaluateAndExecuteDynamicTrigger(
);
const { errors = [] } = response as any;
yield call(dynamicTriggerErrorHandler, errors);
yield fork(logDynamicTriggerExecution, {
dynamicTrigger,
errors,
triggerMeta,
});
return response;
}

View File

@ -515,6 +515,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,

View File

@ -1,7 +1,6 @@
import type {
ConfigTree,
DataTree,
AppsmithEntity,
DataTreeEntity,
} from "entities/DataTree/dataTreeFactory";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
@ -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,
@ -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");
}

View File

@ -11,6 +11,10 @@ import { get } from "lodash";
import { getType } from "utils/TypeHelpers";
import type { JSVarMutatedEvents } from "workers/Evaluation/types";
import { dataTreeEvaluator } from "workers/Evaluation/handlers/evalTree";
import type {
TriggerKind,
TriggerSource,
} from "constants/AppsmithActionConstants/ActionConstants";
const _internalSetTimeout = self.setTimeout;
const _internalClearTimeout = self.clearTimeout;
@ -182,15 +186,20 @@ TriggerEmitter.on(
jsVariableUpdatesHandlerWrapper,
);
export const fnInvokeLogHandler = priorityBatchedActionHandler<string>(
(data) => {
const set = new Set([...data]);
WorkerMessenger.ping({
method: MAIN_THREAD_ACTION.LOG_JS_FUNCTION_EXECUTION,
data: [...set],
});
},
);
export const fnInvokeLogHandler = deferredBatchedActionHandler<{
jsFnFullName: string;
isSuccess: boolean;
triggerMeta: {
source: TriggerSource;
triggerPropertyName: string | undefined;
triggerKind: TriggerKind | undefined;
};
}>((data) => {
WorkerMessenger.ping({
method: MAIN_THREAD_ACTION.LOG_JS_FUNCTION_EXECUTION,
data,
});
});
TriggerEmitter.on(BatchKey.process_batched_fn_invoke_log, fnInvokeLogHandler);

View File

@ -2,7 +2,6 @@ import { isPromise } from "workers/Evaluation/JSObject/utils";
import { postJSFunctionExecutionLog } from "@appsmith/workers/Evaluation/JSObject/postJSFunctionExecution";
import TriggerEmitter, { BatchKey } from "./TriggerEmitter";
import ExecutionMetaData from "./ExecutionMetaData";
import { TriggerKind } from "constants/AppsmithActionConstants/ActionConstants";
declare global {
interface Window {
@ -16,6 +15,7 @@ export type PostProcessorArg = {
executionMetaData: ReturnType<typeof ExecutionMetaData.getExecutionMetaData>;
jsFnFullName: string;
executionResponse: unknown;
isSuccess: boolean;
};
export type PostProcessor = (args: PostProcessorArg) => void;
@ -34,23 +34,13 @@ function saveExecutionData({
});
}
function logJSExecution({ executionMetaData, jsFnFullName }: PostProcessorArg) {
switch (executionMetaData.triggerMeta.triggerKind) {
case TriggerKind.EVENT_EXECUTION: {
TriggerEmitter.emit(BatchKey.process_batched_fn_invoke_log, jsFnFullName);
break;
}
default: {
break;
}
}
postJSFunctionExecutionLog(jsFnFullName);
}
export function jsObjectFunctionFactory<P extends ReadonlyArray<unknown>>(
fn: (...args: P) => unknown,
name: string,
postProcessors: PostProcessor[] = [saveExecutionData, logJSExecution],
postProcessors: PostProcessor[] = [
saveExecutionData,
postJSFunctionExecutionLog,
],
) {
return function (this: unknown, ...args: P) {
if (!ExecutionMetaData.getExecutionMetaData().enableJSFnPostProcessors) {
@ -66,6 +56,7 @@ export function jsObjectFunctionFactory<P extends ReadonlyArray<unknown>>(
executionMetaData,
jsFnFullName: name,
executionResponse: res,
isSuccess: true,
}),
);
return res;
@ -76,6 +67,7 @@ export function jsObjectFunctionFactory<P extends ReadonlyArray<unknown>>(
executionMetaData,
jsFnFullName: name,
executionResponse: undefined,
isSuccess: true,
}),
);
throw e;
@ -86,6 +78,7 @@ export function jsObjectFunctionFactory<P extends ReadonlyArray<unknown>>(
executionMetaData,
jsFnFullName: name,
executionResponse: result,
isSuccess: true,
}),
);
}
@ -96,6 +89,7 @@ export function jsObjectFunctionFactory<P extends ReadonlyArray<unknown>>(
executionMetaData,
jsFnFullName: name,
executionResponse: undefined,
isSuccess: false,
});
});
throw e;