Repository: mozilla/bugbug

Issue:
Create a separate model to classify components for Fenix bugs
Currently, we have [a general model](https://github.com/suhaibmujahid/bugbug/blob/master/bugbug/models/component.py) to classify all components. However, many of the bugs in the [`Fenix::General`](https://bugzilla.mozilla.org/buglist.cgi?bug_type=defect&classification=Client%20Software&classification=Developer%20Infrastructure&classification=Components&classification=Server%20Software&classification=Other&component=General&product=Fenix&resolution=---&order=bug_id&list_id=17022560) are still not classified due to the law confidence score. One possible solution to overcome this issue is to create a detected model for the Fenix product.


Discussion:

Resolving patch:
diff --git a/bugbug/models/__init__.py b/bugbug/models/__init__.py
--- a/bugbug/models/__init__.py
+++ b/bugbug/models/__init__.py
@@ -37,6 +37,7 @@
     "tracking": "bugbug.models.tracking.TrackingModel",
     "uplift": "bugbug.models.uplift.UpliftModel",
     "worksforme": "bugbug.models.worksforme.WorksForMeModel",
+    "fenixcomponent": "bugbug.models.fenixcomponent.FenixComponentModel",
 }
 
 
diff --git a/bugbug/models/fenixcomponent.py b/bugbug/models/fenixcomponent.py
new file mode 100644
--- /dev/null
+++ b/bugbug/models/fenixcomponent.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import logging
+from datetime import datetime, timezone
+
+import dateutil.parser
+import xgboost
+from dateutil.relativedelta import relativedelta
+from imblearn.over_sampling import SMOTE
+from imblearn.pipeline import Pipeline as ImblearnPipeline
+from imblearn.under_sampling import RandomUnderSampler
+from sklearn.compose import ColumnTransformer
+from sklearn.feature_extraction import DictVectorizer
+from sklearn.pipeline import Pipeline
+
+from bugbug import bug_features, bugzilla, feature_cleanup, utils
+from bugbug.model import BugModel
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+
+class FenixComponentModel(BugModel):
+    def __init__(self, lemmatization=False):
+        BugModel.__init__(self, lemmatization)
+
+        self.cross_validation_enabled = False
+        self.calculate_importance = False
+
+        feature_extractors = [
+            bug_features.HasSTR(),
+            bug_features.Severity(),
+            bug_features.Keywords(),
+            bug_features.HasCrashSignature(),
+            bug_features.HasURL(),
+            bug_features.HasW3CURL(),
+            bug_features.HasGithubURL(),
+            bug_features.Whiteboard(),
+            bug_features.Patches(),
+            bug_features.Landings(),
+        ]
+
+        cleanup_functions = [
+            feature_cleanup.fileref(),
+            feature_cleanup.url(),
+            feature_cleanup.synonyms(),
+        ]
+
+        self.extraction_pipeline = Pipeline(
+            [
+                (
+                    "bug_extractor",
+                    bug_features.BugExtractor(feature_extractors, cleanup_functions),
+                ),
+            ]
+        )
+
+        self.clf = ImblearnPipeline(
+            [
+                (
+                    "union",
+                    ColumnTransformer(
+                        [
+                            ("data", DictVectorizer(), "data"),
+                            ("title", self.text_vectorizer(), "title"),
+                            ("comments", self.text_vectorizer(), "comments"),
+                        ]
+                    ),
+                ),
+                ("smote", SMOTE(random_state=42)),
+                ("undersample", RandomUnderSampler(random_state=42)),
+                (
+                    "estimator",
+                    xgboost.XGBClassifier(n_jobs=utils.get_physical_cpu_count()),
+                ),
+            ]
+        )
+
+    def get_labels(self):
+        classes = {}
+
+        date_limit = datetime.now(timezone.utc) - relativedelta(years=2)
+
+        for bug_data in bugzilla.get_bugs():
+            if bug_data["product"] != "Fenix":
+                continue
+
+            # Exclude 'General' because it contains bugs that may belong to other components, thus introducing noise.
+            if bug_data["component"] == "General":
+                continue
+
+            if dateutil.parser.parse(bug_data["creation_time"]) < date_limit:
+                continue
+
+            classes[int(bug_data["id"])] = bug_data["component"]
+
+        return classes, set(classes.values())
+
+    def get_feature_names(self):
+        return self.clf.named_steps["union"].get_feature_names_out()
diff --git a/http_service/bugbug_http/models.py b/http_service/bugbug_http/models.py
--- a/http_service/bugbug_http/models.py
+++ b/http_service/bugbug_http/models.py
@@ -38,6 +38,7 @@
     "accessibility",
     "performancebug",
     "worksforme",
+    "fenixcomponent",
 ]
 
 DEFAULT_EXPIRATION_TTL = 7 * 24 * 3600  # A week


Test patch: