Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect bugs with low crash volume to drop topcrash keywords and decrease the severity #1643

Merged
merged 12 commits into from Sep 15, 2022
Merged
219 changes: 219 additions & 0 deletions auto_nag/scripts/crash_small_volume.py
@@ -0,0 +1,219 @@
# 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/.

from auto_nag import utils
from auto_nag.bzcleaner import BzCleaner
from auto_nag.history import History
from auto_nag.topcrash import TOP_CRASH_IDENTIFICATION_CRITERIA, Topcrash

# TODO: should be moved when resolving https://github.com/mozilla/relman-auto-nag/issues/1384
suhaibmujahid marked this conversation as resolved.
Show resolved Hide resolved
HIGH_SEVERITY = {"S1", "critical", "S2", "major"}

MAX_SIGNATURES_PER_QUERY = 30


class CrashSmallVolume(BzCleaner):
def __init__(self, min_crash_volume: int = 5):
"""Constructor.

Args:
min_crash_volume: the minimum number of crashes per week for a
signature to not be considered low volume.
"""
super().__init__()

self.min_crash_volume = min_crash_volume
topcrash = Topcrash(
criteria=self._adjust_topcrash_criteria(TOP_CRASH_IDENTIFICATION_CRITERIA)
)
self.topcrash_signatures = topcrash.get_signatures()
self.blocked_signatures = topcrash.get_blocked_signatures()

def description(self):
return "Bugs with small crash volume"

def columns(self):
return ["id", "summary", "severity", "deleted_keywords_count"]

def _get_last_topcrash_added(self, bug):
pass

def _adjust_topcrash_criteria(self, topcrash_criteria):
factor = 2
new_criteria = []
for criterion in topcrash_criteria:
criterion = {
**criterion,
"tc_limit": criterion["tc_limit"] * factor,
}
if "tc_startup_limit" in criterion:
criterion["tc_startup_limit"] = criterion["tc_startup_limit"] * factor

new_criteria.append(criterion)

return new_criteria

def handle_bug(self, bug, data):
bugid = str(bug["id"])

signatures = utils.get_signatures(bug["cf_crash_signature"])

if any(signature in self.blocked_signatures for signature in signatures):
# Ignore those bugs as we can't be sure.
return None

top_crash_signatures = [
signature
for signature in signatures
if signature in self.topcrash_signatures
]

keep_topcrash_startup = any(
any(
criterion["is_startup"]
for criterion in self.topcrash_signatures[signature]
)
for signature in top_crash_signatures
)

keywords_to_remove = None
if not top_crash_signatures:
keywords_to_remove = set(bug["keywords"]) & {"topcrash", "topcrash-startup"}
elif not keep_topcrash_startup:
keywords_to_remove = set(bug["keywords"]) & {"topcrash-startup"}
else:
return None

data[bugid] = {
"severity": bug["severity"],
"ignore_severity": (
bug["severity"] not in HIGH_SEVERITY
or bug["groups"] == "security"
or any(keyword.startswith("sec-") for keyword in bug["keywords"])
or self._has_severity_downgrade_comment(bug)
),
"keywords_to_remove": keywords_to_remove,
suhaibmujahid marked this conversation as resolved.
Show resolved Hide resolved
"signatures": signatures,
}

return bug

def _get_low_volume_crash_signatures(self, bugs):
signatures = {
signature
for bug in bugs.values()
if not bug["ignore_severity"]
for signature in bug["signatures"]
}

signature_volume = Topcrash().fetch_signature_volume(signatures)

low_volume_signatures = {
signature
for signature, volume in signature_volume.items()
if volume < self.min_crash_volume
}

return low_volume_signatures

def get_bugs(self, date="today", bug_ids=..., chunk_size=None):
bugs = super().get_bugs(date, bug_ids, chunk_size)
self.set_autofix(bugs)

# Keep only bugs with an autofix
bugs = {
bugid: bug for bugid, bug in bugs.items() if bugid in self.autofix_changes
}

return bugs

def set_autofix(self, bugs):
"""Set the autofix for each bug."""

low_volume_signatures = self._get_low_volume_crash_signatures(bugs)

for bugid, bug in bugs.items():
autofix = {}
reasons = []
if bug["keywords_to_remove"]:
reasons.append(
"Based on the [topcrash criteria](https://wiki.mozilla.org/CrashKill/Topcrash), the crash "
+ (
"signature linked to this bug is not a topcrash signature anymore."
if len(bug["signatures"]) == 1
else "signatures linked to this bug are not in the topcrash signatures anymore."
)
)
autofix["keywords"] = {"remove": list(bug["keywords_to_remove"])}

if not bug["ignore_severity"] and all(
signature in low_volume_signatures for signature in bug["signatures"]
):
reasons.append(
f"Since the crash volume is low (less than {self.min_crash_volume} per week), "
"the severity is downgraded to `S3`. "
"Feel free to change it back if you think the bug is still critical."
)
autofix["severity"] = "S3"
bug["severity"] += " → " + autofix["severity"]

if autofix:
bug["deleted_keywords_count"] = (
len(bug["keywords_to_remove"]) if bug["keywords_to_remove"] else "-"
)
reasons.append(self.get_documentation())
autofix["comment"] = {
"body": "\n\n".join(reasons),
}
self.autofix_changes[bugid] = autofix

@staticmethod
def _has_severity_downgrade_comment(bug):
for comment in reversed(bug["comments"]):
if (
comment["creator"] == History.BOT
and "the severity is downgraded to" in comment["raw_text"]
):
return True
return False

def get_bz_params(self, date):
fields = [
"severity",
"keywords",
"cf_crash_signature",
"comments.raw_text",
"comments.creator",
]
params = {
"include_fields": fields,
"resolution": "---",
"f1": "OP",
"j1": "OR",
"f2": "keywords",
"o2": "anywords",
"v2": ["topcrash", "topcrash-startup"],
"f3": "OP",
"j3": "AND",
"f4": "bug_severity",
"o4": "anyexact",
"v4": list(HIGH_SEVERITY),
"n5": "1",
"f5": "bug_severity",
"o5": "changedafter",
"v5": "-30d",
"f6": "cf_crash_signature",
"o6": "isnotempty",
"f7": "CP",
"f8": "CP",
"f9": "creation_ts",
"o9": "lessthan",
"v9": "-1w",
}

return params


if __name__ == "__main__":
CrashSmallVolume().run()
3 changes: 2 additions & 1 deletion auto_nag/scripts/no_crashes.py
Expand Up @@ -57,7 +57,8 @@ def get_bz_params(self, date):

return params

def chunkify(self, signatures):
@staticmethod
def chunkify(signatures):
"""Make some chunks with signatures,
the total length of each chunk must be <= 1536"""
total = sum(len(s) for s in signatures)
Expand Down
2 changes: 1 addition & 1 deletion auto_nag/scripts/topcrash_add_keyword.py
Expand Up @@ -151,7 +151,7 @@ def get_bugs(self, date="today", bug_ids=[], chunk_size=None):
return bugs

def get_bz_params_list(self, date):
self.topcrashes = Topcrash().get_signatures(date)
self.topcrashes = Topcrash(date).get_signatures()

fields = [
"triage_owner",
Expand Down
16 changes: 3 additions & 13 deletions auto_nag/tests/test_topcrash.py
@@ -1,28 +1,18 @@
import unittest

import libmozdata.socorro as socorro
from libmozdata import utils as lmdutils

from auto_nag.topcrash import Topcrash


class TestTopcrash(unittest.TestCase):
def test___get_params_from_criteria(self):
topcrash = Topcrash()

start_date = lmdutils.get_date("today", 30)
end_date = lmdutils.get_date("today")
date_range = socorro.SuperSearch.get_search_date(start_date, end_date)

def test_get_blocked_signatures(self):
crash_signature_block_patterns = [
"!^EMPTY: ",
"!=OOM | small",
"!=IPCError-browser | ShutDownKill",
]

signatures = topcrash._fetch_signatures_from_patters(
crash_signature_block_patterns, date_range
)
topcrash = Topcrash(signature_block_patterns=crash_signature_block_patterns)
signatures = topcrash.get_blocked_signatures()

assert "OOM | small" in signatures
assert "IPCError-browser | ShutDownKill" in signatures
Expand Down