Skip to content

Commit

Permalink
fix: update potentially-stale status dynamically (#4905)
Browse files Browse the repository at this point in the history
Fixes 2 bugs:

- project-health-service keeping the feature types as an instance
variable and only updating it once was preventing real calculation to
happen if the lifetime value changed for a feature toggle type
- the ui was reading from a predefined map for the lifetime values so
they would never reflect the BE change

Closes #
[SR-66](https://linear.app/unleash/issue/SR-66/slack-question-around-potentially-stale-and-its-uses)

<img width="1680" alt="Screenshot 2023-10-02 at 14 37 17"
src="https://github.com/Unleash/unleash/assets/104830839/7bee8d4a-9054-4214-a1a2-11ad8169c3d5">
<img width="1660" alt="Screenshot 2023-10-02 at 14 37 06"
src="https://github.com/Unleash/unleash/assets/104830839/23bf55c7-a380-4423-a732-205ad81d5c3c">

---------

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
  • Loading branch information
andreas-unleash committed Oct 4, 2023
1 parent bd8b54b commit b07c032
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 44 deletions.
@@ -1,23 +1,32 @@
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
import { PERMISSION, KILLSWITCH } from 'constants/featureToggleTypes';
import { getDiffInDays, expired, toggleExpiryByTypeMap } from '../utils';
import { subDays, parseISO } from 'date-fns';
import { KILLSWITCH, PERMISSION } from 'constants/featureToggleTypes';
import { expired, getDiffInDays } from '../utils';
import { parseISO, subDays } from 'date-fns';
import { FeatureTypeSchema } from 'openapi';

export const formatExpiredAt = (
feature: IFeatureToggleListItem,
featureTypes: FeatureTypeSchema[],
): string | undefined => {
const { type, createdAt } = feature;

if (type === KILLSWITCH || type === PERMISSION) {
const featureType = featureTypes.find(
(featureType) => featureType.name === type,
);

if (
featureType &&
(featureType.name === KILLSWITCH || featureType.name === PERMISSION)
) {
return;
}

const date = parseISO(createdAt);
const now = new Date();
const diff = getDiffInDays(date, now);

if (expired(diff, type)) {
const result = diff - toggleExpiryByTypeMap[type];
if (featureType && expired(diff, featureType)) {
const result = diff - (featureType?.lifetimeDays?.valueOf() || 0);
return subDays(now, result).toISOString();
}

Expand Down
@@ -1,19 +1,30 @@
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
import { getDiffInDays, expired } from '../utils';
import { PERMISSION, KILLSWITCH } from 'constants/featureToggleTypes';
import { expired, getDiffInDays } from '../utils';
import { KILLSWITCH, PERMISSION } from 'constants/featureToggleTypes';
import { parseISO } from 'date-fns';
import { FeatureTypeSchema } from 'openapi';

export type ReportingStatus = 'potentially-stale' | 'healthy';

export const formatStatus = (
feature: IFeatureToggleListItem,
featureTypes: FeatureTypeSchema[],
): ReportingStatus => {
const { type, createdAt } = feature;

const featureType = featureTypes.find(
(featureType) => featureType.name === type,
);
const date = parseISO(createdAt);
const now = new Date();
const diff = getDiffInDays(date, now);

if (expired(diff, type) && type !== KILLSWITCH && type !== PERMISSION) {
if (
featureType &&
expired(diff, featureType) &&
type !== KILLSWITCH &&
type !== PERMISSION
) {
return 'potentially-stale';
}

Expand Down
Expand Up @@ -9,10 +9,10 @@ import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightC
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { sortTypes } from 'utils/sortTypes';
import {
useSortBy,
useFlexLayout,
useGlobalFilter,
useSortBy,
useTable,
useFlexLayout,
} from 'react-table';
import { useMediaQuery, useTheme } from '@mui/material';
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
Expand All @@ -29,6 +29,7 @@ import { formatExpiredAt } from './ReportExpiredCell/formatExpiredAt';
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';

interface IReportTableProps {
projectId: string;
Expand Down Expand Up @@ -56,6 +57,7 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => {
const showEnvironmentLastSeen = Boolean(
uiConfig.flags.lastSeenByEnvironment,
);
const { featureTypes } = useFeatureTypes();

const data: IReportTableRow[] = useMemo<IReportTableRow[]>(
() =>
Expand All @@ -65,10 +67,10 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => {
type: report.type,
stale: report.stale,
environments: report.environments,
status: formatStatus(report),
status: formatStatus(report, featureTypes),
lastSeenAt: report.lastSeenAt,
createdAt: report.createdAt,
expiredAt: formatExpiredAt(report),
expiredAt: formatExpiredAt(report, featureTypes),
})),
[projectId, features],
);
Expand Down
@@ -1,19 +1,12 @@
import differenceInDays from 'date-fns/differenceInDays';
import { EXPERIMENT, OPERATIONAL, RELEASE } from 'constants/featureToggleTypes';

const FORTY_DAYS = 40;
const SEVEN_DAYS = 7;

export const toggleExpiryByTypeMap: Record<string, number> = {
[EXPERIMENT]: FORTY_DAYS,
[RELEASE]: FORTY_DAYS,
[OPERATIONAL]: SEVEN_DAYS,
};
import { FeatureTypeSchema } from 'openapi';

export const getDiffInDays = (date: Date, now: Date) => {
return Math.abs(differenceInDays(date, now));
};

export const expired = (diff: number, type: string) => {
return diff >= toggleExpiryByTypeMap[type];
export const expired = (diff: number, type: FeatureTypeSchema) => {
if (type.lifetimeDays) return diff >= type?.lifetimeDays?.valueOf();

return false;
};
@@ -1,8 +1,8 @@
import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react';
import { useEffect, useState } from 'react';
import { formatApiPath } from 'utils/formatPath';
import { IFeatureType } from 'interfaces/featureTypes';
import handleErrorResponses from '../httpErrorResponseHandler';
import { FeatureTypeSchema } from '../../../../openapi';

const useFeatureTypes = (options: SWRConfiguration = {}) => {
const fetcher = async () => {
Expand All @@ -27,7 +27,7 @@ const useFeatureTypes = (options: SWRConfiguration = {}) => {
}, [data, error]);

return {
featureTypes: (data?.types as IFeatureType[]) || [],
featureTypes: (data?.types as FeatureTypeSchema[]) || [],
error,
loading,
refetch,
Expand Down
22 changes: 6 additions & 16 deletions src/lib/services/project-health-service.ts
Expand Up @@ -3,15 +3,12 @@ import { IUnleashConfig } from '../types/option';
import { Logger } from '../logger';
import type { IProject, IProjectHealthReport } from '../types/model';
import type { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
import type {
IFeatureType,
IFeatureTypeStore,
} from '../types/stores/feature-type-store';
import type { IFeatureTypeStore } from '../types/stores/feature-type-store';
import type { IProjectStore } from '../types/stores/project-store';
import ProjectService from './project-service';
import {
calculateProjectHealth,
calculateHealthRating,
calculateProjectHealth,
} from '../domain/project-health/project-health';

export default class ProjectHealthService {
Expand All @@ -23,8 +20,6 @@ export default class ProjectHealthService {

private featureToggleStore: IFeatureToggleStore;

private featureTypes: IFeatureType[];

private projectService: ProjectService;

constructor(
Expand All @@ -43,17 +38,14 @@ export default class ProjectHealthService {
this.projectStore = projectStore;
this.featureTypeStore = featureTypeStore;
this.featureToggleStore = featureToggleStore;
this.featureTypes = [];

this.projectService = projectService;
}

async getProjectHealthReport(
projectId: string,
): Promise<IProjectHealthReport> {
if (this.featureTypes.length === 0) {
this.featureTypes = await this.featureTypeStore.getAll();
}
const featureTypes = await this.featureTypeStore.getAll();

const overview = await this.projectService.getProjectOverview(
projectId,
Expand All @@ -63,7 +55,7 @@ export default class ProjectHealthService {

const healthRating = calculateProjectHealth(
overview.features,
this.featureTypes,
featureTypes,
);

return {
Expand All @@ -73,16 +65,14 @@ export default class ProjectHealthService {
}

async calculateHealthRating(project: IProject): Promise<number> {
if (this.featureTypes.length === 0) {
this.featureTypes = await this.featureTypeStore.getAll();
}
const featureTypes = await this.featureTypeStore.getAll();

const toggles = await this.featureToggleStore.getAll({
project: project.id,
archived: false,
});

return calculateHealthRating(toggles, this.featureTypes);
return calculateHealthRating(toggles, featureTypes);
}

async setHealthRating(): Promise<void> {
Expand Down

0 comments on commit b07c032

Please sign in to comment.