From 071b9927107e451dcac4afb821e6c7447239aaa2 Mon Sep 17 00:00:00 2001 From: Anand Srinivasan <66776129+eco-monk@users.noreply.github.com> Date: Fri, 6 Jan 2023 19:39:38 +0530 Subject: [PATCH] chore: send segment anonymous id (#19122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add segment's `anonymousId` as a header in all API calls. cached id -> [details](https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/identity/#segment-id-persistence) On Page load actions: - If segment is enabled: - and cached id exists -> trigger with cached id - if cached id doesn’t exist, we wait for max 2 seconds. - if segment init is success -> trigger with anonymous id - if failed/delayed -> trigger without anonymous id - If segment is disabled we don’t wait at all and anonymous id is not sent. Signed-off-by: Shrikant Sharat Kandula Co-authored-by: Shrikant Sharat Kandula Co-authored-by: Hetu Nandu --- app/client/src/actions/analyticsActions.ts | 9 ++ app/client/src/api/ApiUtils.ts | 9 ++ .../src/ce/constants/ReduxActionConstants.tsx | 2 + app/client/src/ce/reducers/index.tsx | 2 + app/client/src/ce/sagas/userSagas.tsx | 38 +++++- .../src/entities/Engine/AppEditorEngine.ts | 3 + .../src/entities/Engine/AppViewerEngine.ts | 2 + .../reducers/uiReducers/analyticsReducer.ts | 37 ++++++ app/client/src/reducers/uiReducers/index.tsx | 2 + .../src/sagas/__tests__/initSagas.test.ts | 5 + .../src/selectors/analyticsSelectors.tsx | 4 + app/client/src/utils/AnalyticsUtil.tsx | 119 ++++++++++-------- app/client/src/utils/AppsmithUtils.tsx | 4 +- .../server/helpers/ExchangeUtils.java | 27 ++++ .../appsmith/server/helpers/GitFileUtils.java | 12 +- .../services/ce/AnalyticsServiceCE.java | 4 +- .../services/ce/AnalyticsServiceCEImpl.java | 48 ++++--- .../server/services/ce/GitServiceCEImpl.java | 9 +- .../services/ce/MockDataServiceCEImpl.java | 21 ++-- .../ce/CreateDBTablePageSolutionCEImpl.java | 10 +- .../ImportExportApplicationServiceCEImpl.java | 12 +- .../server/solutions/ce/UserSignupCEImpl.java | 7 +- 22 files changed, 276 insertions(+), 110 deletions(-) create mode 100644 app/client/src/actions/analyticsActions.ts create mode 100644 app/client/src/reducers/uiReducers/analyticsReducer.ts create mode 100644 app/client/src/selectors/analyticsSelectors.tsx create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ExchangeUtils.java diff --git a/app/client/src/actions/analyticsActions.ts b/app/client/src/actions/analyticsActions.ts new file mode 100644 index 0000000000..20c59b1b08 --- /dev/null +++ b/app/client/src/actions/analyticsActions.ts @@ -0,0 +1,9 @@ +import { ReduxActionTypes } from "ce/constants/ReduxActionConstants"; + +export const segmentInitSuccess = () => ({ + type: ReduxActionTypes.SEGMENT_INITIALIZED, +}); + +export const segmentInitUncertain = () => ({ + type: ReduxActionTypes.SEGMENT_INIT_UNCERTAIN, +}); diff --git a/app/client/src/api/ApiUtils.ts b/app/client/src/api/ApiUtils.ts index 4d410ec531..3aefec9f79 100644 --- a/app/client/src/api/ApiUtils.ts +++ b/app/client/src/api/ApiUtils.ts @@ -18,10 +18,13 @@ import { AUTH_LOGIN_URL } from "constants/routes"; import { getCurrentGitBranch } from "selectors/gitSyncSelectors"; import getQueryParamsObject from "utils/getQueryParamsObject"; import { UserCancelledActionExecutionError } from "sagas/ActionExecution/errorUtils"; +import AnalyticsUtil from "utils/AnalyticsUtil"; +import { getAppsmithConfigs } from "ce/configs"; const executeActionRegex = /actions\/execute/; const timeoutErrorRegex = /timeout of (\d+)ms exceeded/; export const axiosConnectionAbortedCode = "ECONNABORTED"; +const appsmithConfig = getAppsmithConfigs(); const makeExecuteActionResponse = (response: any): ActionExecutionResponse => ({ ...response.data, @@ -40,6 +43,7 @@ const is404orAuthPath = () => { // this will be used to calculate the time taken for an action // execution request export const apiRequestInterceptor = (config: AxiosRequestConfig) => { + config.headers = config.headers ?? {}; const branch = getCurrentGitBranch(store.getState()) || getQueryParamsObject().branch; if (branch && config.headers) { @@ -49,6 +53,11 @@ export const apiRequestInterceptor = (config: AxiosRequestConfig) => { config.timeout = 1000 * 120; // increase timeout for git specific APIs } + const anonymousId = AnalyticsUtil.getAnonymousId(); + appsmithConfig.segment.enabled && + anonymousId && + (config.headers["x-anonymous-user-id"] = anonymousId); + return { ...config, timer: performance.now() }; }; diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index e16881ac51..560a6c0177 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -757,6 +757,8 @@ export const ReduxActionTypes = { "SET_DATASOURCE_DEFAULT_KEY_VALUE_PAIR_SET", RESET_DATASOURCE_DEFAULT_KEY_VALUE_PAIR_SET: "RESET_DATASOURCE_DEFAULT_KEY_VALUE_PAIR_SET", + SEGMENT_INITIALIZED: "SEGMENT_INITIALIZED", + SEGMENT_INIT_UNCERTAIN: "SEGMENT_INIT_UNCERTAIN", }; export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes]; diff --git a/app/client/src/ce/reducers/index.tsx b/app/client/src/ce/reducers/index.tsx index af76fd8caa..3797b5a798 100644 --- a/app/client/src/ce/reducers/index.tsx +++ b/app/client/src/ce/reducers/index.tsx @@ -73,6 +73,7 @@ import { CanvasLevelsReduxState } from "reducers/entityReducers/autoHeightReduce import { LintErrors } from "reducers/lintingReducers/lintErrorsReducers"; import lintErrorReducer from "reducers/lintingReducers"; import { AutoHeightUIState } from "reducers/uiReducers/autoHeightReducer"; +import { AnalyticsReduxState } from "reducers/uiReducers/analyticsReducer"; export const reducerObject = { entities: entityReducer, @@ -86,6 +87,7 @@ export const reducerObject = { export interface AppState { ui: { + analytics: AnalyticsReduxState; editor: EditorReduxState; propertyPane: PropertyPaneReduxState; tableFilterPane: TableFilterPaneReduxState; diff --git a/app/client/src/ce/sagas/userSagas.tsx b/app/client/src/ce/sagas/userSagas.tsx index 1161594d04..40202b6114 100644 --- a/app/client/src/ce/sagas/userSagas.tsx +++ b/app/client/src/ce/sagas/userSagas.tsx @@ -1,4 +1,4 @@ -import { call, put, select, take } from "redux-saga/effects"; +import { call, put, race, select, take } from "redux-saga/effects"; import { ReduxAction, ReduxActionWithPromise, @@ -56,6 +56,13 @@ import { getFirstTimeUserOnboardingIntroModalVisibility, } from "utils/storage"; import { initializeAnalyticsAndTrackers } from "utils/AppsmithUtils"; +import { getAppsmithConfigs } from "ce/configs"; +import { getSegmentState } from "selectors/analyticsSelectors"; +import { + segmentInitUncertain, + segmentInitSuccess, +} from "actions/analyticsActions"; +import { SegmentState } from "reducers/uiReducers/analyticsReducer"; export function* createUserSaga( action: ReduxActionWithPromise, @@ -96,6 +103,25 @@ export function* createUserSaga( } } +export function* waitForSegmentInit(skipWithAnonymousId: boolean) { + if (skipWithAnonymousId && AnalyticsUtil.getAnonymousId()) return; + yield call(waitForFetchUserSuccess); + const currentUser: User | undefined = yield select(getCurrentUser); + const segmentState: SegmentState | undefined = yield select(getSegmentState); + const appsmithConfig = getAppsmithConfigs(); + + if ( + currentUser?.enableTelemetry && + appsmithConfig.segment.enabled && + !segmentState + ) { + yield race([ + take(ReduxActionTypes.SEGMENT_INITIALIZED), + take(ReduxActionTypes.SEGMENT_INIT_UNCERTAIN), + ]); + } +} + export function* getCurrentUserSaga() { try { PerformanceTracker.startAsyncTracking( @@ -108,7 +134,15 @@ export function* getCurrentUserSaga() { //@ts-expect-error: response is of type unknown const { enableTelemetry } = response.data; if (enableTelemetry) { - initializeAnalyticsAndTrackers(); + const promise = initializeAnalyticsAndTrackers(); + if (promise instanceof Promise) { + const result: boolean = yield promise; + if (result) { + yield put(segmentInitSuccess()); + } else { + yield put(segmentInitUncertain()); + } + } } yield put(initAppLevelSocketConnection()); yield put(initPageLevelSocketConnection()); diff --git a/app/client/src/entities/Engine/AppEditorEngine.ts b/app/client/src/entities/Engine/AppEditorEngine.ts index f0232639e0..e82fd0284b 100644 --- a/app/client/src/entities/Engine/AppEditorEngine.ts +++ b/app/client/src/entities/Engine/AppEditorEngine.ts @@ -51,6 +51,7 @@ import { fetchJSLibraries } from "actions/JSLibraryActions"; import CodemirrorTernService from "utils/autocomplete/CodemirrorTernService"; import { selectFeatureFlags } from "selectors/usersSelectors"; import FeatureFlags from "entities/FeatureFlags"; +import { waitForSegmentInit } from "ce/sagas/userSagas"; export default class AppEditorEngine extends AppEngine { constructor(mode: APP_MODE) { @@ -135,6 +136,8 @@ export default class AppEditorEngine extends AppEngine { throw new ActionsNotFoundError( `Unable to fetch actions for the application: ${applicationId}`, ); + + yield call(waitForSegmentInit, true); yield put(fetchAllPageEntityCompletion([executePageLoadActions()])); } diff --git a/app/client/src/entities/Engine/AppViewerEngine.ts b/app/client/src/entities/Engine/AppViewerEngine.ts index 6f729c84a5..af9ec3be2f 100644 --- a/app/client/src/entities/Engine/AppViewerEngine.ts +++ b/app/client/src/entities/Engine/AppViewerEngine.ts @@ -26,6 +26,7 @@ import AppEngine, { ActionsNotFoundError, AppEnginePayload } from "."; import { fetchJSLibraries } from "actions/JSLibraryActions"; import FeatureFlags from "entities/FeatureFlags"; import { selectFeatureFlags } from "selectors/usersSelectors"; +import { waitForSegmentInit } from "ce/sagas/userSagas"; export default class AppViewerEngine extends AppEngine { constructor(mode: APP_MODE) { @@ -113,6 +114,7 @@ export default class AppViewerEngine extends AppEngine { `Unable to fetch actions for the application: ${applicationId}`, ); + yield call(waitForSegmentInit, true); yield put(fetchAllPageEntityCompletion([executePageLoadActions()])); } } diff --git a/app/client/src/reducers/uiReducers/analyticsReducer.ts b/app/client/src/reducers/uiReducers/analyticsReducer.ts new file mode 100644 index 0000000000..622af145d7 --- /dev/null +++ b/app/client/src/reducers/uiReducers/analyticsReducer.ts @@ -0,0 +1,37 @@ +import { ReduxActionTypes } from "ce/constants/ReduxActionConstants"; +import { createReducer } from "utils/ReducerUtils"; + +export type SegmentState = "INIT_SUCCESS" | "INIT_UNCERTAIN"; + +export const initialState: AnalyticsReduxState = { + telemetry: {}, +}; + +export interface AnalyticsReduxState { + telemetry: { + segmentState?: SegmentState; + }; +} + +export const handlers = { + [ReduxActionTypes.SEGMENT_INITIALIZED]: ( + state: AnalyticsReduxState, + ): AnalyticsReduxState => ({ + ...state, + telemetry: { + ...state.telemetry, + segmentState: "INIT_SUCCESS", + }, + }), + [ReduxActionTypes.SEGMENT_INIT_UNCERTAIN]: ( + state: AnalyticsReduxState, + ): AnalyticsReduxState => ({ + ...state, + telemetry: { + ...state.telemetry, + segmentState: "INIT_UNCERTAIN", + }, + }), +}; + +export default createReducer(initialState, handlers); diff --git a/app/client/src/reducers/uiReducers/index.tsx b/app/client/src/reducers/uiReducers/index.tsx index 62e73083eb..83fe865099 100644 --- a/app/client/src/reducers/uiReducers/index.tsx +++ b/app/client/src/reducers/uiReducers/index.tsx @@ -45,8 +45,10 @@ import guidedTourReducer from "./guidedTourReducer"; import libraryReducer from "./libraryReducer"; import appSettingsPaneReducer from "./appSettingsPaneReducer"; import autoHeightUIReducer from "./autoHeightReducer"; +import analyticsReducer from "./analyticsReducer"; const uiReducer = combineReducers({ + analytics: analyticsReducer, editor: editorReducer, errors: errorReducer, propertyPane: propertyPaneReducer, diff --git a/app/client/src/sagas/__tests__/initSagas.test.ts b/app/client/src/sagas/__tests__/initSagas.test.ts index 90714cf1d4..bcb95fb537 100644 --- a/app/client/src/sagas/__tests__/initSagas.test.ts +++ b/app/client/src/sagas/__tests__/initSagas.test.ts @@ -5,6 +5,11 @@ import AppEngineFactory from "entities/Engine/factory"; import { call } from "redux-saga/effects"; import { startAppEngine } from "sagas/InitSagas"; +jest.mock("../../api/Api", () => ({ + __esModule: true, + default: class Api {}, +})); + describe("tests the sagas in initSagas", () => { it("tests the order of execute in startAppEngine", () => { const action = { diff --git a/app/client/src/selectors/analyticsSelectors.tsx b/app/client/src/selectors/analyticsSelectors.tsx new file mode 100644 index 0000000000..ca5e0ec6ba --- /dev/null +++ b/app/client/src/selectors/analyticsSelectors.tsx @@ -0,0 +1,4 @@ +import { AppState } from "ce/reducers"; + +export const getSegmentState = (state: AppState) => + state.ui.analytics.telemetry.segmentState; diff --git a/app/client/src/utils/AnalyticsUtil.tsx b/app/client/src/utils/AnalyticsUtil.tsx index d125e3ede9..2008468ec6 100644 --- a/app/client/src/utils/AnalyticsUtil.tsx +++ b/app/client/src/utils/AnalyticsUtil.tsx @@ -323,59 +323,68 @@ class AnalyticsUtil { } static initializeSegment(key: string) { - (function init(window: any) { - const analytics = (window.analytics = window.analytics || []); - if (!analytics.initialize) { - if (analytics.invoked) { - log.error("Segment snippet included twice."); - } else { - analytics.invoked = !0; - analytics.methods = [ - "trackSubmit", - "trackClick", - "trackLink", - "trackForm", - "pageview", - "identify", - "reset", - "group", - "track", - "ready", - "alias", - "debug", - "page", - "once", - "off", - "on", - ]; - analytics.factory = function(t: any) { - return function() { - const e = Array.prototype.slice.call(arguments); //eslint-disable-line prefer-rest-params - e.unshift(t); - analytics.push(e); - return analytics; + const initPromise = new Promise((resolve) => { + (function init(window: any) { + const analytics = (window.analytics = window.analytics || []); + if (!analytics.initialize) { + if (analytics.invoked) { + log.error("Segment snippet included twice."); + } else { + analytics.invoked = !0; + analytics.methods = [ + "trackSubmit", + "trackClick", + "trackLink", + "trackForm", + "pageview", + "identify", + "reset", + "group", + "track", + "ready", + "alias", + "debug", + "page", + "once", + "off", + "on", + ]; + analytics.factory = function(t: any) { + return function() { + const e = Array.prototype.slice.call(arguments); //eslint-disable-line prefer-rest-params + e.unshift(t); + analytics.push(e); + return analytics; + }; }; + } + for (let t: any = 0; t < analytics.methods.length; t++) { + const e = analytics.methods[t]; + analytics[e] = analytics.factory(e); + } + analytics.load = function(t: any, e: any) { + const n = document.createElement("script"); + n.type = "text/javascript"; + n.async = !0; + // Ref: https://www.notion.so/appsmith/530051a2083040b5bcec15a46121aea3 + n.src = "https://a.appsmith.com/reroute/" + t + "/main.js"; + const a: any = document.getElementsByTagName("script")[0]; + a.parentNode.insertBefore(n, a); + analytics._loadOptions = e; }; + analytics.ready(() => { + resolve(true); + }); + setTimeout(() => { + resolve(false); + }, 2000); + analytics.SNIPPET_VERSION = "4.1.0"; + analytics.load(key); + analytics.page(); } - for (let t: any = 0; t < analytics.methods.length; t++) { - const e = analytics.methods[t]; - analytics[e] = analytics.factory(e); - } - analytics.load = function(t: any, e: any) { - const n = document.createElement("script"); - n.type = "text/javascript"; - n.async = !0; - // Ref: https://www.notion.so/appsmith/530051a2083040b5bcec15a46121aea3 - n.src = "https://a.appsmith.com/reroute/" + t + "/main.js"; - const a: any = document.getElementsByTagName("script")[0]; - a.parentNode.insertBefore(n, a); - analytics._loadOptions = e; - }; - analytics.SNIPPET_VERSION = "4.1.0"; - analytics.load(key); - analytics.page(); - } - })(window); + })(window); + }); + return initPromise; } static logEvent(eventName: EventName, eventData: any = {}) { @@ -478,6 +487,16 @@ class AnalyticsUtil { } } + static getAnonymousId() { + const windowDoc: any = window; + const { segment } = getAppsmithConfigs(); + if (windowDoc.analytics && windowDoc.analytics.user) { + return windowDoc.analytics.user().anonymousId(); + } else if (segment.enabled) { + return localStorage.getItem("ajs_anonymous_id")?.replaceAll('"', ""); + } + } + static reset() { const windowDoc: any = window; if (windowDoc.Intercom) { diff --git a/app/client/src/utils/AppsmithUtils.tsx b/app/client/src/utils/AppsmithUtils.tsx index 935eb8064f..abe7de71ca 100644 --- a/app/client/src/utils/AppsmithUtils.tsx +++ b/app/client/src/utils/AppsmithUtils.tsx @@ -72,10 +72,10 @@ export const initializeAnalyticsAndTrackers = () => { if (appsmithConfigs.segment.enabled && !(window as any).analytics) { if (appsmithConfigs.segment.apiKey) { // This value is only enabled for Appsmith's cloud hosted version. It is not set in self-hosted environments - AnalyticsUtil.initializeSegment(appsmithConfigs.segment.apiKey); + return AnalyticsUtil.initializeSegment(appsmithConfigs.segment.apiKey); } else if (appsmithConfigs.segment.ceKey) { // This value is set in self-hosted environments. But if the analytics are disabled, it's never used. - AnalyticsUtil.initializeSegment(appsmithConfigs.segment.ceKey); + return AnalyticsUtil.initializeSegment(appsmithConfigs.segment.ceKey); } } } catch (e) { diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ExchangeUtils.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ExchangeUtils.java new file mode 100644 index 0000000000..c0f6771c6a --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ExchangeUtils.java @@ -0,0 +1,27 @@ +package com.appsmith.server.helpers; + +import com.appsmith.server.constants.FieldName; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +public class ExchangeUtils { + + public static final String HEADER_ANONYMOUS_USER_ID = "X-Anonymous-User-Id"; + + /** + * Returns the value of `X-Anonymous-User-Id` header, from the _current_ request. Since this gets the header from + * the current request, it has to be called from a request context. It won't work in new background contexts, like + * when calling `.subscribe()` on a Mono. + * @return a Mono that resolves to the value of the `X-Anonymous-User-Id` header, if present. Else, `FieldName.ANONYMOUS_USER`. + */ + public static Mono getAnonymousUserIdFromCurrentRequest() { + return Mono.deferContextual(Mono::just) + .map(contextView -> ObjectUtils.defaultIfNull( + contextView.get(ServerWebExchange.class).getRequest().getHeaders().getFirst(HEADER_ANONYMOUS_USER_ID), + FieldName.ANONYMOUS_USER + )) + .defaultIfEmpty(FieldName.ANONYMOUS_USER); + } + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/GitFileUtils.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/GitFileUtils.java index c04370e9db..c0ec0fe6d0 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/GitFileUtils.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/GitFileUtils.java @@ -94,7 +94,7 @@ public class GitFileUtils { try { Mono repoPathMono = fileUtils.saveApplicationToGitRepo(baseRepoSuffix, applicationReference, branchName).cache(); return Mono.zip(repoPathMono, sessionUserService.getCurrentUser()) - .map(tuple -> { + .flatMap(tuple -> { stopwatch.stopTimer(); Path repoPath = tuple.getT1(); // Path to repo will be : ./container-volumes/git-repo/workspaceId/defaultApplicationId/repoName/ @@ -104,8 +104,8 @@ public class GitFileUtils { FieldName.FLOW_NAME, stopwatch.getFlow(), "executionTime", stopwatch.getExecutionTime() ); - analyticsService.sendEvent(AnalyticsEvents.UNIT_EXECUTION_TIME.getEventName(), tuple.getT2().getUsername(), data); - return repoPath; + return analyticsService.sendEvent(AnalyticsEvents.UNIT_EXECUTION_TIME.getEventName(), tuple.getT2().getUsername(), data) + .thenReturn(repoPath); }); } catch (IOException | GitAPIException e) { log.error("Error occurred while saving files to local git repo: ", e); @@ -239,7 +239,7 @@ public class GitFileUtils { Mono appReferenceMono = fileUtils .reconstructApplicationReferenceFromGitRepo(workspaceId, defaultApplicationId, repoName, branchName); return Mono.zip(appReferenceMono, sessionUserService.getCurrentUser()) - .map(tuple -> { + .flatMap(tuple -> { ApplicationGitReference applicationReference = tuple.getT1(); // Extract application metadata from the json ApplicationJson metadata = getApplicationResource(applicationReference.getMetadata(), ApplicationJson.class); @@ -252,8 +252,8 @@ public class GitFileUtils { FieldName.FLOW_NAME, stopwatch.getFlow(), "executionTime", stopwatch.getExecutionTime() ); - analyticsService.sendEvent(AnalyticsEvents.UNIT_EXECUTION_TIME.getEventName(), tuple.getT2().getUsername(), data); - return applicationJson; + return analyticsService.sendEvent(AnalyticsEvents.UNIT_EXECUTION_TIME.getEventName(), tuple.getT2().getUsername(), data) + .thenReturn(applicationJson); }); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCE.java index 7150f37fec..a9de5ccf3d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCE.java @@ -16,9 +16,9 @@ public interface AnalyticsServiceCE { void identifyInstance(String instanceId, String role, String useCase); - void sendEvent(String event, String userId, Map properties); + Mono sendEvent(String event, String userId, Map properties); - void sendEvent(String event, String userId, Map properties, boolean hashUserId); + Mono sendEvent(String event, String userId, Map properties, boolean hashUserId); Mono sendObjectEvent(AnalyticsEvents event, T object, Map extraProperties); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java index 94ba848212..a55f5e83ee 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java @@ -8,6 +8,7 @@ import com.appsmith.server.domains.NewAction; import com.appsmith.server.domains.NewPage; import com.appsmith.server.domains.User; import com.appsmith.server.domains.UserData; +import com.appsmith.server.helpers.ExchangeUtils; import com.appsmith.server.helpers.PolicyUtils; import com.appsmith.server.helpers.UserUtils; import com.appsmith.server.services.ConfigService; @@ -19,9 +20,9 @@ import com.segment.analytics.messages.TrackMessage; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; import java.util.HashMap; import java.util.List; @@ -118,14 +119,14 @@ public class AnalyticsServiceCEImpl implements AnalyticsServiceCE { } @Override - public void sendEvent(String event, String userId, Map properties) { - sendEvent(event, userId, properties, true); + public Mono sendEvent(String event, String userId, Map properties) { + return sendEvent(event, userId, properties, true); } @Override - public void sendEvent(String event, String userId, Map properties, boolean hashUserId) { + public Mono sendEvent(String event, String userId, Map properties, boolean hashUserId) { if (!isActive()) { - return; + return Mono.empty(); } // Can't update the properties directly as it's throwing ImmutableCollection error @@ -158,17 +159,26 @@ public class AnalyticsServiceCEImpl implements AnalyticsServiceCE { } final String finalUserId = userId; - configService.getInstanceId() - .map(instanceId -> { - TrackMessage.Builder messageBuilder = TrackMessage.builder(event).userId(finalUserId); + + return Mono.zip( + ExchangeUtils.getAnonymousUserIdFromCurrentRequest(), + configService.getInstanceId() + .defaultIfEmpty("unknown-instance-id") + ).map(tuple -> { + final String userIdFromClient = tuple.getT1(); + final String instanceId = tuple.getT2(); + String userIdToSend = finalUserId; + if (FieldName.ANONYMOUS_USER.equals(finalUserId)) { + userIdToSend = StringUtils.defaultIfEmpty(userIdFromClient, FieldName.ANONYMOUS_USER); + } + TrackMessage.Builder messageBuilder = TrackMessage.builder(event).userId(userIdToSend); analyticsProperties.put("originService", "appsmith-server"); analyticsProperties.put("instanceId", instanceId); messageBuilder = messageBuilder.properties(analyticsProperties); analytics.enqueue(messageBuilder); return instanceId; }) - .subscribeOn(Schedulers.boundedElastic()) - .subscribe(); + .then(); } @Override @@ -197,7 +207,15 @@ public class AnalyticsServiceCEImpl implements AnalyticsServiceCE { .switchIfEmpty(Mono.just(anonymousUser)); return userMono - .map(user -> { + .flatMap(user -> Mono.zip( + user.isAnonymous() + ? ExchangeUtils.getAnonymousUserIdFromCurrentRequest() + : Mono.just(user.getUsername()), + Mono.just(user) + )) + .flatMap(tuple -> { + final String id = tuple.getT1(); + final User user = tuple.getT2(); // In case the user is anonymous, don't raise an event, unless it's a signup, logout, page view or action execution event. boolean isEventUserSignUpOrLogout = object instanceof User && (event == AnalyticsEvents.CREATE || event == AnalyticsEvents.LOGOUT); @@ -205,13 +223,13 @@ public class AnalyticsServiceCEImpl implements AnalyticsServiceCE { boolean isEventActionExecution = object instanceof NewAction && event == AnalyticsEvents.EXECUTE_ACTION; boolean isAvoidLoggingEvent = user.isAnonymous() && !(isEventUserSignUpOrLogout || isEventPageView || isEventActionExecution); if (isAvoidLoggingEvent) { - return object; + return Mono.just(object); } final String username = (object instanceof User ? (User) object : user).getUsername(); HashMap analyticsProperties = new HashMap<>(); - analyticsProperties.put("id", username); + analyticsProperties.put("id", id); analyticsProperties.put("oid", object.getId()); if (extraProperties != null) { analyticsProperties.putAll(extraProperties); @@ -219,8 +237,8 @@ public class AnalyticsServiceCEImpl implements AnalyticsServiceCE { analyticsProperties.remove(FieldName.EVENT_DATA); } - sendEvent(eventTag, username, analyticsProperties); - return object; + return sendEvent(eventTag, username, analyticsProperties) + .thenReturn(object); }); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/GitServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/GitServiceCEImpl.java index 99f12ec312..b5f034a1e0 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/GitServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/GitServiceCEImpl.java @@ -18,7 +18,6 @@ import com.appsmith.server.constants.GitDefaultCommitMessage; import com.appsmith.server.constants.SerialiseApplicationObjective; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.ApplicationMode; -import com.appsmith.server.dtos.ApplicationJson; import com.appsmith.server.domains.GitApplicationMetadata; import com.appsmith.server.domains.GitAuth; import com.appsmith.server.domains.GitDeployKeys; @@ -27,6 +26,7 @@ import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.UserData; import com.appsmith.server.domains.Workspace; import com.appsmith.server.dtos.ApplicationImportDTO; +import com.appsmith.server.dtos.ApplicationJson; import com.appsmith.server.dtos.GitCommitDTO; import com.appsmith.server.dtos.GitConnectDTO; import com.appsmith.server.dtos.GitDocsDTO; @@ -93,8 +93,6 @@ import static com.appsmith.external.constants.GitConstants.EMPTY_COMMIT_ERROR_ME import static com.appsmith.external.constants.GitConstants.GIT_CONFIG_ERROR; import static com.appsmith.external.constants.GitConstants.GIT_PROFILE_ERROR; import static com.appsmith.external.constants.GitConstants.MERGE_CONFLICT_BRANCH_NAME; -import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; -import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; import static com.appsmith.server.constants.CommentConstants.APPSMITH_BOT_USERNAME; import static com.appsmith.server.constants.FieldName.DEFAULT; import static com.appsmith.server.helpers.DefaultResourcesUtils.createDefaultIdsOrUpdateWithGivenResourceIds; @@ -2455,10 +2453,7 @@ public class GitServiceCEImpl implements GitServiceCE { ); analyticsProps.put(FieldName.EVENT_DATA, eventData); return sessionUserService.getCurrentUser() - .map(user -> { - analyticsService.sendEvent(eventName, user.getUsername(), analyticsProps); - return application; - }); + .flatMap(user -> analyticsService.sendEvent(eventName, user.getUsername(), analyticsProps).thenReturn(application)); } @Override diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/MockDataServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/MockDataServiceCEImpl.java index bd9be7eb7a..a0ed6ba5df 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/MockDataServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/MockDataServiceCEImpl.java @@ -237,17 +237,16 @@ public class MockDataServiceCEImpl implements MockDataServiceCE { } return sessionUserService.getCurrentUser() - .map(user -> { - analyticsService.sendEvent( - AnalyticsEvents.CREATE.getEventName(), - user.getUsername(), - Map.of( - "MockDataSource", defaultIfNull(name, ""), - "orgId", defaultIfNull(workspaceId, "") - ) - ); - return user; - }); + .flatMap(user -> + analyticsService.sendEvent( + AnalyticsEvents.CREATE.getEventName(), + user.getUsername(), + Map.of( + "MockDataSource", defaultIfNull(name, ""), + "orgId", defaultIfNull(workspaceId, "") + ) + ).thenReturn(user) + ); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/CreateDBTablePageSolutionCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/CreateDBTablePageSolutionCEImpl.java index 3717b52888..5286394d6f 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/CreateDBTablePageSolutionCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/CreateDBTablePageSolutionCEImpl.java @@ -1059,7 +1059,7 @@ public class CreateDBTablePageSolutionCEImpl implements CreateDBTablePageSolutio private Mono sendGenerateCRUDPageAnalyticsEvent(CRUDPageResponseDTO crudPage, Datasource datasource, String pluginName) { PageDTO page = crudPage.getPage(); return sessionUserService.getCurrentUser() - .map(currentUser -> { + .flatMap(currentUser -> { try { final Map data = Map.of( "applicationId", page.getApplicationId(), @@ -1069,13 +1069,13 @@ public class CreateDBTablePageSolutionCEImpl implements CreateDBTablePageSolutio "datasourceId", datasource.getId(), "organizationId", datasource.getWorkspaceId() ); - analyticsService.sendEvent(AnalyticsEvents.GENERATE_CRUD_PAGE.getEventName(), currentUser.getUsername(), data); + return analyticsService.sendEvent(AnalyticsEvents.GENERATE_CRUD_PAGE.getEventName(), currentUser.getUsername(), data) + .thenReturn(crudPage); } catch (Exception e) { log.warn("Error sending generate CRUD DB table page data point", e); } - return crudPage; + return Mono.just(crudPage); }); } - -} \ No newline at end of file +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java index 12dbb15fd9..58d91e609d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java @@ -480,7 +480,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica }); }) .then(currentUserMono) - .map(user -> { + .flatMap(user -> { stopwatch.stopTimer(); final Map data = Map.of( FieldName.APPLICATION_ID, applicationId, @@ -490,8 +490,8 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica FieldName.FLOW_NAME, stopwatch.getFlow(), "executionTime", stopwatch.getExecutionTime() ); - analyticsService.sendEvent(AnalyticsEvents.UNIT_EXECUTION_TIME.getEventName(), user.getUsername(), data); - return applicationJson; + return analyticsService.sendEvent(AnalyticsEvents.UNIT_EXECUTION_TIME.getEventName(), user.getUsername(), data) + .thenReturn(applicationJson); }) .then(allCustomJSLibListMono) .map(allCustomLibList -> { @@ -1182,7 +1182,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica .then(applicationService.update(importedApplication.getId(), importedApplication)) .then(sendImportExportApplicationAnalyticsEvent(importedApplication.getId(), AnalyticsEvents.IMPORT)) .zipWith(currUserMono) - .map(tuple -> { + .flatMap(tuple -> { Application application = tuple.getT1(); stopwatch.stopTimer(); stopwatch.stopAndLogTimeInMillis(); @@ -1195,8 +1195,8 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica FieldName.FLOW_NAME, stopwatch.getFlow(), "executionTime", stopwatch.getExecutionTime() ); - analyticsService.sendEvent(AnalyticsEvents.UNIT_EXECUTION_TIME.getEventName(), tuple.getT2().getUsername(), data); - return application; + return analyticsService.sendEvent(AnalyticsEvents.UNIT_EXECUTION_TIME.getEventName(), tuple.getT2().getUsername(), data) + .thenReturn(application); }); }) .onErrorResume(throwable -> { diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/UserSignupCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/UserSignupCEImpl.java index e2717aa63f..2c655f6faf 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/UserSignupCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/UserSignupCEImpl.java @@ -227,7 +227,8 @@ public class UserSignupCEImpl implements UserSignupCE { configService.getInstanceId() .map(instanceId -> { log.debug("Installation setup complete."); - analyticsService.sendEvent( + analyticsService.identifyInstance(instanceId, userData.getRole(), userData.getUseCase()); + return analyticsService.sendEvent( AnalyticsEvents.INSTALLATION_SETUP_COMPLETE.getEventName(), instanceId, Map.of( @@ -238,9 +239,7 @@ public class UserSignupCEImpl implements UserSignupCE { "goal", ObjectUtils.defaultIfNull(userData.getUseCase(), "") ), false - ); - analyticsService.identifyInstance(instanceId, userData.getRole(), userData.getUseCase()); - return instanceId; + ).thenReturn(instanceId); }), envManager.applyChanges(Map.of( APPSMITH_DISABLE_TELEMETRY.name(),