mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-24 17:51:14 +02:00
chore: one unknown flag per row
This commit is contained in:
parent
829c2c5bc3
commit
a2a7d5b4ff
@ -11,7 +11,7 @@ interface IUnknownFlagsSeenInUnleashCellProps extends ITimeAgoCellProps {
|
||||
unknownFlag: UnknownFlag;
|
||||
}
|
||||
|
||||
export const UnknownFlagsSeenInUnleashCell = ({
|
||||
export const UnknownFlagsLastEventCell = ({
|
||||
unknownFlag,
|
||||
...props
|
||||
}: IUnknownFlagsSeenInUnleashCellProps) => {
|
@ -0,0 +1,128 @@
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
import type { UnknownFlag } from './hooks/useUnknownFlags.js';
|
||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||
import { formatDateYMDHMS } from 'utils/formatDate.js';
|
||||
import { useLocationSettings } from 'hooks/useLocationSettings.js';
|
||||
import { styled } from '@mui/material';
|
||||
import { Highlighter } from 'component/common/Highlighter/Highlighter.js';
|
||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext.js';
|
||||
|
||||
const REPORT_APP_LIMIT = 20;
|
||||
const REPORT_ENV_LIMIT = 10;
|
||||
|
||||
const StyledTooltip = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledReport = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'& > ul': {
|
||||
padding: theme.spacing(0, 3),
|
||||
margin: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
interface IUnknownFlagsLastReportedCellProps {
|
||||
row: { original: UnknownFlag };
|
||||
}
|
||||
|
||||
const UnknownFlagsLastReportedCellTooltip = ({
|
||||
unknownFlag,
|
||||
searchQuery,
|
||||
}: { unknownFlag: UnknownFlag; searchQuery: string }) => {
|
||||
const { locationSettings } = useLocationSettings();
|
||||
const lastReported = formatDateYMDHMS(
|
||||
unknownFlag.lastSeenAt,
|
||||
locationSettings.locale,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledTooltip>
|
||||
Last reported: {lastReported}
|
||||
{unknownFlag.reports
|
||||
.slice(0, REPORT_APP_LIMIT)
|
||||
.map(({ appName, environments }) => (
|
||||
<StyledReport key={appName}>
|
||||
<b>
|
||||
<Highlighter search={searchQuery}>
|
||||
{appName}
|
||||
</Highlighter>
|
||||
</b>
|
||||
<ul>
|
||||
{environments
|
||||
.slice(0, REPORT_ENV_LIMIT)
|
||||
.map(({ environment, seenAt }) => (
|
||||
<li key={environment}>
|
||||
<Highlighter search={searchQuery}>
|
||||
{environment}
|
||||
</Highlighter>
|
||||
:{' '}
|
||||
{formatDateYMDHMS(
|
||||
seenAt,
|
||||
locationSettings.locale,
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
{environments.length > REPORT_ENV_LIMIT && (
|
||||
<li>
|
||||
and {environments.length - REPORT_ENV_LIMIT}{' '}
|
||||
more
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</StyledReport>
|
||||
))}
|
||||
{unknownFlag.reports.length > REPORT_APP_LIMIT && (
|
||||
<span>
|
||||
and {unknownFlag.reports.length - REPORT_APP_LIMIT} more
|
||||
</span>
|
||||
)}
|
||||
</StyledTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const UnknownFlagsLastReportedCell = ({
|
||||
row,
|
||||
}: IUnknownFlagsLastReportedCellProps) => {
|
||||
const { original: unknownFlag } = row;
|
||||
const { searchQuery } = useSearchHighlightContext();
|
||||
|
||||
const searchableAppNames = Array.from(
|
||||
new Set(
|
||||
unknownFlag.reports.map((report) => report.appName.toLowerCase()),
|
||||
),
|
||||
).join('\n');
|
||||
const searchableEnvironments = Array.from(
|
||||
new Set(
|
||||
unknownFlag.reports.flatMap((report) =>
|
||||
report.environments.map((env) => env.environment.toLowerCase()),
|
||||
),
|
||||
),
|
||||
).join('\n');
|
||||
|
||||
return (
|
||||
<TextCell>
|
||||
<TooltipLink
|
||||
tooltip={
|
||||
<UnknownFlagsLastReportedCellTooltip
|
||||
unknownFlag={unknownFlag}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
}
|
||||
highlighted={
|
||||
searchQuery.length > 0 &&
|
||||
(searchableAppNames.includes(searchQuery.toLowerCase()) ||
|
||||
searchableEnvironments.includes(
|
||||
searchQuery.toLowerCase(),
|
||||
))
|
||||
}
|
||||
>
|
||||
<TimeAgo date={unknownFlag.lastSeenAt} />
|
||||
</TooltipLink>
|
||||
</TextCell>
|
||||
);
|
||||
};
|
@ -11,14 +11,14 @@ import { Search } from 'component/common/Search/Search';
|
||||
import { useSearch } from 'hooks/useSearch';
|
||||
import { type UnknownFlag, useUnknownFlags } from './hooks/useUnknownFlags.js';
|
||||
import theme from 'themes/theme.js';
|
||||
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell.js';
|
||||
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 { UnknownFlagsSeenInUnleashCell } from './UnknownFlagsSeenInUnleashCell.js';
|
||||
import { UnknownFlagsLastEventCell } from './UnknownFlagsLastEventCell.js';
|
||||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon.js';
|
||||
import { UnknownFlagsActionsCell } from './UnknownFlagsActionsCell.js';
|
||||
import { UnknownFlagsLastReportedCell } from './UnknownFlagsLastReportedCell.js';
|
||||
|
||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||
marginBottom: theme.spacing(3),
|
||||
@ -51,36 +51,19 @@ export const UnknownFlagsTable = () => {
|
||||
minWidth: 100,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
Header: 'Application',
|
||||
accessor: 'appName',
|
||||
searchable: true,
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
Header: 'Environment',
|
||||
accessor: 'environment',
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
Header: (
|
||||
<StyledHeader>
|
||||
Reported
|
||||
Last reported
|
||||
<HelpIcon
|
||||
tooltip={`Feature flags are reported when your SDK evaluates them and they don't exist in Unleash`}
|
||||
size='16px'
|
||||
/>
|
||||
</StyledHeader>
|
||||
),
|
||||
accessor: 'seenAt',
|
||||
Cell: ({ value }) => (
|
||||
<TimeAgoCell
|
||||
value={value}
|
||||
title={(date) => `Reported: ${date}`}
|
||||
dateFormat={formatDateYMDHMS}
|
||||
/>
|
||||
),
|
||||
width: 150,
|
||||
accessor: 'lastSeenAt',
|
||||
Cell: UnknownFlagsLastReportedCell,
|
||||
width: 170,
|
||||
},
|
||||
{
|
||||
Header: (
|
||||
@ -98,7 +81,7 @@ export const UnknownFlagsTable = () => {
|
||||
}: {
|
||||
row: { original: UnknownFlag };
|
||||
}) => (
|
||||
<UnknownFlagsSeenInUnleashCell
|
||||
<UnknownFlagsLastEventCell
|
||||
unknownFlag={unknownFlag}
|
||||
dateFormat={formatDateYMDHMS}
|
||||
/>
|
||||
@ -116,12 +99,34 @@ export const UnknownFlagsTable = () => {
|
||||
width: 100,
|
||||
disableSortBy: true,
|
||||
},
|
||||
// Always hidden -- for search
|
||||
{
|
||||
accessor: (row: UnknownFlag) =>
|
||||
row.reports.map(({ appName }) => appName).join('\n'),
|
||||
id: 'appNames',
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
accessor: (row: UnknownFlag) =>
|
||||
Array.from(
|
||||
new Set(
|
||||
row.reports.flatMap(({ environments }) =>
|
||||
environments.map(
|
||||
({ environment }) => environment,
|
||||
),
|
||||
),
|
||||
),
|
||||
).join('\n'),
|
||||
id: 'environments',
|
||||
searchable: true,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const [initialState] = useState({
|
||||
sortBy: [{ id: 'name', desc: false }],
|
||||
hiddenColumns: ['appNames', 'environments'],
|
||||
});
|
||||
|
||||
const { data, getSearchText } = useSearch(
|
||||
@ -155,7 +160,7 @@ export const UnknownFlagsTable = () => {
|
||||
isLoading={loading}
|
||||
header={
|
||||
<PageHeader
|
||||
title={`Unknown flag report (${rows.length})`}
|
||||
title={`Unknown flags (${rows.length})`}
|
||||
actions={
|
||||
<>
|
||||
<ConditionallyRender
|
||||
@ -187,7 +192,7 @@ export const UnknownFlagsTable = () => {
|
||||
>
|
||||
<StyledAlert severity='info'>
|
||||
<StyledAlertContent>
|
||||
<p>
|
||||
<div>
|
||||
<b>
|
||||
Clean up unknown flags to keep your code and
|
||||
configuration in sync
|
||||
@ -195,9 +200,9 @@ export const UnknownFlagsTable = () => {
|
||||
<br />
|
||||
Unknown flags are feature flags that your SDKs tried to
|
||||
evaluate but which Unleash doesn't recognize.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<div>
|
||||
<b>Unknown flags can include:</b>
|
||||
<ul>
|
||||
<li>
|
||||
@ -209,14 +214,7 @@ export const UnknownFlagsTable = () => {
|
||||
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>
|
||||
</div>
|
||||
</StyledAlertContent>
|
||||
</StyledAlert>
|
||||
|
||||
|
@ -5,12 +5,21 @@ import { useConditionalSWR } from 'hooks/api/getters/useConditionalSWR/useCondit
|
||||
import handleErrorResponses from 'hooks/api/getters/httpErrorResponseHandler';
|
||||
import type { SWRConfiguration } from 'swr';
|
||||
|
||||
type UnknownFlagEnvReport = {
|
||||
environment: string;
|
||||
seenAt: Date;
|
||||
};
|
||||
|
||||
type UnknownFlagAppReport = {
|
||||
appName: string;
|
||||
environments: UnknownFlagEnvReport[];
|
||||
};
|
||||
|
||||
export type UnknownFlag = {
|
||||
name: string;
|
||||
appName: string;
|
||||
seenAt: Date;
|
||||
environment: string;
|
||||
lastEventAt: Date;
|
||||
lastSeenAt: Date;
|
||||
lastEventAt?: Date;
|
||||
reports: UnknownFlagAppReport[];
|
||||
};
|
||||
|
||||
type UnknownFlagsResponse = {
|
||||
|
@ -32,7 +32,7 @@ import {
|
||||
MetricsTranslator,
|
||||
} from '../impact/metrics-translator.js';
|
||||
import { impactRegister } from '../impact/impact-register.js';
|
||||
import type { UnknownFlag } from '../unknown-flags/unknown-flags-store.js';
|
||||
import type { UnknownFlagReport } from '../unknown-flags/unknown-flags-store.js';
|
||||
|
||||
export default class ClientMetricsServiceV2 {
|
||||
private config: IUnleashConfig;
|
||||
@ -209,14 +209,14 @@ export default class ClientMetricsServiceV2 {
|
||||
`Got ${toggleNames.length} metrics (${invalidCount > 0 ? `${invalidCount} invalid` : 'all valid'}).`,
|
||||
);
|
||||
|
||||
const unknownFlags: UnknownFlag[] = [];
|
||||
const unknownFlags: UnknownFlagReport[] = [];
|
||||
for (const [featureName, group] of metricsByToggle) {
|
||||
if (unknownSet.has(featureName)) {
|
||||
for (const m of group) {
|
||||
unknownFlags.push({
|
||||
name: featureName,
|
||||
appName: m.appName,
|
||||
seenAt: m.timestamp,
|
||||
lastSeenAt: m.timestamp,
|
||||
environment: m.environment,
|
||||
});
|
||||
}
|
||||
|
@ -1,25 +1,67 @@
|
||||
import type {
|
||||
IUnknownFlagsStore,
|
||||
UnknownFlag,
|
||||
UnknownFlagReport,
|
||||
QueryParams,
|
||||
} from './unknown-flags-store.js';
|
||||
|
||||
export class FakeUnknownFlagsStore implements IUnknownFlagsStore {
|
||||
private unknownFlagMap = new Map<string, UnknownFlag>();
|
||||
private unknownFlagMap = new Map<string, UnknownFlagReport>();
|
||||
|
||||
private getKey(flag: UnknownFlag): string {
|
||||
private getKey(flag: UnknownFlagReport): string {
|
||||
return `${flag.name}:${flag.appName}:${flag.environment}`;
|
||||
}
|
||||
|
||||
async insert(flags: UnknownFlag[]): Promise<void> {
|
||||
async insert(flags: UnknownFlagReport[]): Promise<void> {
|
||||
this.unknownFlagMap.clear();
|
||||
for (const flag of flags) {
|
||||
this.unknownFlagMap.set(this.getKey(flag), flag);
|
||||
}
|
||||
}
|
||||
|
||||
private groupFlags(flags: UnknownFlagReport[]): UnknownFlag[] {
|
||||
const byName = new Map<string, Map<string, Map<string, Date>>>();
|
||||
|
||||
for (const f of flags) {
|
||||
const apps =
|
||||
byName.get(f.name) ?? new Map<string, Map<string, Date>>();
|
||||
const envs = apps.get(f.appName) ?? new Map<string, Date>();
|
||||
const prev = envs.get(f.environment);
|
||||
if (!prev || f.lastSeenAt > prev)
|
||||
envs.set(f.environment, f.lastSeenAt);
|
||||
apps.set(f.appName, envs);
|
||||
byName.set(f.name, apps);
|
||||
}
|
||||
|
||||
const out: UnknownFlag[] = [];
|
||||
for (const [name, appsMap] of byName) {
|
||||
let lastSeenAt: Date | null = null;
|
||||
|
||||
const reports = Array.from(appsMap.entries()).map(
|
||||
([appName, envMap]) => {
|
||||
const environments = Array.from(envMap.entries()).map(
|
||||
([environment, seenAt]) => {
|
||||
if (!lastSeenAt || seenAt > lastSeenAt)
|
||||
lastSeenAt = seenAt;
|
||||
return { environment, seenAt };
|
||||
},
|
||||
);
|
||||
return { appName, environments };
|
||||
},
|
||||
);
|
||||
|
||||
out.push({
|
||||
name,
|
||||
lastSeenAt: lastSeenAt ?? new Date(0),
|
||||
reports,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async getAll({ limit, orderBy }: QueryParams = {}): Promise<UnknownFlag[]> {
|
||||
const flags = Array.from(this.unknownFlagMap.values());
|
||||
const flat = Array.from(this.unknownFlagMap.values());
|
||||
const flags = this.groupFlags(flat);
|
||||
if (orderBy) {
|
||||
flags.sort((a, b) => {
|
||||
for (const { column, order } of orderBy) {
|
||||
@ -36,7 +78,7 @@ export class FakeUnknownFlagsStore implements IUnknownFlagsStore {
|
||||
async clear(hoursAgo: number): Promise<void> {
|
||||
const cutoff = Date.now() - hoursAgo * 60 * 60 * 1000;
|
||||
for (const [key, flag] of this.unknownFlagMap.entries()) {
|
||||
if (flag.seenAt.getTime() < cutoff) {
|
||||
if (flag.lastSeenAt.getTime() < cutoff) {
|
||||
this.unknownFlagMap.delete(key);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,11 @@ import type {
|
||||
IUnleashConfig,
|
||||
} from '../../../types/index.js';
|
||||
import type { IUnleashStores } from '../../../types/index.js';
|
||||
import type { QueryParams, UnknownFlag } from './unknown-flags-store.js';
|
||||
import type {
|
||||
QueryParams,
|
||||
UnknownFlag,
|
||||
UnknownFlagReport,
|
||||
} from './unknown-flags-store.js';
|
||||
|
||||
export class UnknownFlagsService {
|
||||
private logger: Logger;
|
||||
@ -14,7 +18,7 @@ export class UnknownFlagsService {
|
||||
|
||||
private unknownFlagsStore: IUnknownFlagsStore;
|
||||
|
||||
private unknownFlagsCache: Map<string, UnknownFlag>;
|
||||
private unknownFlagsCache: Map<string, UnknownFlagReport>;
|
||||
|
||||
constructor(
|
||||
{ unknownFlagsStore }: Pick<IUnleashStores, 'unknownFlagsStore'>,
|
||||
@ -25,14 +29,14 @@ export class UnknownFlagsService {
|
||||
this.logger = config.getLogger(
|
||||
'/features/metrics/unknown-flags/unknown-flags-service.ts',
|
||||
);
|
||||
this.unknownFlagsCache = new Map<string, UnknownFlag>();
|
||||
this.unknownFlagsCache = new Map<string, UnknownFlagReport>();
|
||||
}
|
||||
|
||||
private getKey(flag: UnknownFlag) {
|
||||
private getKey(flag: UnknownFlagReport) {
|
||||
return `${flag.name}:${flag.appName}:${flag.environment}`;
|
||||
}
|
||||
|
||||
register(unknownFlags: UnknownFlag[]) {
|
||||
register(unknownFlags: UnknownFlagReport[]) {
|
||||
if (!this.flagResolver.isEnabled('reportUnknownFlags')) return;
|
||||
for (const flag of unknownFlags) {
|
||||
const key = this.getKey(flag);
|
||||
|
@ -5,12 +5,28 @@ const TABLE = 'unknown_flags';
|
||||
const TABLE_EVENTS = 'events';
|
||||
const MAX_INSERT_BATCH_SIZE = 100;
|
||||
|
||||
type UnknownFlagEnvReport = {
|
||||
environment: string;
|
||||
seenAt: Date;
|
||||
};
|
||||
|
||||
type UnknownFlagAppReport = {
|
||||
appName: string;
|
||||
environments: UnknownFlagEnvReport[];
|
||||
};
|
||||
|
||||
export type UnknownFlag = {
|
||||
name: string;
|
||||
appName: string;
|
||||
seenAt: Date;
|
||||
environment: string;
|
||||
lastSeenAt: Date;
|
||||
lastEventAt?: Date;
|
||||
reports: UnknownFlagAppReport[];
|
||||
};
|
||||
|
||||
export type UnknownFlagReport = {
|
||||
name: string;
|
||||
appName: string;
|
||||
lastSeenAt: Date;
|
||||
environment: string;
|
||||
};
|
||||
|
||||
export type QueryParams = {
|
||||
@ -22,7 +38,7 @@ export type QueryParams = {
|
||||
};
|
||||
|
||||
export interface IUnknownFlagsStore {
|
||||
insert(flags: UnknownFlag[]): Promise<void>;
|
||||
insert(flags: UnknownFlagReport[]): Promise<void>;
|
||||
getAll(params?: QueryParams): Promise<UnknownFlag[]>;
|
||||
clear(hoursAgo: number): Promise<void>;
|
||||
deleteAll(): Promise<void>;
|
||||
@ -39,15 +55,17 @@ export class UnknownFlagsStore implements IUnknownFlagsStore {
|
||||
this.logger = getLogger('unknown-flags-store.ts');
|
||||
}
|
||||
|
||||
async insert(flags: UnknownFlag[]): Promise<void> {
|
||||
async insert(flags: UnknownFlagReport[]): Promise<void> {
|
||||
if (!flags.length) return;
|
||||
|
||||
const rows = flags.map(({ name, appName, seenAt, environment }) => ({
|
||||
name,
|
||||
app_name: appName,
|
||||
seen_at: seenAt,
|
||||
environment,
|
||||
}));
|
||||
const rows = flags.map(
|
||||
({ name, appName, lastSeenAt, environment }) => ({
|
||||
name,
|
||||
app_name: appName,
|
||||
seen_at: lastSeenAt,
|
||||
environment,
|
||||
}),
|
||||
);
|
||||
|
||||
for (let i = 0; i < rows.length; i += MAX_INSERT_BATCH_SIZE) {
|
||||
const chunk = rows.slice(i, i + MAX_INSERT_BATCH_SIZE);
|
||||
@ -66,42 +84,67 @@ export class UnknownFlagsStore implements IUnknownFlagsStore {
|
||||
}
|
||||
|
||||
async getAll({ limit, orderBy }: QueryParams = {}): Promise<UnknownFlag[]> {
|
||||
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`,
|
||||
),
|
||||
const base = this.db
|
||||
.with('base', (qb) =>
|
||||
qb
|
||||
.from(`${TABLE} as uf`)
|
||||
.leftJoin('features as f', 'f.name', 'uf.name')
|
||||
.whereNull('f.name')
|
||||
.select('uf.name', 'uf.app_name', 'uf.environment')
|
||||
.max({ seen_at: 'uf.seen_at' })
|
||||
.groupBy('uf.name', 'uf.app_name', 'uf.environment'),
|
||||
)
|
||||
.whereNotExists(
|
||||
this.db('features as f')
|
||||
.select(this.db.raw('1'))
|
||||
.whereRaw('f.name = uf.name'),
|
||||
.select(
|
||||
'b.name',
|
||||
this.db.raw('MAX(b.seen_at) as last_seen_at'),
|
||||
this.db.raw(
|
||||
`(SELECT MAX(e.created_at) FROM ${TABLE_EVENTS} e WHERE e.feature_name = b.name) as last_event_at`,
|
||||
),
|
||||
this.db.raw(`
|
||||
jsonb_object_agg(
|
||||
b.app_name,
|
||||
(
|
||||
SELECT jsonb_object_agg(env_row.environment, env_row.seen_at)
|
||||
FROM (
|
||||
SELECT environment, MAX(seen_at) AS seen_at
|
||||
FROM base
|
||||
WHERE name = b.name AND app_name = b.app_name
|
||||
GROUP BY environment
|
||||
) env_row
|
||||
)
|
||||
) as reports
|
||||
`),
|
||||
)
|
||||
.from('base as b')
|
||||
.groupBy('b.name');
|
||||
|
||||
let q = base;
|
||||
if (orderBy) q = q.orderBy(orderBy);
|
||||
if (limit) q = q.limit(limit);
|
||||
|
||||
const rows = await q;
|
||||
|
||||
return rows.map((r) => {
|
||||
const reportsObj = r.reports ?? {};
|
||||
const reports = Object.entries(reportsObj).map(
|
||||
([appName, envs]) => ({
|
||||
appName,
|
||||
environments: Object.entries(
|
||||
envs as Record<string, Date>,
|
||||
).map(([environment, seenAt]) => ({
|
||||
environment,
|
||||
seenAt: new Date(seenAt),
|
||||
})),
|
||||
}),
|
||||
);
|
||||
|
||||
if (orderBy) {
|
||||
query = query.orderBy(orderBy);
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
query = query.limit(limit);
|
||||
}
|
||||
|
||||
const rows = await query;
|
||||
|
||||
return rows.map((row) => ({
|
||||
name: row.name,
|
||||
appName: row.app_name,
|
||||
seenAt: row.seen_at,
|
||||
environment: row.environment,
|
||||
lastEventAt: row.last_event_at,
|
||||
}));
|
||||
return {
|
||||
name: r.name,
|
||||
lastSeenAt: r.last_seen_at,
|
||||
lastEventAt: r.last_event_at,
|
||||
reports,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async clear(hoursAgo: number): Promise<void> {
|
||||
|
@ -105,9 +105,18 @@ describe('should register unknown flags', () => {
|
||||
expect(unknownFlags).toHaveLength(1);
|
||||
expect(unknownFlags[0]).toMatchObject({
|
||||
name: 'unknown_flag',
|
||||
environment: 'development',
|
||||
appName: 'demo',
|
||||
seenAt: expect.any(Date),
|
||||
lastSeenAt: expect.any(Date),
|
||||
reports: [
|
||||
{
|
||||
appName: 'demo',
|
||||
environments: [
|
||||
{
|
||||
environment: 'development',
|
||||
seenAt: expect.any(Date),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(eventBus.emit).toHaveBeenCalledWith(
|
||||
CLIENT_METRICS,
|
||||
@ -167,9 +176,18 @@ describe('should register unknown flags', () => {
|
||||
expect(unknownFlags).toHaveLength(1);
|
||||
expect(unknownFlags[0]).toMatchObject({
|
||||
name: 'unknown_flag_bulk',
|
||||
environment: 'development',
|
||||
appName: 'demo',
|
||||
seenAt: expect.any(Date),
|
||||
lastSeenAt: expect.any(Date),
|
||||
reports: [
|
||||
{
|
||||
appName: 'demo',
|
||||
environments: [
|
||||
{
|
||||
environment: 'development',
|
||||
seenAt: expect.any(Date),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(eventBus.emit).toHaveBeenCalledWith(
|
||||
CLIENT_METRICS,
|
||||
@ -242,15 +260,35 @@ describe('should fetch unknown flags', () => {
|
||||
expect(res.body.unknownFlags).toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'unknown_flag_1',
|
||||
environment: 'development',
|
||||
appName: 'demo',
|
||||
lastSeenAt: expect.any(String),
|
||||
lastEventAt: null,
|
||||
reports: [
|
||||
{
|
||||
appName: 'demo',
|
||||
environments: [
|
||||
{
|
||||
environment: 'development',
|
||||
seenAt: expect.any(String),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: 'unknown_flag_2',
|
||||
environment: 'development',
|
||||
appName: 'demo',
|
||||
lastSeenAt: expect.any(String),
|
||||
lastEventAt: null,
|
||||
reports: [
|
||||
{
|
||||
appName: 'demo',
|
||||
environments: [
|
||||
{
|
||||
environment: 'development',
|
||||
seenAt: expect.any(String),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
@ -312,9 +350,18 @@ describe('should fetch unknown flags', () => {
|
||||
expect(res.body.unknownFlags).toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'unknown_flag_2',
|
||||
environment: 'development',
|
||||
appName: 'demo',
|
||||
lastEventAt: null,
|
||||
lastSeenAt: expect.any(String),
|
||||
reports: [
|
||||
{
|
||||
appName: 'demo',
|
||||
environments: [
|
||||
{
|
||||
environment: 'development',
|
||||
seenAt: expect.any(String),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
@ -4,7 +4,7 @@ export const unknownFlagSchema = {
|
||||
$id: '#/components/schemas/unknownFlagSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['name', 'appName', 'seenAt', 'environment'],
|
||||
required: ['name', 'lastSeenAt'],
|
||||
description: 'An unknown flag report',
|
||||
properties: {
|
||||
name: {
|
||||
@ -12,25 +12,13 @@ export const unknownFlagSchema = {
|
||||
description: 'The name of the unknown flag.',
|
||||
example: 'my-unknown-flag',
|
||||
},
|
||||
appName: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The name of the application that reported the unknown flag.',
|
||||
example: 'my-app',
|
||||
},
|
||||
seenAt: {
|
||||
lastSeenAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description:
|
||||
'The date and time when the unknown flag was reported.',
|
||||
'The date and time when the unknown flag was last reported.',
|
||||
example: '2023-10-01T12:00:00Z',
|
||||
},
|
||||
environment: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The environment in which the unknown flag was reported.',
|
||||
example: 'production',
|
||||
},
|
||||
lastEventAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
@ -39,6 +27,48 @@ export const unknownFlagSchema = {
|
||||
example: '2023-10-01T12:00:00Z',
|
||||
nullable: true,
|
||||
},
|
||||
reports: {
|
||||
type: 'array',
|
||||
description: 'The list of reports for this unknown flag.',
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['appName', 'environments'],
|
||||
properties: {
|
||||
appName: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The name of the application that reported the unknown flag.',
|
||||
example: 'my-app',
|
||||
},
|
||||
environments: {
|
||||
type: 'array',
|
||||
description:
|
||||
'The list of environments where this application reported the unknown flag.',
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['environment', 'seenAt'],
|
||||
properties: {
|
||||
environment: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The environment in which the unknown flag was reported.',
|
||||
example: 'production',
|
||||
},
|
||||
seenAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description:
|
||||
'The date and time when the unknown flag was last seen in this environment.',
|
||||
example: '2023-10-01T12:00:00Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
Loading…
Reference in New Issue
Block a user