@samitouri / QOS-React / commits / 982cf95c8b

Add --cleanup option to flags script to show groups of flags by status (#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 #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" />

Jack Pope committed Dec 13, 2024 at 15:49 UTC 982cf95c8b1914f87c5b1ea4ed73528e9d5dfb66
1 file changed +174 -43
scripts/flags/flags.js
+174 -43
@@ -62,6 +62,12 @@ const argv = yargs
62 'experimental',
63 ],
64 },
65 + cleanup: {
66 + describe: 'output flags by cleanup category.',
67 + requiresArg: false,
68 + type: 'boolean',
69 + default: false,
70 + },
71 }).argv;
72
73 // Load ReactFeatureFlags with __NEXT_MAJOR__ replaced with 'next'.
@@ -375,53 +381,86 @@ const FLAG_CONFIG = {
381
382 const FLAG_COLUMNS = Object.keys(FLAG_CONFIG);
383
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 +
393 // 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 + }, {});
418
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;
421 }
397 - return acc;
398 - }, {});
422 + }
423
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));
439 }
440 +
441 + return sorted;
442 }
443
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;
456 }
457
458 if (argv.csv) {
459 + const table = buildTable();
460 const csvRows = [
461 `Flag name, ${FLAG_COLUMNS.join(', ')}`,
462 ...Object.keys(table).map(flag => {
424 - const row = sorted[flag];
463 + const row = table[flag];
464 return `${flag}, ${FLAG_COLUMNS.map(col => row[col]).join(', ')}`;
465 }),
466 ];
@@ -433,16 +472,108 @@ if (argv.csv) {
472 });
473 }
474
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 +}
576
444 -// print table with formatting
445 -console.table(padded);
577 console.log(`
578 Legend:
579 ✅ On