PromucFlow_constructor/app/client/src/ce/sagas/userSagas.tsx
Diljit ed17ee0376
chore: ce - remove sentry performance tracker (#35710)
## Description
Fix the following errors

### POST https://o296332.ingest.sentry.io/api/1546547/envelope/... 429
(Too Many Requests)

We have an integration with Sentry to instrument page loads and other
transactions. This is no longer used. All page load metrics are
collected using a new relic integration.
The sentry transactions api was throwing a 429 error when we exceed our
trial quota. Removing the integration should curb this error.

<img width="1181" alt="Screenshot 2024-08-15 at 1 22 28 PM"
src="https://github.com/user-attachments/assets/543c0ec1-e87f-4439-b715-e75b3a6fd3ed">

[Slack thread
](https://theappsmith.slack.com/archives/CGBPVEJ5C/p1723699775838509)for
more context on our sentry sub exceeding its quota.

### TypeError: e.className.split is not a function at t.value
(PageLoadInstrumentation.ts:112:33)

Add a type check for string.


Fixes #`Issue Number`  
_or_  
Fixes `Issue URL`
> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

/ok-to-test tags="@tag.All"

### 🔍 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/10400625143>
> Commit: fc83198b613a973c9a02644fc742947a92bfbd3c
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=10400625143&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Thu, 15 Aug 2024 08:45:49 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

- **New Features**
- Removed performance tracking functionality across various components
and sagas, simplifying the codebase and reducing overhead.
- **Bug Fixes**
- No specific bug fixes were made; improvements focus on performance
tracking removal.
- **Chores**
- Eliminated unnecessary dependencies related to performance metrics,
streamlining the application's dependency management.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-08-15 19:40:53 +05:30

663 lines
19 KiB
TypeScript

import { call, put, race, select, take } from "redux-saga/effects";
import type {
ReduxAction,
ReduxActionWithPromise,
} from "ee/constants/ReduxActionConstants";
import {
ReduxActionTypes,
ReduxActionErrorTypes,
} from "ee/constants/ReduxActionConstants";
import { reset } from "redux-form";
import type {
CreateUserRequest,
CreateUserResponse,
ForgotPasswordRequest,
VerifyTokenRequest,
TokenPasswordUpdateRequest,
UpdateUserRequest,
LeaveWorkspaceRequest,
} from "ee/api/UserApi";
import UserApi from "ee/api/UserApi";
import { AUTH_LOGIN_URL, SETUP } from "constants/routes";
import history from "utils/history";
import type { ApiResponse } from "api/ApiResponses";
import type { ErrorActionPayload } from "sagas/ErrorSagas";
import {
validateResponse,
getResponseErrorMessage,
callAPI,
} from "sagas/ErrorSagas";
import {
logoutUserSuccess,
logoutUserError,
verifyInviteSuccess,
verifyInviteError,
invitedUserSignupError,
invitedUserSignupSuccess,
fetchFeatureFlagsSuccess,
fetchFeatureFlagsError,
fetchProductAlertSuccess,
fetchProductAlertFailure,
fetchFeatureFlagsInit,
} from "actions/userActions";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import { INVITE_USERS_TO_WORKSPACE_FORM } from "ee/constants/forms";
import type { User } from "constants/userConstants";
import { ANONYMOUS_USERNAME } from "constants/userConstants";
import {
flushErrorsAndRedirect,
safeCrashAppRequest,
} from "actions/errorActions";
import localStorage from "utils/localStorage";
import log from "loglevel";
import {
getCurrentUser,
getFeatureFlagsFetched,
} from "selectors/usersSelectors";
import {
initAppLevelSocketConnection,
initPageLevelSocketConnection,
} from "actions/websocketActions";
import {
getEnableStartSignposting,
getFirstTimeUserOnboardingApplicationIds,
getFirstTimeUserOnboardingIntroModalVisibility,
} from "utils/storage";
import { initializeAnalyticsAndTrackers } from "utils/AppsmithUtils";
import { getAppsmithConfigs } from "ee/configs";
import { getSegmentState } from "selectors/analyticsSelectors";
import {
segmentInitUncertain,
segmentInitSuccess,
} from "actions/analyticsActions";
import type { SegmentState } from "reducers/uiReducers/analyticsReducer";
import type { FeatureFlags } from "ee/entities/FeatureFlag";
import { DEFAULT_FEATURE_FLAG_VALUE } from "ee/entities/FeatureFlag";
import UsagePulse from "usagePulse";
import { toast } from "@appsmith/ads";
import { isAirgapped } from "ee/utils/airgapHelpers";
import {
USER_PROFILE_PICTURE_UPLOAD_FAILED,
UPDATE_USER_DETAILS_FAILED,
} from "ee/constants/messages";
import { createMessage } from "@appsmith/ads-old";
import type {
ProductAlert,
ProductAlertConfig,
} from "reducers/uiReducers/usersReducer";
import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors";
import { getFromServerWhenNoPrefetchedResult } from "sagas/helper";
export function* createUserSaga(
action: ReduxActionWithPromise<CreateUserRequest>,
) {
const { email, password, reject, resolve } = action.payload;
try {
const request: CreateUserRequest = { email, password };
const response: CreateUserResponse = yield callAPI(
UserApi.createUser,
request,
);
//TODO(abhinav): DRY this
const isValidResponse: boolean = yield validateResponse(response);
if (!isValidResponse) {
const errorMessage = getResponseErrorMessage(response);
yield call(reject, { _error: errorMessage });
} else {
//@ts-expect-error: response is of type unknown
const { email, id, name } = response.data;
yield put({
type: ReduxActionTypes.CREATE_USER_SUCCESS,
payload: {
email,
name,
id,
},
});
yield call(resolve);
}
} catch (error) {
yield call(reject, { _error: (error as Error).message });
yield put({
type: ReduxActionErrorTypes.CREATE_USER_ERROR,
payload: {
error,
},
});
}
}
export function* waitForSegmentInit(skipWithAnonymousId: boolean) {
if (skipWithAnonymousId && AnalyticsUtil.getAnonymousId()) return;
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(action?: {
payload?: { userProfile?: ApiResponse };
}) {
const userProfile = action?.payload?.userProfile;
try {
const response: ApiResponse = yield call(
getFromServerWhenNoPrefetchedResult,
userProfile,
() => call(UserApi.getCurrentUser),
);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS,
payload: response.data,
});
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.FETCH_USER_DETAILS_ERROR,
payload: {
error,
},
});
yield put(safeCrashAppRequest());
}
}
export function* runUserSideEffectsSaga() {
const currentUser: User = yield select(getCurrentUser);
const { enableTelemetry } = currentUser;
const isAirgappedInstance = isAirgapped();
if (enableTelemetry) {
const promise = initializeAnalyticsAndTrackers();
if (promise instanceof Promise) {
const result: boolean = yield promise;
if (result) {
yield put(segmentInitSuccess());
} else {
yield put(segmentInitUncertain());
}
}
}
if (!currentUser.isAnonymous && currentUser.username !== ANONYMOUS_USERNAME) {
enableTelemetry && AnalyticsUtil.identifyUser(currentUser);
}
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
UsagePulse.stopTrackingActivity();
UsagePulse.startTrackingActivity(
enableTelemetry && getAppsmithConfigs().segment.enabled,
currentUser?.isAnonymous ?? false,
isFreeLicense,
);
}
yield put(initAppLevelSocketConnection());
yield put(initPageLevelSocketConnection());
if (currentUser.emptyInstance) {
history.replace(SETUP);
}
}
export function* forgotPasswordSaga(
action: ReduxActionWithPromise<ForgotPasswordRequest>,
) {
const { email, reject, resolve } = action.payload;
try {
const request: ForgotPasswordRequest = { email };
const response: ApiResponse = yield callAPI(
UserApi.forgotPassword,
request,
);
const isValidResponse: boolean = yield validateResponse(response);
if (!isValidResponse) {
const errorMessage: string | undefined =
yield getResponseErrorMessage(response);
yield call(reject, { _error: errorMessage });
} else {
yield put({
type: ReduxActionTypes.FORGOT_PASSWORD_SUCCESS,
});
yield call(resolve);
}
} catch (error) {
log.error(error);
yield call(reject, { _error: (error as Error).message });
yield put({
type: ReduxActionErrorTypes.FORGOT_PASSWORD_ERROR,
});
}
}
export function* resetPasswordSaga(
action: ReduxActionWithPromise<TokenPasswordUpdateRequest>,
) {
const { email, password, reject, resolve, token } = action.payload;
try {
const request: TokenPasswordUpdateRequest = {
email,
password,
token,
};
const response: ApiResponse = yield callAPI(UserApi.resetPassword, request);
const isValidResponse: boolean = yield validateResponse(response);
if (!isValidResponse) {
const errorMessage: string | undefined =
yield getResponseErrorMessage(response);
yield call(reject, { _error: errorMessage });
} else {
yield put({
type: ReduxActionTypes.RESET_USER_PASSWORD_SUCCESS,
});
yield call(resolve);
}
} catch (error) {
log.error(error);
yield call(reject, { _error: (error as Error).message });
yield put({
type: ReduxActionErrorTypes.RESET_USER_PASSWORD_ERROR,
payload: {
error: (error as Error).message,
},
});
}
}
export function* invitedUserSignupSaga(
action: ReduxActionWithPromise<TokenPasswordUpdateRequest>,
) {
const { email, password, reject, resolve, token } = action.payload;
try {
const request: TokenPasswordUpdateRequest = { email, password, token };
const response: ApiResponse = yield callAPI(
UserApi.confirmInvitedUserSignup,
request,
);
const isValidResponse: boolean = yield validateResponse(response);
if (!isValidResponse) {
const errorMessage: string | undefined =
yield getResponseErrorMessage(response);
yield call(reject, { _error: errorMessage });
} else {
yield put(invitedUserSignupSuccess());
yield call(resolve);
}
} catch (error) {
log.error(error);
yield call(reject, { _error: (error as Error).message });
yield put(invitedUserSignupError(error));
}
}
interface InviteUserPayload {
email: string;
permissionGroupId: string;
}
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function* inviteUser(payload: InviteUserPayload, reject: any) {
const response: ApiResponse = yield callAPI(UserApi.inviteUser, payload);
const isValidResponse: boolean = yield validateResponse(response);
if (!isValidResponse) {
let errorMessage = `${payload.email}: `;
errorMessage += getResponseErrorMessage(response);
yield call(reject, { _error: errorMessage });
}
yield;
}
export function* inviteUsers(
action: ReduxActionWithPromise<{
data: {
usernames: string[];
workspaceId: string;
permissionGroupId: string;
recaptchaToken?: string;
};
}>,
) {
const { data, reject, resolve } = action.payload;
try {
const response: ApiResponse<{ id: string; username: string }[]> =
yield callAPI(UserApi.inviteUser, {
usernames: data.usernames,
permissionGroupId: data.permissionGroupId,
recaptchaToken: data.recaptchaToken,
});
const isValidResponse: boolean = yield validateResponse(response, false);
if (!isValidResponse) {
let errorMessage = `${data.usernames}: `;
errorMessage += getResponseErrorMessage(response);
yield call(reject, { _error: errorMessage });
}
yield put({
type: ReduxActionTypes.FETCH_ALL_USERS_INIT,
payload: {
workspaceId: data.workspaceId,
},
});
const { data: responseData } = response;
yield put({
type: ReduxActionTypes.INVITED_USERS_TO_WORKSPACE,
payload: {
workspaceId: data.workspaceId,
users: responseData.map((user: { id: string; username: string }) => ({
userId: user.id,
username: user.username,
permissionGroupId: data.permissionGroupId,
})),
},
});
yield call(resolve);
yield put(reset(INVITE_USERS_TO_WORKSPACE_FORM));
} catch (error) {
yield call(reject, { _error: (error as Error).message });
}
}
export function* updateUserDetailsSaga(action: ReduxAction<UpdateUserRequest>) {
try {
const { email, intercomConsentGiven, name, proficiency, role, useCase } =
action.payload;
const response: ApiResponse = yield callAPI(UserApi.updateUser, {
email,
name,
proficiency,
role,
useCase,
intercomConsentGiven,
});
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.UPDATE_USER_DETAILS_SUCCESS,
payload: response.data,
});
}
} catch (error) {
const payload: ErrorActionPayload = {
show: true,
error: {
message:
(error as Error).message ?? createMessage(UPDATE_USER_DETAILS_FAILED),
},
};
yield put({
type: ReduxActionErrorTypes.UPDATE_USER_DETAILS_ERROR,
payload,
});
}
}
export function* verifyResetPasswordTokenSaga(
action: ReduxAction<VerifyTokenRequest>,
) {
try {
const request: VerifyTokenRequest = action.payload;
const response: ApiResponse = yield call(
UserApi.verifyResetPasswordToken,
request,
);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse && response.data) {
yield put({
type: ReduxActionTypes.RESET_PASSWORD_VERIFY_TOKEN_SUCCESS,
});
} else {
yield put({
type: ReduxActionErrorTypes.RESET_PASSWORD_VERIFY_TOKEN_ERROR,
});
}
} catch (error) {
log.error(error);
yield put({
type: ReduxActionErrorTypes.RESET_PASSWORD_VERIFY_TOKEN_ERROR,
});
}
}
export function* verifyUserInviteSaga(action: ReduxAction<VerifyTokenRequest>) {
try {
const request: VerifyTokenRequest = action.payload;
const response: ApiResponse = yield call(UserApi.verifyUserInvite, request);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
yield put(verifyInviteSuccess());
}
} catch (error) {
log.error(error);
yield put(verifyInviteError(error));
}
}
export function* logoutSaga(action: ReduxAction<{ redirectURL: string }>) {
try {
const redirectURL = action.payload?.redirectURL;
const response: ApiResponse = yield call(UserApi.logoutUser);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
UsagePulse.stopTrackingActivity();
AnalyticsUtil.reset();
const currentUser: User | undefined = yield select(getCurrentUser);
yield put(logoutUserSuccess(!!currentUser?.emptyInstance));
localStorage.clear();
yield put(flushErrorsAndRedirect(redirectURL || AUTH_LOGIN_URL));
}
} catch (error) {
log.error(error);
yield put(logoutUserError(error));
}
}
export function* waitForFetchUserSuccess() {
const currentUser: string | undefined = yield select(getCurrentUser);
if (!currentUser) {
yield take(ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS);
}
}
export function* removePhoto(
action: ReduxAction<{ callback: (id: string) => void }>,
) {
try {
const response: ApiResponse = yield call(UserApi.deletePhoto);
//@ts-expect-error: response is of type unknown
const photoId = response.data?.profilePhotoAssetId; //get updated photo id of iploaded image
if (action.payload.callback) action.payload.callback(photoId);
} catch (error) {
log.error(error);
}
}
export function* updatePhoto(
action: ReduxAction<{ file: File; callback: (id: string) => void }>,
) {
try {
const response: ApiResponse = yield call(UserApi.uploadPhoto, {
file: action.payload.file,
});
if (!response.responseMeta.success) {
throw response.responseMeta.error;
}
//@ts-expect-error: response is of type unknown
const photoId = response.data?.profilePhotoAssetId; //get updated photo id of iploaded image
if (action.payload.callback) action.payload.callback(photoId);
} catch (error) {
log.error(error);
const payload: ErrorActionPayload = {
show: true,
error: {
message:
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(error as any).message ??
createMessage(USER_PROFILE_PICTURE_UPLOAD_FAILED),
},
};
yield put({
type: ReduxActionErrorTypes.USER_PROFILE_PICTURE_UPLOAD_FAILED,
payload,
});
}
}
export function* fetchFeatureFlags(action?: {
payload?: { featureFlags?: ApiResponse<FeatureFlags> };
}) {
const featureFlags = action?.payload?.featureFlags;
try {
const response: ApiResponse<FeatureFlags> = yield call(
getFromServerWhenNoPrefetchedResult,
featureFlags,
() => call(UserApi.fetchFeatureFlags),
);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
yield put(
fetchFeatureFlagsSuccess({
...DEFAULT_FEATURE_FLAG_VALUE,
...response.data,
}),
);
}
} catch (error) {
log.error(error);
yield put(fetchFeatureFlagsError(error));
}
}
export function* updateFirstTimeUserOnboardingSage() {
const enable: boolean | null = yield call(getEnableStartSignposting);
if (enable) {
const applicationIds: string[] =
yield getFirstTimeUserOnboardingApplicationIds() || [];
const introModalVisibility: string | null =
yield getFirstTimeUserOnboardingIntroModalVisibility();
yield put({
type: ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_APPLICATION_IDS,
payload: applicationIds,
});
yield put({
type: ReduxActionTypes.SET_SHOW_FIRST_TIME_USER_ONBOARDING_MODAL,
payload: introModalVisibility,
});
}
}
export function* leaveWorkspaceSaga(
action: ReduxAction<LeaveWorkspaceRequest>,
) {
try {
const request: LeaveWorkspaceRequest = action.payload;
const { workspaceId } = action.payload;
const response: ApiResponse = yield call(UserApi.leaveWorkspace, request);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.DELETE_WORKSPACE_SUCCESS,
payload: workspaceId,
});
toast.show(`You have successfully left the workspace`, {
kind: "success",
});
history.push("/applications");
}
} catch (error) {
// do nothing as it's already handled globally
}
}
export function* fetchProductAlertSaga(action?: {
payload?: { productAlert?: ApiResponse<ProductAlert> };
}) {
const productAlert = action?.payload?.productAlert;
try {
const response: ApiResponse<ProductAlert> = yield call(
getFromServerWhenNoPrefetchedResult,
productAlert,
() => call(UserApi.getProductAlert),
);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
const message = response.data;
if (message.messageId) {
const config = getMessageConfig(message.messageId);
yield put(fetchProductAlertSuccess({ message, config }));
}
} else {
yield put(fetchProductAlertFailure(response.data));
}
} catch (e) {
yield put(fetchProductAlertFailure(e));
}
}
export const PRODUCT_ALERT_CONFIG_STORAGE_KEY = "PRODUCT_ALERT_CONFIG";
export const getMessageConfig = (id: string): ProductAlertConfig => {
const storedConfig =
localStorage.getItem(PRODUCT_ALERT_CONFIG_STORAGE_KEY) || "{}";
const alertConfig: Record<string, ProductAlertConfig> =
JSON.parse(storedConfig);
if (id in alertConfig) {
return alertConfig[id];
}
return {
snoozeTill: new Date(),
dismissed: false,
};
};
export const setMessageConfig = (id: string, config: ProductAlertConfig) => {
const storedConfig =
localStorage.getItem(PRODUCT_ALERT_CONFIG_STORAGE_KEY) || "{}";
const alertConfig: Record<string, ProductAlertConfig> =
JSON.parse(storedConfig);
const updatedConfig: Record<string, ProductAlertConfig> = {
...alertConfig,
[id]: config,
};
localStorage.setItem(
PRODUCT_ALERT_CONFIG_STORAGE_KEY,
JSON.stringify(updatedConfig),
);
};