diff --git a/frontend/src/component/App.tsx b/frontend/src/component/App.tsx
index 4b3de13696..5ff7d0ab1a 100644
--- a/frontend/src/component/App.tsx
+++ b/frontend/src/component/App.tsx
@@ -21,6 +21,7 @@ import { styled } from '@mui/material';
import { InitialRedirect } from './InitialRedirect';
import { InternalBanners } from './banners/internalBanners/InternalBanners';
import { ExternalBanners } from './banners/externalBanners/ExternalBanners';
+import { EdgeUpgradeBanner } from './banners/EdgeUpgradeBanner/EdgeUpgradeBanner';
import { LicenseBanner } from './banners/internalBanners/LicenseBanner';
import { Demo } from './demo/Demo';
@@ -68,6 +69,7 @@ export const App = () => {
+
diff --git a/frontend/src/component/banners/EdgeUpgradeBanner/EdgeUpgradeBanner.tsx b/frontend/src/component/banners/EdgeUpgradeBanner/EdgeUpgradeBanner.tsx
new file mode 100644
index 0000000000..5c4e33f2e8
--- /dev/null
+++ b/frontend/src/component/banners/EdgeUpgradeBanner/EdgeUpgradeBanner.tsx
@@ -0,0 +1,22 @@
+import { useUiFlag } from '../../../hooks/useUiFlag';
+import { ConditionallyRender } from '../../common/ConditionallyRender/ConditionallyRender';
+import { Banner } from '../Banner/Banner';
+import { IBanner } from '../../../interfaces/banner';
+
+export const EdgeUpgradeBanner = () => {
+ const displayUpgradeEdgeBanner = useUiFlag('displayUpgradeEdgeBanner');
+ const upgradeEdgeBanner: IBanner = {
+ message: `We noticed that you're using an outdated Unleash Edge. To ensure you continue to receive metrics, we recommend upgrading to v17.0.0 or later.`,
+ link: 'https://github.com/Unleash/unleash-edge',
+ linkText: 'Get latest',
+ variant: 'warning',
+ };
+ return (
+ <>
+ }
+ />
+ >
+ );
+};
diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts
index 64805b24b7..3562790a0d 100644
--- a/frontend/src/interfaces/uiConfig.ts
+++ b/frontend/src/interfaces/uiConfig.ts
@@ -79,6 +79,7 @@ export type UiFlags = {
executiveDashboard?: boolean;
changeRequestConflictHandling?: boolean;
feedbackComments?: Variant;
+ displayUpgradeEdgeBanner?: boolean;
};
export interface IVersionInfo {
diff --git a/src/lib/db/client-instance-store.ts b/src/lib/db/client-instance-store.ts
index 00fb718b93..ccdcf388d8 100644
--- a/src/lib/db/client-instance-store.ts
+++ b/src/lib/db/client-instance-store.ts
@@ -166,6 +166,15 @@ export default class ClientInstanceStore implements IClientInstanceStore {
return rows.map(mapRow);
}
+ async getBySdkName(sdkName: string): Promise {
+ const rows = await this.db
+ .select()
+ .from(TABLE)
+ .whereRaw(`sdk_version LIKE '??%'`, [sdkName])
+ .orderBy('last_seen', 'desc');
+ return rows.map(mapRow);
+ }
+
async getDistinctApplications(): Promise {
const rows = await this.db
.distinct('app_name')
diff --git a/src/lib/features/metrics/instance/instance-service.ts b/src/lib/features/metrics/instance/instance-service.ts
index 309de68a28..56339bc325 100644
--- a/src/lib/features/metrics/instance/instance-service.ts
+++ b/src/lib/features/metrics/instance/instance-service.ts
@@ -19,7 +19,7 @@ import { clientMetricsSchema } from '../shared/schema';
import { PartialSome } from '../../../types/partial';
import { IPrivateProjectChecker } from '../../private-project/privateProjectCheckerType';
import { IFlagResolver, SYSTEM_USER } from '../../../types';
-import { ALL_PROJECTS } from '../../../util';
+import { ALL_PROJECTS, parseStrictSemVer } from '../../../util';
import { Logger } from '../../../logger';
export default class ClientInstanceService {
@@ -224,4 +224,24 @@ export default class ClientInstanceService {
async removeInstancesOlderThanTwoDays(): Promise {
return this.clientInstanceStore.removeInstancesOlderThanTwoDays();
}
+
+ async usesSdkOlderThan(
+ sdkName: string,
+ sdkVersion: string,
+ ): Promise {
+ const semver = parseStrictSemVer(sdkVersion);
+ const instancesOfSdk =
+ await this.clientInstanceStore.getBySdkName(sdkName);
+ return instancesOfSdk.some((instance) => {
+ if (instance.sdkVersion) {
+ const [_sdkName, sdkVersion] = instance.sdkVersion.split(':');
+ const instanceUsedSemver = parseStrictSemVer(sdkVersion);
+ return (
+ instanceUsedSemver !== null &&
+ semver !== null &&
+ instanceUsedSemver < semver
+ );
+ }
+ });
+ }
}
diff --git a/src/lib/routes/admin-api/config.test.ts b/src/lib/routes/admin-api/config.test.ts
index 562de5eba1..44e86be8c6 100644
--- a/src/lib/routes/admin-api/config.test.ts
+++ b/src/lib/routes/admin-api/config.test.ts
@@ -9,6 +9,7 @@ import {
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
} from '../../util/segments';
import TestAgent from 'supertest/lib/agent';
+import { IUnleashStores } from '../../types';
const uiConfig = {
headerBackground: 'red',
@@ -28,17 +29,20 @@ async function getSetup() {
return {
base,
+ stores,
request: supertest(app),
};
}
let request: TestAgent;
let base: string;
+let stores: IUnleashStores;
beforeEach(async () => {
const setup = await getSetup();
request = setup.request;
base = setup.base;
+ stores = setup.stores;
});
test('should get ui config', async () => {
@@ -52,3 +56,45 @@ test('should get ui config', async () => {
expect(body.segmentValuesLimit).toEqual(DEFAULT_SEGMENT_VALUES_LIMIT);
expect(body.strategySegmentsLimit).toEqual(DEFAULT_STRATEGY_SEGMENTS_LIMIT);
});
+
+describe('displayUpgradeEdgeBanner', () => {
+ test('ui config should have displayUpgradeEdgeBanner to be set if an instance using edge has been seen', async () => {
+ await stores.clientInstanceStore.insert({
+ appName: 'my-app',
+ instanceId: 'some-instance',
+ sdkVersion: 'unleash-edge:16.0.0',
+ });
+ const { body } = await request
+ .get(`${base}/api/admin/ui-config`)
+ .expect('Content-Type', /json/)
+ .expect(200);
+ expect(body.flags).toBeTruthy();
+ expect(body.flags.displayUpgradeEdgeBanner).toBeTruthy();
+ });
+ test('ui config should not get displayUpgradeEdgeBanner flag if edge >= 17.0.0 has been seen', async () => {
+ await stores.clientInstanceStore.insert({
+ appName: 'my-app',
+ instanceId: 'some-instance',
+ sdkVersion: 'unleash-edge:17.1.0',
+ });
+ const { body } = await request
+ .get(`${base}/api/admin/ui-config`)
+ .expect('Content-Type', /json/)
+ .expect(200);
+ expect(body.flags).toBeTruthy();
+ expect(body.flags.displayUpgradeEdgeBanner).toEqual(false);
+ });
+ test('ui config should not get displayUpgradeEdgeBanner flag if java-client has been seen', async () => {
+ await stores.clientInstanceStore.insert({
+ appName: 'my-app',
+ instanceId: 'some-instance',
+ sdkVersion: 'unleash-client-java:9.1.0',
+ });
+ const { body } = await request
+ .get(`${base}/api/admin/ui-config`)
+ .expect('Content-Type', /json/)
+ .expect(200);
+ expect(body.flags).toBeTruthy();
+ expect(body.flags.displayUpgradeEdgeBanner).toEqual(false);
+ });
+});
diff --git a/src/lib/routes/admin-api/config.ts b/src/lib/routes/admin-api/config.ts
index 9f8cf0ee53..837451e44c 100644
--- a/src/lib/routes/admin-api/config.ts
+++ b/src/lib/routes/admin-api/config.ts
@@ -26,6 +26,9 @@ import { SetUiConfigSchema } from '../../openapi/spec/set-ui-config-schema';
import { createRequestSchema } from '../../openapi/util/create-request-schema';
import { ProxyService } from '../../services';
import MaintenanceService from '../../features/maintenance/maintenance-service';
+import memoizee from 'memoizee';
+import { minutesToMilliseconds } from 'date-fns';
+import ClientInstanceService from '../../features/metrics/instance/instance-service';
class ConfigController extends Controller {
private versionService: VersionService;
@@ -36,8 +39,12 @@ class ConfigController extends Controller {
private emailService: EmailService;
+ private clientInstanceService: ClientInstanceService;
+
private maintenanceService: MaintenanceService;
+ private usesOldEdgeFunction: () => Promise;
+
private readonly openApiService: OpenApiService;
constructor(
@@ -49,6 +56,7 @@ class ConfigController extends Controller {
openApiService,
proxyService,
maintenanceService,
+ clientInstanceService,
}: Pick<
IUnleashServices,
| 'versionService'
@@ -57,6 +65,7 @@ class ConfigController extends Controller {
| 'openApiService'
| 'proxyService'
| 'maintenanceService'
+ | 'clientInstanceService'
>,
) {
super(config);
@@ -66,6 +75,18 @@ class ConfigController extends Controller {
this.openApiService = openApiService;
this.proxyService = proxyService;
this.maintenanceService = maintenanceService;
+ this.clientInstanceService = clientInstanceService;
+ this.usesOldEdgeFunction = memoizee(
+ async () =>
+ this.clientInstanceService.usesSdkOlderThan(
+ 'unleash-edge',
+ '17.0.0',
+ ),
+ {
+ promise: true,
+ maxAge: minutesToMilliseconds(10),
+ },
+ );
this.route({
method: 'get',
@@ -109,14 +130,17 @@ class ConfigController extends Controller {
req: AuthedRequest,
res: Response,
): Promise {
- const [frontendSettings, simpleAuthSettings, maintenanceMode] =
- await Promise.all([
- this.proxyService.getFrontendSettings(false),
- this.settingService.get(
- simpleAuthSettingsKey,
- ),
- this.maintenanceService.isMaintenanceMode(),
- ]);
+ const [
+ frontendSettings,
+ simpleAuthSettings,
+ maintenanceMode,
+ usesOldEdge,
+ ] = await Promise.all([
+ this.proxyService.getFrontendSettings(false),
+ this.settingService.get(simpleAuthSettingsKey),
+ this.maintenanceService.isMaintenanceMode(),
+ this.usesOldEdgeFunction(),
+ ]);
const disablePasswordAuth =
simpleAuthSettings?.disabled ||
@@ -126,7 +150,11 @@ class ConfigController extends Controller {
email: req.user.email,
});
- const flags = { ...this.config.ui.flags, ...expFlags };
+ const flags = {
+ ...this.config.ui.flags,
+ ...expFlags,
+ displayUpgradeEdgeBanner: usesOldEdge,
+ };
const response: UiConfigSchema = {
...this.config.ui,
diff --git a/src/lib/types/stores/client-instance-store.ts b/src/lib/types/stores/client-instance-store.ts
index bae30dc46f..eb4857da24 100644
--- a/src/lib/types/stores/client-instance-store.ts
+++ b/src/lib/types/stores/client-instance-store.ts
@@ -21,6 +21,7 @@ export interface IClientInstanceStore
setLastSeen(INewClientInstance): Promise;
insert(details: INewClientInstance): Promise;
getByAppName(appName: string): Promise;
+ getBySdkName(sdkName: string): Promise;
getDistinctApplications(): Promise;
getDistinctApplicationsCount(daysBefore?: number): Promise;
deleteForApplication(appName: string): Promise;
diff --git a/src/test/fixtures/fake-client-instance-store.ts b/src/test/fixtures/fake-client-instance-store.ts
index 6d7a0cd296..531375d6e2 100644
--- a/src/test/fixtures/fake-client-instance-store.ts
+++ b/src/test/fixtures/fake-client-instance-store.ts
@@ -31,6 +31,14 @@ export default class FakeClientInstanceStore implements IClientInstanceStore {
return;
}
+ async getBySdkName(sdkName: string): Promise {
+ return Promise.resolve(
+ this.instances.filter((instance) =>
+ instance.sdkVersion?.startsWith(sdkName),
+ ),
+ );
+ }
+
async deleteAll(): Promise {
this.instances = [];
}