## Description This PR added fix for not triggering usage pulse for air gapped instances Fixes https://github.com/appsmithorg/cloud-services/issues/1883 ## Automation /ok-to-test tags="@tag.Sanity" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/12867501950> > Commit: 2300d200cf4213edfc734c1a8b89b4ad797cdb64 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12867501950&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Sanity` > Spec: > <hr>Mon, 20 Jan 2025 12:38:11 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Enhanced user activity tracking by introducing airgapped environment detection, preventing unnecessary tracking in restricted network settings. - **Tests** - Added a new test suite to verify the behavior of the user activity tracking method in airgapped conditions, ensuring correct functionality based on the airgapped status. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
168 lines
5.0 KiB
TypeScript
168 lines
5.0 KiB
TypeScript
import {
|
|
getAppViewerPageIdFromPath,
|
|
isEditorPath,
|
|
isViewerPath,
|
|
} from "ee/pages/Editor/Explorer/helpers";
|
|
import { fetchWithRetry, getUsagePulsePayload } from "./utils";
|
|
import {
|
|
PULSE_API_ENDPOINT,
|
|
PULSE_API_MAX_RETRY_COUNT,
|
|
PULSE_API_RETRY_TIMEOUT,
|
|
USER_ACTIVITY_LISTENER_EVENTS,
|
|
} from "ee/constants/UsagePulse";
|
|
import PageApi from "api/PageApi";
|
|
import { APP_MODE } from "entities/App";
|
|
import { getFirstTimeUserOnboardingIntroModalVisibility } from "utils/storage";
|
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
|
import { PULSE_INTERVAL as PULSE_INTERVAL_CE } from "ce/constants/UsagePulse";
|
|
import { PULSE_INTERVAL as PULSE_INTERVAL_EE } from "ee/constants/UsagePulse";
|
|
import store from "store";
|
|
import type { PageListReduxState } from "reducers/entityReducers/pageListReducer";
|
|
import { isAirgapped } from "ee/utils/airgapHelpers";
|
|
|
|
class UsagePulse {
|
|
static userAnonymousId: string | undefined;
|
|
static Timer: ReturnType<typeof setTimeout>;
|
|
static unlistenRouteChange: () => void;
|
|
static isTelemetryEnabled: boolean;
|
|
static isAnonymousUser: boolean;
|
|
static isFreePlan: boolean;
|
|
static isAirgapped = isAirgapped();
|
|
|
|
/*
|
|
* Function to check if the given URL is trakable or not.
|
|
* app builder and viewer urls are trackable
|
|
*/
|
|
static async isTrackableUrl(path: string) {
|
|
if (isViewerPath(path)) {
|
|
if (UsagePulse.isAnonymousUser) {
|
|
/*
|
|
In App view mode for non-logged in user, first we must have to check if the app is public or not.
|
|
If it is private app with non-logged in user, we do not send pulse at this point instead we redirect to the login page.
|
|
And for login page no usage pulse is required.
|
|
*/
|
|
const basePageId = getAppViewerPageIdFromPath(path);
|
|
const { pages } = store.getState().entities
|
|
.pageList as PageListReduxState;
|
|
const pageId = pages?.find(
|
|
(page) => page.basePageId === basePageId,
|
|
)?.pageId;
|
|
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const response: any = await PageApi.fetchAppAndPages({
|
|
pageId,
|
|
mode: APP_MODE.PUBLISHED,
|
|
});
|
|
const { data } = response ?? {};
|
|
|
|
if (data?.application && !data.application.isPublic) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} else if (isEditorPath(path)) {
|
|
/*
|
|
During onboarding we show the Intro Modal and let user use the app for the first time.
|
|
During this exploration period, we do no send usage pulse.
|
|
*/
|
|
const isFirstTimeOnboarding =
|
|
await getFirstTimeUserOnboardingIntroModalVisibility();
|
|
|
|
if (!isFirstTimeOnboarding) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static sendPulse() {
|
|
const payload = getUsagePulsePayload(
|
|
UsagePulse.isTelemetryEnabled,
|
|
UsagePulse.isAnonymousUser,
|
|
);
|
|
|
|
const fetchWithRetryConfig = {
|
|
url: PULSE_API_ENDPOINT,
|
|
payload,
|
|
retries: PULSE_API_MAX_RETRY_COUNT,
|
|
retryTimeout: PULSE_API_RETRY_TIMEOUT,
|
|
};
|
|
|
|
fetchWithRetry(fetchWithRetryConfig);
|
|
}
|
|
|
|
static registerActivityListener() {
|
|
USER_ACTIVITY_LISTENER_EVENTS.forEach((event) => {
|
|
window.document.body.addEventListener(
|
|
event,
|
|
UsagePulse.sendPulseAndScheduleNext,
|
|
);
|
|
});
|
|
}
|
|
|
|
static deregisterActivityListener() {
|
|
USER_ACTIVITY_LISTENER_EVENTS.forEach((event) => {
|
|
window.document.body.removeEventListener(
|
|
event,
|
|
UsagePulse.sendPulseAndScheduleNext,
|
|
);
|
|
});
|
|
}
|
|
|
|
/*
|
|
* Function that suspends active tracking listeners
|
|
* and schedules when next listeners should be registered.
|
|
*/
|
|
static scheduleNextActivityListeners() {
|
|
UsagePulse.deregisterActivityListener();
|
|
|
|
UsagePulse.Timer = setTimeout(
|
|
UsagePulse.registerActivityListener,
|
|
UsagePulse.isFreePlan ? PULSE_INTERVAL_CE : PULSE_INTERVAL_EE,
|
|
);
|
|
}
|
|
|
|
/*
|
|
* Point of entry for the user tracking
|
|
*/
|
|
static async startTrackingActivity(
|
|
isTelemetryEnabled: boolean,
|
|
isAnonymousUser: boolean,
|
|
isFree: boolean,
|
|
) {
|
|
UsagePulse.isTelemetryEnabled = isTelemetryEnabled;
|
|
UsagePulse.isAnonymousUser = isAnonymousUser;
|
|
UsagePulse.isFreePlan = isFree;
|
|
|
|
if (await UsagePulse.isTrackableUrl(window.location.pathname)) {
|
|
await UsagePulse.sendPulseAndScheduleNext();
|
|
}
|
|
}
|
|
|
|
/*
|
|
* triggers a pulse and schedules the pulse , if user is on a trackable url, otherwise
|
|
* registers listeners to wait for the user to go to a trackable url
|
|
*/
|
|
static async sendPulseAndScheduleNext() {
|
|
if (UsagePulse.isAirgapped) {
|
|
return;
|
|
}
|
|
|
|
UsagePulse.sendPulse();
|
|
UsagePulse.scheduleNextActivityListeners();
|
|
}
|
|
|
|
/*
|
|
* Function to cleanup states and listeners
|
|
*/
|
|
static stopTrackingActivity() {
|
|
UsagePulse.userAnonymousId = undefined;
|
|
clearTimeout(UsagePulse.Timer);
|
|
UsagePulse.unlistenRouteChange && UsagePulse.unlistenRouteChange();
|
|
UsagePulse.deregisterActivityListener();
|
|
}
|
|
}
|
|
|
|
export default UsagePulse;
|