diff --git a/app/client/src/sagas/EvalWorkerActionSagas.ts b/app/client/src/sagas/EvalWorkerActionSagas.ts index afd84422ec0b..2ecb025fcfd3 100644 --- a/app/client/src/sagas/EvalWorkerActionSagas.ts +++ b/app/client/src/sagas/EvalWorkerActionSagas.ts @@ -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"; @@ -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) { while (true) { @@ -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) { diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts index d04e61a9d5ae..a223158c0a81 100644 --- a/app/client/src/sagas/EvaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -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(); @@ -312,7 +313,6 @@ export function* evaluateAndExecuteDynamicTrigger( const unEvalTree: ReturnType = yield select( getUnevaluatedDataTree, ); - // const unEvalTree = unEvalAndConfigTree.unEvalTree; log.debug({ execute: dynamicTrigger }); const response: { errors: EvaluationError[]; result: unknown } = yield call( evalWorker.request, @@ -328,6 +328,11 @@ export function* evaluateAndExecuteDynamicTrigger( ); const { errors = [] } = response as any; yield call(dynamicTriggerErrorHandler, errors); + yield fork(logDynamicTriggerExecution, { + dynamicTrigger, + errors, + triggerMeta, + }); return response; } diff --git a/app/client/src/sagas/analyticsSaga.ts b/app/client/src/sagas/analyticsSaga.ts new file mode 100644 index 000000000000..8bd4a710f485 --- /dev/null +++ b/app/client/src/sagas/analyticsSaga.ts @@ -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 = yield select(getAppMode); + const currentApp: ReturnType = yield select( + getCurrentApplication, + ); + const user: ReturnType = yield select(getCurrentUser); + const instanceId: ReturnType = yield select( + getInstanceId, + ); + const { cloudHosting } = getAppsmithConfigs(); + const source = cloudHosting ? "cloud" : "ce"; + const pageId: ReturnType = 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 | 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 = yield select( + (state: AppState) => + getJSActionFromName(state, JSObjectName, functionName), + ); + const triggeredWidget: ReturnType | 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, + }, + ); + } +} diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index 122822646e1c..3c6271267e0b 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -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, diff --git a/app/client/src/workers/Evaluation/JSObject/utils.ts b/app/client/src/workers/Evaluation/JSObject/utils.ts index ee09b6e69961..e931a1f7111a 100644 --- a/app/client/src/workers/Evaluation/JSObject/utils.ts +++ b/app/client/src/workers/Evaluation/JSObject/utils.ts @@ -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 { return Boolean(value && typeof value.then === "function"); } diff --git a/app/client/src/workers/Evaluation/fns/utils/TriggerEmitter.ts b/app/client/src/workers/Evaluation/fns/utils/TriggerEmitter.ts index d46c4a303c5f..6caf5a211bbe 100644 --- a/app/client/src/workers/Evaluation/fns/utils/TriggerEmitter.ts +++ b/app/client/src/workers/Evaluation/fns/utils/TriggerEmitter.ts @@ -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,21 @@ TriggerEmitter.on( jsVariableUpdatesHandlerWrapper, ); -export const fnInvokeLogHandler = priorityBatchedActionHandler( - (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) => { + const set = new Set([...data]); + WorkerMessenger.ping({ + method: MAIN_THREAD_ACTION.LOG_JS_FUNCTION_EXECUTION, + data: [...set], + }); +}); TriggerEmitter.on(BatchKey.process_batched_fn_invoke_log, fnInvokeLogHandler); diff --git a/app/client/src/workers/Evaluation/fns/utils/jsObjectFnFactory.ts b/app/client/src/workers/Evaluation/fns/utils/jsObjectFnFactory.ts index 6864d1f0b952..9f73b22662bc 100644 --- a/app/client/src/workers/Evaluation/fns/utils/jsObjectFnFactory.ts +++ b/app/client/src/workers/Evaluation/fns/utils/jsObjectFnFactory.ts @@ -16,6 +16,7 @@ export type PostProcessorArg = { executionMetaData: ReturnType; jsFnFullName: string; executionResponse: unknown; + isSuccess: boolean; }; export type PostProcessor = (args: PostProcessorArg) => void; @@ -34,10 +35,18 @@ function saveExecutionData({ }); } -function logJSExecution({ executionMetaData, jsFnFullName }: PostProcessorArg) { +function logJSExecution({ + executionMetaData, + isSuccess, + jsFnFullName, +}: PostProcessorArg) { switch (executionMetaData.triggerMeta.triggerKind) { case TriggerKind.EVENT_EXECUTION: { - TriggerEmitter.emit(BatchKey.process_batched_fn_invoke_log, jsFnFullName); + TriggerEmitter.emit(BatchKey.process_batched_fn_invoke_log, { + jsFnFullName, + isSuccess, + triggerMeta: executionMetaData.triggerMeta, + }); break; } default: { @@ -66,6 +75,7 @@ export function jsObjectFunctionFactory

>( executionMetaData, jsFnFullName: name, executionResponse: res, + isSuccess: true, }), ); return res; @@ -76,6 +86,7 @@ export function jsObjectFunctionFactory

>( executionMetaData, jsFnFullName: name, executionResponse: undefined, + isSuccess: true, }), ); throw e; @@ -86,6 +97,7 @@ export function jsObjectFunctionFactory

>( executionMetaData, jsFnFullName: name, executionResponse: result, + isSuccess: true, }), ); } @@ -96,6 +108,7 @@ export function jsObjectFunctionFactory

>( executionMetaData, jsFnFullName: name, executionResponse: undefined, + isSuccess: false, }); }); throw e;