diff --git a/app/client/src/ce/sagas/JSFunctionExecutionSaga.ts b/app/client/src/ce/sagas/JSFunctionExecutionSaga.ts index e1a3d1f139..0f2fb86747 100644 --- a/app/client/src/ce/sagas/JSFunctionExecutionSaga.ts +++ b/app/client/src/ce/sagas/JSFunctionExecutionSaga.ts @@ -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); } diff --git a/app/client/src/ce/sagas/analyticsSaga.ts b/app/client/src/ce/sagas/analyticsSaga.ts new file mode 100644 index 0000000000..2b7fcb94fb --- /dev/null +++ b/app/client/src/ce/sagas/analyticsSaga.ts @@ -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 = yield select(getAppMode); + const currentApp: ReturnType = yield select( + getCurrentApplication, + ); + const user: ReturnType = yield select(getCurrentUser); + const instanceId: ReturnType = yield select( + getInstanceId, + ); + 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: 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 | 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/ce/workers/Evaluation/JSObject/postJSFunctionExecution.ts b/app/client/src/ce/workers/Evaluation/JSObject/postJSFunctionExecution.ts index e1b7b61105..62af8de5f9 100644 --- a/app/client/src/ce/workers/Evaluation/JSObject/postJSFunctionExecution.ts +++ b/app/client/src/ce/workers/Evaluation/JSObject/postJSFunctionExecution.ts @@ -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, + }); } diff --git a/app/client/src/ee/sagas/analyticsSaga.ts b/app/client/src/ee/sagas/analyticsSaga.ts new file mode 100644 index 0000000000..b03dbf7e6b --- /dev/null +++ b/app/client/src/ee/sagas/analyticsSaga.ts @@ -0,0 +1 @@ +export * from "../../ce/sagas/analyticsSaga"; diff --git a/app/client/src/sagas/EvalWorkerActionSagas.ts b/app/client/src/sagas/EvalWorkerActionSagas.ts index e001e4337d..7b3ae0b01a 100644 --- a/app/client/src/sagas/EvalWorkerActionSagas.ts +++ b/app/client/src/sagas/EvalWorkerActionSagas.ts @@ -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) { const { body } = message; @@ -145,7 +131,7 @@ export function* handleEvalWorkerMessage(message: TMessage) { 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: { diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts index 038f42a853..a2042512c1 100644 --- a/app/client/src/sagas/EvaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -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 = 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; } diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index 16af507bb7..e337b785d8 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -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, diff --git a/app/client/src/workers/Evaluation/JSObject/utils.ts b/app/client/src/workers/Evaluation/JSObject/utils.ts index ee09b6e699..e931a1f711 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 d46c4a303c..4d673705dd 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,20 @@ 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) => { + WorkerMessenger.ping({ + method: MAIN_THREAD_ACTION.LOG_JS_FUNCTION_EXECUTION, + data, + }); +}); 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 6864d1f0b9..63e0f9d5f8 100644 --- a/app/client/src/workers/Evaluation/fns/utils/jsObjectFnFactory.ts +++ b/app/client/src/workers/Evaluation/fns/utils/jsObjectFnFactory.ts @@ -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; 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

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

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

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

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

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