Skip to content

Commit 982cf95

Browse files
authored
Add --cleanup option to flags script to show groups of flags by status (facebook#31762)
`yarn flags --cleanup` will categorize flags to make it more clear which ones may need to be cleaned up, experiments checked on, or are blocked by internal rollouts. Alternative to facebook#31760 <img width="787" alt="Screenshot 2024-12-13 at 2 31 30 PM" src="https://github.com/user-attachments/assets/452aee7e-9caf-4210-a621-53941d59cb2b" />
1 parent 08dfd0b commit 982cf95

File tree

1 file changed

+174
-43
lines changed

1 file changed

+174
-43
lines changed

scripts/flags/flags.js

+174-43
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ const argv = yargs
6262
'experimental',
6363
],
6464
},
65+
cleanup: {
66+
describe: 'output flags by cleanup category.',
67+
requiresArg: false,
68+
type: 'boolean',
69+
default: false,
70+
},
6571
}).argv;
6672

6773
// Load ReactFeatureFlags with __NEXT_MAJOR__ replaced with 'next'.
@@ -375,53 +381,86 @@ const FLAG_CONFIG = {
375381

376382
const FLAG_COLUMNS = Object.keys(FLAG_CONFIG);
377383

384+
const INTERNAL_VARIANTS = ['WWW Classic', 'WWW Modern', 'RN FB'];
385+
const OSS_VARIANTS = [
386+
'OSS Next Major',
387+
'OSS Canary',
388+
'OSS Experimental',
389+
'RN OSS',
390+
'RN Next Major',
391+
];
392+
378393
// Build the table with the value for each flag.
379-
const isDiff = argv.diff != null && argv.diff.length > 1;
380-
const table = {};
381-
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
382-
for (const flag of allFlagsUniqueFlags) {
383-
const values = FLAG_COLUMNS.reduce((acc, key) => {
384-
acc[key] = FLAG_CONFIG[key](flag);
385-
return acc;
386-
}, {});
387-
388-
if (!isDiff) {
389-
table[flag] = values;
390-
continue;
391-
}
394+
function buildTable(filterFn) {
395+
const isDiff = argv.diff != null && argv.diff.length > 1;
396+
const table = {};
397+
const filteredFlags = filterFn
398+
? allFlagsUniqueFlags.filter(filterFn)
399+
: allFlagsUniqueFlags;
400+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
401+
for (const flag of filteredFlags) {
402+
const values = FLAG_COLUMNS.reduce((acc, key) => {
403+
acc[key] = FLAG_CONFIG[key](flag);
404+
return acc;
405+
}, {});
406+
407+
if (!isDiff) {
408+
table[flag] = values;
409+
continue;
410+
}
411+
412+
const subset = argv.diff.map(argToHeader).reduce((acc, key) => {
413+
if (key in values) {
414+
acc[key] = values[key];
415+
}
416+
return acc;
417+
}, {});
392418

393-
const subset = argv.diff.map(argToHeader).reduce((acc, key) => {
394-
if (key in values) {
395-
acc[key] = values[key];
419+
if (new Set(Object.values(subset)).size !== 1) {
420+
table[flag] = subset;
396421
}
397-
return acc;
398-
}, {});
422+
}
399423

400-
if (new Set(Object.values(subset)).size !== 1) {
401-
table[flag] = subset;
424+
// Sort the table
425+
let sorted = table;
426+
if (isDiff || argv.sort) {
427+
const sortChannel = argToHeader(isDiff ? argv.diff[0] : argv.sort);
428+
const sortBy =
429+
sortChannel === 'flag'
430+
? ([flagA], [flagB]) => {
431+
return flagA.localeCompare(flagB);
432+
}
433+
: ([, rowA], [, rowB]) => {
434+
return rowB[sortChannel]
435+
.toString()
436+
.localeCompare(rowA[sortChannel]);
437+
};
438+
sorted = Object.fromEntries(Object.entries(table).sort(sortBy));
402439
}
440+
441+
return sorted;
403442
}
404443

405-
// Sort the table
406-
let sorted = table;
407-
if (isDiff || argv.sort) {
408-
const sortChannel = argToHeader(isDiff ? argv.diff[0] : argv.sort);
409-
const sortBy =
410-
sortChannel === 'flag'
411-
? ([flagA], [flagB]) => {
412-
return flagA.localeCompare(flagB);
413-
}
414-
: ([, rowA], [, rowB]) => {
415-
return rowB[sortChannel].toString().localeCompare(rowA[sortChannel]);
416-
};
417-
sorted = Object.fromEntries(Object.entries(table).sort(sortBy));
444+
function formatTable(tableData) {
445+
// left align the flag names.
446+
const maxLength = Math.max(
447+
...Object.keys(tableData).map(item => item.length)
448+
);
449+
const padded = {};
450+
Object.keys(tableData).forEach(key => {
451+
const newKey = key.padEnd(maxLength, ' ');
452+
padded[newKey] = tableData[key];
453+
});
454+
455+
return padded;
418456
}
419457

420458
if (argv.csv) {
459+
const table = buildTable();
421460
const csvRows = [
422461
`Flag name, ${FLAG_COLUMNS.join(', ')}`,
423462
...Object.keys(table).map(flag => {
424-
const row = sorted[flag];
463+
const row = table[flag];
425464
return `${flag}, ${FLAG_COLUMNS.map(col => row[col]).join(', ')}`;
426465
}),
427466
];
@@ -433,16 +472,108 @@ if (argv.csv) {
433472
});
434473
}
435474

436-
// left align the flag names.
437-
const maxLength = Math.max(...Object.keys(sorted).map(item => item.length));
438-
const padded = {};
439-
Object.keys(sorted).forEach(key => {
440-
const newKey = key.padEnd(maxLength, ' ');
441-
padded[newKey] = sorted[key];
442-
});
475+
if (argv.cleanup) {
476+
const allPassingFlags = [];
477+
const allFailingFlags = [];
478+
const needsShippedExperimentFlags = [];
479+
const earlyExperimentationFlags = [];
480+
const internalOnlyFlags = [];
481+
482+
const diffedFlagColumns =
483+
argv.diff[0] != null ? argv.diff.map(argToHeader) : FLAG_COLUMNS;
484+
485+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
486+
for (const flag of allFlagsUniqueFlags) {
487+
const values = diffedFlagColumns.reduce((acc, key) => {
488+
acc[key] = FLAG_CONFIG[key](flag);
489+
return acc;
490+
}, {});
491+
492+
const uniqueValues = new Set(Object.values(values));
493+
494+
if (
495+
uniqueValues.size === 1 &&
496+
(uniqueValues.has('✅') ||
497+
typeof uniqueValues.values().next().value === 'number')
498+
) {
499+
allPassingFlags.push(flag);
500+
}
501+
502+
if (uniqueValues.size === 1 && uniqueValues.has('❌')) {
503+
allFailingFlags.push(flag);
504+
}
505+
506+
const internalVariantValues = INTERNAL_VARIANTS.filter(value =>
507+
diffedFlagColumns.includes(value)
508+
).map(v => values[v]);
509+
const ossVariantValues = OSS_VARIANTS.filter(value =>
510+
diffedFlagColumns.includes(value)
511+
).map(v => values[v]);
512+
513+
if (
514+
internalVariantValues.some(v => v === '✅') &&
515+
ossVariantValues.every(v => v === '❌')
516+
) {
517+
internalOnlyFlags.push(flag);
518+
}
519+
520+
if (
521+
internalVariantValues.some(v => v === '🧪') &&
522+
(ossVariantValues.every(v => v === '❌') ||
523+
(ossVariantValues.some(v => v === '❌') &&
524+
values['OSS Experimental'] === '✅'))
525+
) {
526+
earlyExperimentationFlags.push(flag);
527+
}
528+
529+
if (
530+
internalVariantValues.some(v => v === '🧪' || v === '❌') &&
531+
ossVariantValues.every(v => v === '✅')
532+
) {
533+
needsShippedExperimentFlags.push(flag);
534+
}
535+
}
536+
537+
if (allPassingFlags.length > 0) {
538+
console.log('ALL VARIANTS PASS (✅)');
539+
console.table(
540+
formatTable(buildTable(flag => allPassingFlags.includes(flag)))
541+
);
542+
}
543+
544+
if (allFailingFlags.length > 0) {
545+
console.log('ALL VARIANTS FAIL (❌)');
546+
console.table(
547+
formatTable(buildTable(flag => allFailingFlags.includes(flag)))
548+
);
549+
}
550+
551+
if (internalOnlyFlags.length > 0) {
552+
console.log('INTERNAL ONLY (✅)');
553+
console.table(
554+
formatTable(buildTable(flag => internalOnlyFlags.includes(flag)))
555+
);
556+
}
557+
558+
if (earlyExperimentationFlags.length > 0) {
559+
console.log('WAITING ON RESULTS (🧪)');
560+
console.table(
561+
formatTable(buildTable(flag => earlyExperimentationFlags.includes(flag)))
562+
);
563+
}
564+
565+
if (needsShippedExperimentFlags.length > 0) {
566+
console.log('WAITING ON ROLLOUT (🧪)');
567+
console.table(
568+
formatTable(
569+
buildTable(flag => needsShippedExperimentFlags.includes(flag))
570+
)
571+
);
572+
}
573+
} else {
574+
console.table(formatTable(buildTable()));
575+
}
443576

444-
// print table with formatting
445-
console.table(padded);
446577
console.log(`
447578
Legend:
448579
✅ On

0 commit comments

Comments
 (0)