Repository: mozilla/bugbug

Issue:
MEI: Calculate the time to burn down all the defects backlog
Based on [this doc](https://docs.google.com/document/d/1nWP3DtSWneYT8ietXuBa6uPszMKMgICXf3b2dwMVXYk/edit#heading=h.9joq5dbpnw41).

Estimate the time needed to burn down the defects backlog based on the past velocity.

```
If WeighedClosedDefects(TimeInterval) > WeighedOpenedDefects(TimeInterval):
    WeighedBurndownTime(TimeInterval) := Duration(TimeInterval) * (WeighedOpenDefects(Now) / (WeighedClosedDefects(TimeInterval) - WeighedOpenedDefects(TimeInterval)))
Else:
    WeighedBurndownTime(TimeInterval) := Infinity

If NumClosedDefects(TimeInterval) > NumOpenedDefects(TimeInterval):
    NumDefectsBurndownTime(TimeInterval) := Duration(TimeInterval) * (NumOpenDefects(Now) / (NumClosedDefects(TimeInterval) - NumOpenedDefects(TimeInterval)))
Else:
    NumDefectsBurndownTime(TimeInterval) := Infinity
```

We calculate two variants to compare the difference between weighed and unweighed defect numbers as it is not clear if there is a strong relationship between "being impactful" (high severity) and "being expensive" (much effort to be fixed/closed).



Discussion:

Resolving patch:
diff --git a/bugbug/bugzilla.py b/bugbug/bugzilla.py
--- a/bugbug/bugzilla.py
+++ b/bugbug/bugzilla.py
@@ -5,6 +5,7 @@
 
 import collections
 import csv
+import math
 import re
 from datetime import datetime
 from logging import INFO, basicConfig, getLogger
@@ -444,6 +445,7 @@ def calculate_maintenance_effectiveness_indicator(
     components=None,
 ):
     data: dict[str, dict[str, int]] = {
+        "open": {},
         "opened": {},
         "closed": {},
     }
@@ -460,8 +462,6 @@ def calculate_maintenance_effectiveness_indicator(
             "count_only": 1,
             "type": "defect",
             "team_name": team,
-            "chfieldfrom": from_date.strftime("%Y-%m-%d"),
-            "chfieldto": to_date.strftime("%Y-%m-%d"),
         }
 
         if severity != "--":
@@ -470,8 +470,24 @@ def calculate_maintenance_effectiveness_indicator(
         if components is not None:
             params["component"] = components
 
-        for query_type in ("opened", "closed"):
-            if query_type == "opened":
+        for query_type in data.keys():
+            if query_type in ("opened", "closed"):
+                params.update(
+                    {
+                        "chfieldfrom": from_date.strftime("%Y-%m-%d"),
+                        "chfieldto": to_date.strftime("%Y-%m-%d"),
+                    }
+                )
+
+            if query_type == "open":
+                params.update(
+                    {
+                        "f1": "resolution",
+                        "o1": "equals",
+                        "v1": "---",
+                    }
+                )
+            elif query_type == "opened":
                 params["chfield"] = "[Bug creation]"
             elif query_type == "closed":
                 params.update(
@@ -493,17 +509,21 @@ def calculate_maintenance_effectiveness_indicator(
             data[query_type][severity] = r.json()["bug_count"]
 
     # Calculate number of bugs without severity set.
-    for query_type in ("opened", "closed"):
+    for query_type in data.keys():
         data[query_type]["--"] = data[query_type]["--"] - sum(
             data[query_type][s]
             for s in MAINTENANCE_EFFECTIVENESS_SEVERITY_WEIGHTS.keys()
             if s != "--"
         )
 
+    open_defects = sum(data["open"].values())
+    opened_defects = sum(data["opened"].values())
+    closed_defects = sum(data["closed"].values())
+
     print("Before applying weights:")
     print(data)
 
-    for query_type in ("opened", "closed"):
+    for query_type in data.keys():
         # Apply weights.
         for (
             severity,
@@ -514,9 +534,31 @@ def calculate_maintenance_effectiveness_indicator(
     print("After applying weights:")
     print(data)
 
+    weighed_open_defects = sum(data["open"].values())
     weighed_opened_defects = sum(data["opened"].values())
     weighed_closed_defects = sum(data["closed"].values())
+
     if weighed_opened_defects > 0:
-        return 100 * weighed_closed_defects / weighed_opened_defects
+        mei = 100 * weighed_closed_defects / weighed_opened_defects
+    else:
+        mei = 100 * (weighed_closed_defects + 1)
+
+    duration = (to_date - from_date).total_seconds() / 31536000
+
+    if closed_defects > opened_defects:
+        bdtime = duration * (open_defects / (closed_defects - opened_defects))
     else:
-        return 100 * (weighed_closed_defects + 1)
+        bdtime = math.inf
+
+    if weighed_closed_defects > weighed_opened_defects:
+        wbdtime = duration * (
+            weighed_open_defects / (weighed_closed_defects - weighed_opened_defects)
+        )
+    else:
+        wbdtime = math.inf
+
+    return {
+        "ME%": mei,
+        "BDTime": bdtime,
+        "WBDTime": wbdtime,
+    }
diff --git a/scripts/generate_landings_risk_report.py b/scripts/generate_landings_risk_report.py
--- a/scripts/generate_landings_risk_report.py
+++ b/scripts/generate_landings_risk_report.py
@@ -1786,12 +1786,16 @@ def calculate_maintenance_effectiveness(period):
                     2,
                 )
 
+            def format_maintenance_effectiveness(period):
+                me = calculate_maintenance_effectiveness(period)
+                return ", ".join(f"{factor}: {value}" for factor, value in me.items())
+
             maintenance_effectiveness_section = f"""<b>MAINTENANCE EFFECTIVENESS</b>
 <br />
 
-Last week: {calculate_maintenance_effectiveness(relativedelta(weeks=1))}
-Last month: {calculate_maintenance_effectiveness(relativedelta(months=1))}
-Last year: {calculate_maintenance_effectiveness(relativedelta(years=1))}
+Last week: {format_maintenance_effectiveness(relativedelta(weeks=1))}
+Last month: {format_maintenance_effectiveness(relativedelta(months=1))}
+Last year: {format_maintenance_effectiveness(relativedelta(years=1))}
 """
 
             sections = [
diff --git a/scripts/maintenance_effectiveness_indicator.py b/scripts/maintenance_effectiveness_indicator.py
--- a/scripts/maintenance_effectiveness_indicator.py
+++ b/scripts/maintenance_effectiveness_indicator.py
@@ -4,6 +4,7 @@
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import argparse
+import math
 from logging import INFO, basicConfig, getLogger
 
 import dateutil.parser
@@ -45,18 +46,16 @@ def main():
             "If you want to include security bugs too, please set the BUGBUG_BUGZILLA_TOKEN environment variable to your Bugzilla API key."
         )
 
-    logger.info(
-        round(
-            bugzilla.calculate_maintenance_effectiveness_indicator(
-                args.team,
-                dateutil.parser.parse(args.start_date),
-                dateutil.parser.parse(args.end_date),
-                args.components,
-            ),
-            2,
-        )
+    result = bugzilla.calculate_maintenance_effectiveness_indicator(
+        args.team,
+        dateutil.parser.parse(args.start_date),
+        dateutil.parser.parse(args.end_date),
+        args.components,
     )
 
+    for factor, value in result.items():
+        logger.info(f"{factor}: {round(value, 2) if value != math.inf else value}")
+
 
 if __name__ == "__main__":
     main()


Test patch: