1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-27 13:49:10 +02:00

chore: implement unknown flags UX feedback (#10519)

https://linear.app/unleash/issue/2-3809/implement-the-latest-feedback-from-ux

Implements the latest feedback from UX regarding **unknown flags**.

Bit unsure about our column headers. E.g. instead of "Last seen" and
"Seen in Unleash" we could call them "Reported" and "Last event".

<img width="1490" height="838" alt="image"
src="https://github.com/user-attachments/assets/30ca2570-1395-429f-8d60-ccc6fe83ba92"
/>
This commit is contained in:
Nuno Góis 2025-08-22 14:04:01 +01:00 committed by GitHub
parent d76b676fb8
commit 885d3e1817
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 139 additions and 93 deletions

View File

@ -1,17 +1,17 @@
import { Tooltip, Typography } from '@mui/material';
import { useLocationSettings } from 'hooks/useLocationSettings';
import type { FC } from 'react';
import type { FC, ReactNode } from 'react';
import { formatDateYMD } from 'utils/formatDate';
import { TextCell } from '../TextCell/TextCell.tsx';
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
import type { ColumnInstance } from 'react-table';
interface ITimeAgoCellProps {
export interface ITimeAgoCellProps {
value?: string | number | Date | null;
column?: ColumnInstance;
live?: boolean;
emptyText?: string;
title?: (date: string) => string;
title?: (date?: string) => ReactNode;
dateFormat?: (value: string | number | Date, locale: string) => string;
}
@ -20,18 +20,19 @@ export const TimeAgoCell: FC<ITimeAgoCellProps> = ({
column,
live = false,
emptyText = 'Never',
title = (date) => (column ? `${column.Header}: ${date}` : date),
title = (date) =>
date ? (column ? `${column.Header}: ${date}` : date) : '',
dateFormat = formatDateYMD,
}) => {
const { locationSettings } = useLocationSettings();
if (!value) return <TextCell>{emptyText}</TextCell>;
const date = dateFormat(value, locationSettings.locale);
const tooltip = value
? title(dateFormat(value, locationSettings.locale))
: title();
return (
<TextCell>
<Tooltip title={title(date)} arrow>
<Tooltip title={tooltip} arrow>
<Typography
noWrap
sx={{
@ -42,7 +43,11 @@ export const TimeAgoCell: FC<ITimeAgoCellProps> = ({
variant='body2'
data-loading
>
<TimeAgo date={value} refresh={live} />
{value ? (
<TimeAgo date={value} refresh={live} />
) : (
emptyText
)}
</Typography>
</Tooltip>
</TextCell>

View File

@ -1,33 +0,0 @@
import { Box, styled } from '@mui/material';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import EventNoteIcon from '@mui/icons-material/EventNote';
import type { UnknownFlag } from './hooks/useUnknownFlags.js';
import { Link } from 'react-router-dom';
const StyledBox = styled(Box)(() => ({
display: 'flex',
justifyContent: 'center',
}));
interface IUnknownFlagsActionsCellProps {
unknownFlag: UnknownFlag;
}
export const UnknownFlagsActionsCell = ({
unknownFlag,
}: IUnknownFlagsActionsCellProps) => (
<StyledBox>
<PermissionIconButton
component={Link}
data-loading
to={`/history?feature=${encodeURIComponent(`IS:${unknownFlag.name}`)}`}
permission={ADMIN}
tooltipProps={{
title: 'See events',
}}
>
<EventNoteIcon />
</PermissionIconButton>
</StyledBox>
);

View File

@ -0,0 +1,37 @@
import type { UnknownFlag } from './hooks/useUnknownFlags.js';
import { Link } from 'react-router-dom';
import {
TimeAgoCell,
type ITimeAgoCellProps,
} from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell.js';
import AccessContext from 'contexts/AccessContext.js';
import { useContext } from 'react';
interface IUnknownFlagsSeenInUnleashCellProps extends ITimeAgoCellProps {
unknownFlag: UnknownFlag;
}
export const UnknownFlagsSeenInUnleashCell = ({
unknownFlag,
...props
}: IUnknownFlagsSeenInUnleashCellProps) => {
const { isAdmin } = useContext(AccessContext);
const value = unknownFlag.lastEventAt;
const title = value
? (date) => `Last event: ${date}`
: () => 'This flag has never existed in Unleash';
const TimeAgo = <TimeAgoCell value={value} title={title} {...props} />;
if (value && isAdmin) {
return (
<Link
to={`/history?feature=${encodeURIComponent(`IS:${unknownFlag.name}`)}`}
>
{TimeAgo}
</Link>
);
}
return TimeAgo;
};

View File

@ -16,7 +16,8 @@ import { formatDateYMDHMS } from 'utils/formatDate.js';
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell.js';
import { useUiFlag } from 'hooks/useUiFlag.js';
import NotFound from 'component/common/NotFound/NotFound.js';
import { UnknownFlagsActionsCell } from './UnknownFlagsActionsCell.js';
import { UnknownFlagsSeenInUnleashCell } from './UnknownFlagsSeenInUnleashCell.js';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon.js';
const StyledAlert = styled(Alert)(({ theme }) => ({
marginBottom: theme.spacing(3),
@ -25,13 +26,12 @@ const StyledAlert = styled(Alert)(({ theme }) => ({
const StyledAlertContent = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
gap: theme.spacing(2),
}));
const StyledUl = styled('ul')(({ theme }) => ({
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1),
}));
const StyledHeader = styled('div')({
display: 'flex',
});
export const UnknownFlagsTable = () => {
const { unknownFlags, loading } = useUnknownFlags();
@ -46,13 +46,14 @@ export const UnknownFlagsTable = () => {
{
Header: 'Flag name',
accessor: 'name',
minWidth: 200,
minWidth: 100,
searchable: true,
},
{
Header: 'Application',
accessor: 'appName',
searchable: true,
minWidth: 100,
},
{
Header: 'Environment',
@ -60,27 +61,47 @@ export const UnknownFlagsTable = () => {
searchable: true,
},
{
Header: 'Last seen',
Header: (
<StyledHeader>
Reported
<HelpIcon
tooltip={`Flags reported when your SDK evaluates them but they don't exist in Unleash`}
size='16px'
/>
</StyledHeader>
),
accessor: 'seenAt',
Cell: ({ value, column }) => (
Cell: ({ value }) => (
<TimeAgoCell
value={value}
column={column}
title={(date) => `Reported: ${date}`}
dateFormat={formatDateYMDHMS}
/>
),
width: 150,
},
{
Header: 'Actions',
id: 'Actions',
align: 'center',
Header: (
<StyledHeader>
Last event
<HelpIcon
tooltip='Events are only logged for feature flags that have been set up in Unleash first'
size='16px'
/>
</StyledHeader>
),
accessor: 'lastEventAt',
Cell: ({
row: { original: unknownFlag },
}: { row: { original: UnknownFlag } }) => (
<UnknownFlagsActionsCell unknownFlag={unknownFlag} />
}: {
row: { original: UnknownFlag };
}) => (
<UnknownFlagsSeenInUnleashCell
unknownFlag={unknownFlag}
dateFormat={formatDateYMDHMS}
/>
),
width: 100,
disableSortBy: true,
width: 150,
},
],
[],
@ -121,7 +142,7 @@ export const UnknownFlagsTable = () => {
isLoading={loading}
header={
<PageHeader
title={`Unknown flags (${rows.length})`}
title={`Unknown flag report (${rows.length})`}
actions={
<>
<ConditionallyRender
@ -154,36 +175,34 @@ export const UnknownFlagsTable = () => {
<StyledAlert severity='info'>
<StyledAlertContent>
<p>
<strong>Unknown flags</strong> are feature flags that
your SDKs tried to evaluate but which Unleash doesn't
recognize. Tracking them helps you catch typos, remove
outdated flags, and keep your code and configuration in
sync. These can include:
</p>
<StyledUl>
<li>
<b>Missing flags</b>: typos or flags referenced in
code that don't exist in Unleash.
</li>
<li>
<b>Invalid flags</b>: flags with malformed or
unexpected names, unsupported by Unleash.
</li>
</StyledUl>
<p>
Each row in the table represents an{' '}
<strong>unknown flag report</strong>, which is a unique
combination of <em>flag name</em>, <em>application</em>,
and <em>environment</em>. The same flag name may appear
multiple times if it's been seen in different
applications or environments.
<b>
Clean up unknown flags to keep your code and
configuration in sync
</b>
<br />
Unknown flags are feature flags that your SDKs tried to
evaluate but which Unleash doesn't recognize.
</p>
<p>
We display up to 1,000 unknown flag reports from the
last 24 hours. Older reports are automatically pruned.
<b>Unknown flags can include:</b>
<ul>
<li>
Missing flags: typos or flags referenced in code
that don't exist in Unleash.
</li>
<li>
Invalid flags: flags with malformed or
unexpected names, unsupported by Unleash.
</li>
</ul>
</p>
<p>
<b>Why do I see the same flag name multiple times?</b>
<br />
The same flag name will appear multiple times if it's
been seen in different applications or environments.
</p>
</StyledAlertContent>
</StyledAlert>

View File

@ -10,6 +10,7 @@ export type UnknownFlag = {
appName: string;
seenAt: Date;
environment: string;
lastEventAt: Date;
};
type UnknownFlagsResponse = {

View File

@ -2,6 +2,7 @@ import type { Db } from '../../../db/db.js';
import type { Logger, LogProvider } from '../../../logger.js';
const TABLE = 'unknown_flags';
const TABLE_EVENTS = 'events';
const MAX_INSERT_BATCH_SIZE = 100;
export type UnknownFlag = {
@ -9,6 +10,7 @@ export type UnknownFlag = {
appName: string;
seenAt: Date;
environment: string;
lastEventAt?: Date;
};
export type QueryParams = {
@ -64,11 +66,17 @@ export class UnknownFlagsStore implements IUnknownFlagsStore {
}
async getAll({ limit, orderBy }: QueryParams = {}): Promise<UnknownFlag[]> {
let query = this.db(TABLE).select(
'name',
'app_name',
'seen_at',
'environment',
let query = this.db(`${TABLE} AS uf`).select(
'uf.name',
'uf.app_name',
'uf.seen_at',
'uf.environment',
this.db.raw(
`(SELECT MAX(e.created_at)
FROM ${TABLE_EVENTS} AS e
WHERE e.feature_name = uf.name)
AS last_event_at`,
),
);
if (orderBy) {
@ -84,8 +92,9 @@ export class UnknownFlagsStore implements IUnknownFlagsStore {
return rows.map((row) => ({
name: row.name,
appName: row.app_name,
seenAt: new Date(row.seen_at),
seenAt: row.seen_at,
environment: row.environment,
lastEventAt: row.last_event_at,
}));
}

View File

@ -31,6 +31,14 @@ export const unknownFlagSchema = {
'The environment in which the unknown flag was reported.',
example: 'production',
},
lastEventAt: {
type: 'string',
format: 'date-time',
description:
'The date and time when the last event for the unknown flag name occurred, if any.',
example: '2023-10-01T12:00:00Z',
nullable: true,
},
},
components: {},
} as const;