1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

task: Add banner encouraging edge upgrade (#6018)

Only triggers if there is any rows in client instances that have

    sdk_version: unleash-edge with version < 17.0.0

The function that checks this memoizes the check for 10 minutes to avoid
scanning the client instances table too often.
This commit is contained in:
Christopher Kolstad 2024-01-24 15:22:48 +01:00 committed by GitHub
parent 3acdfc2cf4
commit 17d826ddf4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 147 additions and 10 deletions

View File

@ -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 = () => {
<LicenseBanner />
<ExternalBanners />
<InternalBanners />
<EdgeUpgradeBanner />
<StyledContainer>
<ToastRenderer />
<Routes>

View File

@ -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 (
<>
<ConditionallyRender
condition={displayUpgradeEdgeBanner}
show={<Banner key={'upgradeEdge'} banner={upgradeEdgeBanner} />}
/>
</>
);
};

View File

@ -79,6 +79,7 @@ export type UiFlags = {
executiveDashboard?: boolean;
changeRequestConflictHandling?: boolean;
feedbackComments?: Variant;
displayUpgradeEdgeBanner?: boolean;
};
export interface IVersionInfo {

View File

@ -166,6 +166,15 @@ export default class ClientInstanceStore implements IClientInstanceStore {
return rows.map(mapRow);
}
async getBySdkName(sdkName: string): Promise<IClientInstance[]> {
const rows = await this.db
.select()
.from(TABLE)
.whereRaw(`sdk_version LIKE '??%'`, [sdkName])
.orderBy('last_seen', 'desc');
return rows.map(mapRow);
}
async getDistinctApplications(): Promise<string[]> {
const rows = await this.db
.distinct('app_name')

View File

@ -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<void> {
return this.clientInstanceStore.removeInstancesOlderThanTwoDays();
}
async usesSdkOlderThan(
sdkName: string,
sdkVersion: string,
): Promise<boolean> {
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
);
}
});
}
}

View File

@ -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<Test>;
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);
});
});

View File

@ -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<boolean>;
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<UiConfigSchema>,
): Promise<void> {
const [frontendSettings, simpleAuthSettings, maintenanceMode] =
await Promise.all([
this.proxyService.getFrontendSettings(false),
this.settingService.get<SimpleAuthSettings>(
simpleAuthSettingsKey,
),
this.maintenanceService.isMaintenanceMode(),
]);
const [
frontendSettings,
simpleAuthSettings,
maintenanceMode,
usesOldEdge,
] = await Promise.all([
this.proxyService.getFrontendSettings(false),
this.settingService.get<SimpleAuthSettings>(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,

View File

@ -21,6 +21,7 @@ export interface IClientInstanceStore
setLastSeen(INewClientInstance): Promise<void>;
insert(details: INewClientInstance): Promise<void>;
getByAppName(appName: string): Promise<IClientInstance[]>;
getBySdkName(sdkName: string): Promise<IClientInstance[]>;
getDistinctApplications(): Promise<string[]>;
getDistinctApplicationsCount(daysBefore?: number): Promise<number>;
deleteForApplication(appName: string): Promise<void>;

View File

@ -31,6 +31,14 @@ export default class FakeClientInstanceStore implements IClientInstanceStore {
return;
}
async getBySdkName(sdkName: string): Promise<IClientInstance[]> {
return Promise.resolve(
this.instances.filter((instance) =>
instance.sdkVersion?.startsWith(sdkName),
),
);
}
async deleteAll(): Promise<void> {
this.instances = [];
}