Skip to content

Commit

Permalink
chore: Include version in all requests (appsmithorg#37551)
Browse files Browse the repository at this point in the history
We're adding `x-appsmith-version` to every request from the client. If
server sees a request with a `x-appsmith-version` that doesn't match its
own, it rejects. If the server sees a request _without_ any
`x-appsmith-version` header, we don't reject it... for now.

On the client, when server responds with the "version mismatch" error,
we trigger the "There's a new version, please refresh" toast UI flow.

This is a step towards removing our dependency on websockets and
client—RTS connection.

## Automation

/test 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/12029721517>
> Commit: c4321f7
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12029721517&attempt=2"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Sanity`
> Spec:
> <hr>Tue, 26 Nov 2024 12:28:32 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

## Release Notes

- **New Features**
- Introduced a custom header for API requests to include application
version.
- Added specific error handling for 400 Bad Request errors, enhancing
user feedback on version mismatches.

- **Improvements**
- Enhanced error handling for version mismatches, providing clearer
error messages.
- Simplified version update handling, focusing on logging and user
prompts without state management.
- Improved default handling for application version configuration to
prevent undefined values.

- **Chores**
- Removed outdated version state management functions to streamline
code.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Pawan Kumar <[email protected]>
  • Loading branch information
sharat87 and jsartisan authored Nov 27, 2024
1 parent e6eda3a commit d7276e7
Show file tree
Hide file tree
Showing 10 changed files with 74 additions and 124 deletions.
2 changes: 1 addition & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@
: LOG_LEVELS[1],
cloudHosting: CLOUD_HOSTING,
appVersion: {
id: parseConfig('{{env "APPSMITH_VERSION_ID"}}'),
id: parseConfig('{{env "APPSMITH_VERSION_ID"}}') || "UNKNOWN",
sha: parseConfig('{{env "APPSMITH_VERSION_SHA"}}'),
releaseDate: parseConfig('{{env "APPSMITH_VERSION_RELEASE_DATE"}}'),
},
Expand Down
13 changes: 13 additions & 0 deletions src/api/interceptors/request/addVersionHeader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { InternalAxiosRequestConfig } from "axios";

export const addVersionHeader = (
config: InternalAxiosRequestConfig,
options: { version: string },
) => {
const { version } = options;

config.headers = config.headers || {};
config.headers["X-Appsmith-Version"] = version;

return config;
};
9 changes: 9 additions & 0 deletions src/api/interceptors/request/apiRequestInterceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import getQueryParamsObject from "utils/getQueryParamsObject";
import { addRequestedByHeader } from "./addRequestedByHeader";
import { increaseGitApiTimeout } from "./increaseGitApiTimeout";
import { getCurrentGitBranch } from "selectors/gitSyncSelectors";
import { addVersionHeader as _addVersionHeader } from "./addVersionHeader";
import { getCurrentEnvironmentId } from "ee/selectors/environmentSelectors";
import { addGitBranchHeader as _addGitBranchHeader } from "./addGitBranchHeader";
import { addPerformanceMonitoringHeaders } from "./addPerformanceMonitoringHeaders";
Expand Down Expand Up @@ -49,10 +50,18 @@ const addAnonymousUserIdHeader = (config: InternalAxiosRequestConfig) => {
return _addAnonymousUserIdHeader(config, { anonymousId, segmentEnabled });
};

const addVersionHeader = (config: InternalAxiosRequestConfig) => {
const appsmithConfig = getAppsmithConfigs();
const version = appsmithConfig.appVersion.id;

return _addVersionHeader(config, { version });
};

export const apiRequestInterceptor = (config: InternalAxiosRequestConfig) => {
const interceptorPipeline = compose<InternalAxiosRequestConfig>(
blockAirgappedRoutes,
addRequestedByHeader,
addVersionHeader,
addGitBranchHeader,
increaseGitApiTimeout,
addEnvironmentHeader,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const apiFailureResponseInterceptor = async (
failureHandlers.handleServerError,
failureHandlers.handleUnauthorizedError,
failureHandlers.handleNotFoundError,
failureHandlers.handleBadRequestError,
];

for (const handler of handlers) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { AxiosError } from "axios";
import type { ApiResponse } from "../../../ApiResponses";
import { handleVersionMismatch } from "sagas/WebsocketSagas/versionUpdatePrompt";
import { getAppsmithConfigs } from "ee/configs";
import { SERVER_ERROR_CODES } from "ee/constants/ApiConstants";

export const handleBadRequestError = async (error: AxiosError<ApiResponse>) => {
const errorCode = error?.response?.data?.responseMeta.error?.code;

if (
error?.response?.status === 400 &&
SERVER_ERROR_CODES.VERSION_MISMATCH.includes("" + errorCode)
) {
const responseData = error?.response?.data;
const message = responseData?.responseMeta.error?.message;
const serverVersion = (responseData?.data as { serverVersion: string })
.serverVersion;

handleVersionMismatch(getAppsmithConfigs().appVersion.id, serverVersion);

return Promise.reject({
...error,
clientDefinedError: true,
statusCode: errorCode,
message,
});
}

return null;
};
1 change: 1 addition & 0 deletions src/api/interceptors/response/failureHandlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { handleCancelError } from "./handleCancelError";
export { handleOfflineError } from "./handleOfflineError";
export { handleTimeoutError } from "./handleTimeoutError";
export { handleNotFoundError } from "./handleNotFoundError";
export { handleBadRequestError } from "./handleBadRequestError";
export { handleUnauthorizedError } from "./handleUnauthorizedError";
export { handleExecuteActionError } from "./handleExecuteActionError";
export { handleMissingResponseMeta } from "./handleMissingResponseMeta";
1 change: 1 addition & 0 deletions src/ce/constants/ApiConstants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const SERVER_ERROR_CODES = {
"AE-APP-4013",
],
UNABLE_TO_FIND_PAGE: ["AE-APP-4027", "AE-USR-4004"],
VERSION_MISMATCH: ["AE-BAD-4002"],
};

export enum ERROR_CODES {
Expand Down
11 changes: 0 additions & 11 deletions src/sagas/WebsocketSagas/handleAppLevelSocketEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { put } from "redux-saga/effects";
import { APP_LEVEL_SOCKET_EVENTS } from "./socketEvents";

import { collabSetAppEditors } from "actions/appCollabActions";
import { getAppsmithConfigs } from "ee/configs";
import { handleVersionUpdate } from "./versionUpdatePrompt";

// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -13,15 +11,6 @@ export default function* handleAppLevelSocketEvents(event: any) {
case APP_LEVEL_SOCKET_EVENTS.LIST_ONLINE_APP_EDITORS: {
yield put(collabSetAppEditors(event.payload[0]));

return;
}
// notification on release version
case APP_LEVEL_SOCKET_EVENTS.RELEASE_VERSION_NOTIFICATION: {
const { appVersion } = getAppsmithConfigs();
const [serverVersion] = event.payload;

handleVersionUpdate(appVersion, serverVersion);

return;
}
}
Expand Down
110 changes: 18 additions & 92 deletions src/sagas/WebsocketSagas/versionUpdatePrompt.ts
Original file line number Diff line number Diff line change
@@ -1,111 +1,37 @@
// Check if user is updating the app when toast is shown
// Check how many times does the user see a toast before updating

import { toast } from "@appsmith/ads";
import {
createMessage,
INFO_VERSION_MISMATCH_FOUND_RELOAD_REQUEST,
} from "ee/constants/messages";
import type { AppVersionData } from "ee/configs/types";
import {
getVersionUpdateState,
removeVersionUpdateState,
setVersionUpdateState,
} from "utils/storage";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";

enum UpdateStateEvent {
PROMPT_SHOWN = "PROMPT_SHOWN",
UPDATE_REQUESTED = "UPDATE_REQUESTED",
function handleUpdateRequested(fromVersion: string, toVersion: string) {
AnalyticsUtil.logEvent("VERSION_UPDATE_REQUESTED", {
fromVersion,
toVersion,
});
// Reload to fetch the latest app version
location.reload();
}

export interface VersionUpdateState {
currentVersion: string;
upgradeVersion: string;
timesShown: number;
event: UpdateStateEvent;
}
export async function handleVersionMismatch(
currentVersion: string,
serverVersion: string,
) {
// If no version is set, ignore
if (!currentVersion) return;

let timesShown = 0;
AnalyticsUtil.logEvent("VERSION_UPDATE_SHOWN", {
fromVersion: currentVersion,
toVersion: serverVersion,
});

function showPrompt(newUpdateState: VersionUpdateState) {
toast.show(createMessage(INFO_VERSION_MISMATCH_FOUND_RELOAD_REQUEST), {
kind: "info",
autoClose: false,
action: {
text: "refresh",
effect: () => handleUpdateRequested(newUpdateState),
effect: () => handleUpdateRequested(currentVersion, serverVersion),
},
});
}

function handleUpdateRequested(newUpdateState: VersionUpdateState) {
// store version update with timesShown counter
setVersionUpdateState({
...newUpdateState,
event: UpdateStateEvent.UPDATE_REQUESTED,
}).then(() => {
AnalyticsUtil.logEvent("VERSION_UPDATE_REQUESTED", {
fromVersion: newUpdateState.currentVersion,
toVersion: newUpdateState.upgradeVersion,
timesShown,
});
// Reload to fetch the latest app version
location.reload();
});
}

export async function handleVersionUpdate(
currentVersionData: AppVersionData,
serverVersion: string,
) {
const { edition, id: currentVersion } = currentVersionData;

// If no version is set, ignore
if (!currentVersion) return;

const versionState: VersionUpdateState | null = await getVersionUpdateState();

if (currentVersion === serverVersion) {
if (versionState) {
AnalyticsUtil.logEvent("VERSION_UPDATE_SUCCESS", {
fromVersion: versionState.currentVersion,
toVersion: versionState.upgradeVersion,
edition,
});
await removeVersionUpdateState();
}
}

if (currentVersion !== serverVersion) {
if (versionState) {
timesShown = versionState.timesShown;

if (
currentVersion === versionState.currentVersion &&
versionState.event === UpdateStateEvent.UPDATE_REQUESTED
) {
AnalyticsUtil.logEvent("VERSION_UPDATED_FAILED", {
fromVersion: versionState.currentVersion,
toVersion: versionState.upgradeVersion,
edition,
});
}
}

const newUpdateState: VersionUpdateState = {
currentVersion,
upgradeVersion: serverVersion,
// Increment the timesShown counter
timesShown: timesShown + 1,
event: UpdateStateEvent.PROMPT_SHOWN,
};

AnalyticsUtil.logEvent("VERSION_UPDATE_SHOWN", {
fromVersion: currentVersion,
toVersion: serverVersion,
timesShown,
});
showPrompt(newUpdateState);
}
}
20 changes: 0 additions & 20 deletions src/utils/storage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import log from "loglevel";
import moment from "moment";
import localforage from "localforage";
import type { VersionUpdateState } from "../sagas/WebsocketSagas/versionUpdatePrompt";
import { isNumber } from "lodash";
import { EditorModes } from "components/editorComponents/CodeEditor/EditorConfig";
import type { EditorViewMode } from "ee/entities/IDE/constants";
Expand Down Expand Up @@ -761,25 +760,6 @@ export const isUserSignedUpFlagSet = async (email: string) => {
}
};

export const setVersionUpdateState = async (state: VersionUpdateState) => {
try {
await store.setItem(STORAGE_KEYS.VERSION_UPDATE_STATE, state);
} catch (e) {
log.error("An error occurred while storing version update state", e);
}
};

export const getVersionUpdateState =
async (): Promise<VersionUpdateState | null> => {
return await store.getItem<VersionUpdateState | null>(
STORAGE_KEYS.VERSION_UPDATE_STATE,
);
};

export const removeVersionUpdateState = async () => {
return store.removeItem(STORAGE_KEYS.VERSION_UPDATE_STATE);
};

export const getAppKbState = async (appId: string) => {
try {
const aiKBApplicationMap: Record<
Expand Down

0 comments on commit d7276e7

Please sign in to comment.