diff --git a/app/client/src/actions/analyticsActions.ts b/app/client/src/actions/analyticsActions.ts new file mode 100644 index 0000000000..d020fabe66 --- /dev/null +++ b/app/client/src/actions/analyticsActions.ts @@ -0,0 +1,8 @@ +import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; + +export const segmentInitSuccess = () => ({ + type: ReduxActionTypes.SEGMENT_INITIALIZED, +}); +export const segmentInitUncertain = () => ({ + type: ReduxActionTypes.SEGMENT_INIT_UNCERTAIN, +}); diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index 8c31dda880..0a4fc3c4f8 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -1216,6 +1216,8 @@ const TenantActionErrorTypes = { }; const AnalyticsActionTypes = { + SEGMENT_INITIALIZED: "SEGMENT_INITIALIZED", + SEGMENT_INIT_UNCERTAIN: "SEGMENT_INIT_UNCERTAIN", SET_BUILDING_BLOCK_DRAG_START_TIME: "SET_BUILDING_BLOCK_DRAG_START_TIME", RESET_BUILDING_BLOCK_DRAG_START_TIME: "RESET_BUILDING_BLOCK_DRAG_START_TIME", SEND_ANALYTICS_FOR_SIDE_BY_SIDE_HOVER: diff --git a/app/client/src/ce/reducers/index.tsx b/app/client/src/ce/reducers/index.tsx index 73842c65b5..e1339f8472 100644 --- a/app/client/src/ce/reducers/index.tsx +++ b/app/client/src/ce/reducers/index.tsx @@ -64,6 +64,7 @@ import type { CanvasLevelsReduxState } from "reducers/entityReducers/autoHeightR import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers"; import lintErrorReducer from "reducers/lintingReducers"; import type { AutoHeightUIState } from "reducers/uiReducers/autoHeightReducer"; +import type { AnalyticsReduxState } from "reducers/uiReducers/analyticsReducer"; import type { MetaWidgetsReduxState } from "reducers/entityReducers/metaWidgetsReducer"; import type { layoutConversionReduxState } from "reducers/uiReducers/layoutConversionReducer"; import type { OneClickBindingState } from "reducers/uiReducers/oneClickBindingReducer"; @@ -94,6 +95,7 @@ export const reducerObject = { export interface AppState { ui: { consolidatedPageLoad: ConsolidatedPageLoadState; + analytics: AnalyticsReduxState; editor: EditorReduxState; propertyPane: PropertyPaneReduxState; tableFilterPane: TableFilterPaneReduxState; diff --git a/app/client/src/ce/reducers/uiReducers/index.tsx b/app/client/src/ce/reducers/uiReducers/index.tsx index d59c5aa1a6..3685e13afb 100644 --- a/app/client/src/ce/reducers/uiReducers/index.tsx +++ b/app/client/src/ce/reducers/uiReducers/index.tsx @@ -39,6 +39,7 @@ import { editorContextReducer } from "ee/reducers/uiReducers/editorContextReduce import libraryReducer from "reducers/uiReducers/libraryReducer"; import appSettingsPaneReducer from "reducers/uiReducers/appSettingsPaneReducer"; import autoHeightUIReducer from "reducers/uiReducers/autoHeightReducer"; +import analyticsReducer from "reducers/uiReducers/analyticsReducer"; import layoutConversionReducer from "reducers/uiReducers/layoutConversionReducer"; import oneClickBindingReducer from "reducers/uiReducers/oneClickBindingReducer"; import activeFieldReducer from "reducers/uiReducers/activeFieldEditorReducer"; @@ -48,6 +49,7 @@ import consolidatedPageLoadReducer from "reducers/uiReducers/consolidatedPageLoa import { pluginActionReducer } from "PluginActionEditor/store"; export const uiReducerObject = { + analytics: analyticsReducer, editor: editorReducer, errors: errorReducer, propertyPane: propertyPaneReducer, diff --git a/app/client/src/ce/sagas/userSagas.tsx b/app/client/src/ce/sagas/userSagas.tsx index 7da583fe34..34031d6ac6 100644 --- a/app/client/src/ce/sagas/userSagas.tsx +++ b/app/client/src/ce/sagas/userSagas.tsx @@ -1,4 +1,12 @@ -import { call, fork, put, select, take } from "redux-saga/effects"; +import { + all, + call, + fork, + put, + select, + take, + type TakeEffect, +} from "redux-saga/effects"; import type { ReduxAction, ReduxActionWithPromise, @@ -75,6 +83,11 @@ import type { import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors"; import { getFromServerWhenNoPrefetchedResult } from "sagas/helper"; import type { SessionRecordingConfig } from "utils/Analytics/mixpanel"; +import { + segmentInitSuccess, + segmentInitUncertain, +} from "actions/analyticsActions"; +import { getSegmentState } from "selectors/analyticsSelectors"; export function* getCurrentUserSaga(action?: { payload?: { userProfile?: ApiResponse }; @@ -139,44 +152,82 @@ function* getSessionRecordingConfig() { function* initTrackers(currentUser: User) { try { + const isFFFetched: boolean = yield select(getFeatureFlagsFetched); + + if (!isFFFetched) { + yield take(ReduxActionTypes.FETCH_FEATURE_FLAGS_SUCCESS); + } + const sessionRecordingConfig: SessionRecordingConfig = yield call( getSessionRecordingConfig, ); yield call(AnalyticsUtil.initialize, currentUser, sessionRecordingConfig); + yield put(segmentInitSuccess()); } catch (e) { log.error(e); + yield put(segmentInitUncertain()); } } +function* waitForInitialization() { + const currentUser: User = yield select(getCurrentUser); + // Dependents for starting tracking + const isFFFetched: boolean = yield select(getFeatureFlagsFetched); + const isSegmentInitialized: string | undefined = + yield select(getSegmentState); + + const waits: TakeEffect[] = []; + + // FF is required to know if GAC is enabled for the user + if (!isFFFetched) { + yield fork(fetchFeatureFlagsInit); + waits.push(take(ReduxActionTypes.FETCH_FEATURE_FLAGS_SUCCESS)); + } + + // If the user is anonymous, we need to wait for the editor or viewer to initialize + if (currentUser?.isAnonymous) { + waits.push( + take([ + ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS, + ReduxActionTypes.INITIALIZE_PAGE_VIEWER_SUCCESS, + ]), + ); + + // If the user is anonymous, we need to wait for the segment to initialize + // As it will provide the anonymous id + if (isSegmentInitialized === undefined) { + waits.push( + take([ + ReduxActionTypes.SEGMENT_INITIALIZED, + ReduxActionTypes.SEGMENT_INIT_UNCERTAIN, + ]), + ); + } + } + + // Only wait for actions that are still pending + yield all(waits); +} + function* restartUserTracking() { const currentUser: User = yield select(getCurrentUser); const { enableTelemetry } = currentUser; const isAirgappedInstance = isAirgapped(); - const isFFFetched: boolean = yield select(getFeatureFlagsFetched); - - if (!isFFFetched) { - yield call(fetchFeatureFlagsInit); - yield take(ReduxActionTypes.FETCH_FEATURE_FLAGS_SUCCESS); - } - - const featureFlags: FeatureFlags = yield select(selectFeatureFlags); - - const isGACEnabled = featureFlags?.license_gac_enabled; - - const isFreeLicense = !isGACEnabled; - if (!isAirgappedInstance) { - // We need to stop and start tracking activity to ensure that the tracking from previous session is not carried forward + // We need to stop and start tracking activity to ensure that the tracking + // from previous session is not carried forward yield call(UsagePulse.stopTrackingActivity); - if (currentUser?.isAnonymous) { - yield take([ - ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS, - ReduxActionTypes.INITIALIZE_PAGE_VIEWER_SUCCESS, - ]); - } + // Wait for any items that are required for tracking + yield call(waitForInitialization); + + const featureFlags: FeatureFlags = yield select(selectFeatureFlags); + + const isGACEnabled = featureFlags?.license_gac_enabled; + + const isFreeLicense = !isGACEnabled; yield call( UsagePulse.startTrackingActivity, @@ -191,12 +242,14 @@ export function* runUserSideEffectsSaga() { const currentUser: User = yield select(getCurrentUser); const { enableTelemetry } = currentUser; + yield fork(restartUserTracking); + if (enableTelemetry) { yield fork(initTrackers, currentUser); + } else { + yield put(segmentInitSuccess()); } - yield fork(restartUserTracking); - if (currentUser.emptyInstance) { history.replace(SETUP); } diff --git a/app/client/src/reducers/uiReducers/analyticsReducer.ts b/app/client/src/reducers/uiReducers/analyticsReducer.ts new file mode 100644 index 0000000000..c14cba65bb --- /dev/null +++ b/app/client/src/reducers/uiReducers/analyticsReducer.ts @@ -0,0 +1,35 @@ +import { ReduxActionTypes } from "ee/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/selectors/analyticsSelectors.ts b/app/client/src/selectors/analyticsSelectors.ts new file mode 100644 index 0000000000..0f3eb5ba64 --- /dev/null +++ b/app/client/src/selectors/analyticsSelectors.ts @@ -0,0 +1,4 @@ +import type { AppState } from "ee/reducers"; + +export const getSegmentState = (state: AppState) => + state.ui.analytics.telemetry.segmentState;