Home | History | Annotate | Download | only in dashboard
      1 # Copyright 2015 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Provides the web interface for a set of alerts and their graphs."""
      6 
      7 import json
      8 
      9 from google.appengine.ext import ndb
     10 
     11 from dashboard import alerts
     12 from dashboard import chart_handler
     13 from dashboard import list_tests
     14 from dashboard import request_handler
     15 from dashboard import test_owner
     16 from dashboard import update_test_suites
     17 from dashboard import utils
     18 from dashboard.models import anomaly
     19 from dashboard.models import stoppage_alert
     20 
     21 # This is the max number of alerts to query at once. This is used in cases
     22 # when we may want to query more many more alerts than actually get displayed.
     23 _QUERY_LIMIT = 2000
     24 
     25 # Maximum number of alerts that we might want to try display in one table.
     26 _DISPLAY_LIMIT = 500
     27 
     28 
     29 class GroupReportHandler(chart_handler.ChartHandler):
     30   """Request handler for requests for group report page."""
     31 
     32   def get(self):
     33     """Renders the UI for the group report page."""
     34     self.RenderStaticHtml('group_report.html')
     35 
     36   def post(self):
     37     """Returns dynamic data for /group_report with some set of alerts.
     38 
     39     The set of alerts is determined by the keys, bug ID or revision given.
     40 
     41     Request parameters:
     42       keys: A comma-separated list of urlsafe Anomaly keys (optional).
     43       bug_id: A bug number on the Chromium issue tracker (optional).
     44       rev: A revision number (optional).
     45 
     46     Outputs:
     47       JSON for the /group_report page XHR request.
     48     """
     49     keys = self.request.get('keys')
     50     bug_id = self.request.get('bug_id')
     51     rev = self.request.get('rev')
     52 
     53     try:
     54       if bug_id:
     55         self._ShowAlertsWithBugId(bug_id)
     56       elif keys:
     57         self._ShowAlertsForKeys(keys)
     58       elif rev:
     59         self._ShowAlertsAroundRevision(rev)
     60       else:
     61         # TODO(qyearsley): Instead of just showing an error here, show a form
     62         # where the user can input a bug ID or revision.
     63         raise request_handler.InvalidInputError('No anomalies specified.')
     64     except request_handler.InvalidInputError as error:
     65       self.response.out.write(json.dumps({'error': str(error)}))
     66 
     67   def _ShowAlertsWithBugId(self, bug_id):
     68     """Show alerts for |bug_id|.
     69 
     70     Args:
     71       bug_id: A bug ID (as an int or string). Could be also be a pseudo-bug ID,
     72           such as -1 or -2 indicating invalid or ignored.
     73     """
     74     if not _IsInt(bug_id):
     75       raise request_handler.InvalidInputError('Invalid bug ID "%s".' % bug_id)
     76     bug_id = int(bug_id)
     77     anomaly_query = anomaly.Anomaly.query(
     78         anomaly.Anomaly.bug_id == bug_id)
     79     anomalies = anomaly_query.fetch(limit=_DISPLAY_LIMIT)
     80     stoppage_alert_query = stoppage_alert.StoppageAlert.query(
     81         stoppage_alert.StoppageAlert.bug_id == bug_id)
     82     stoppage_alerts = stoppage_alert_query.fetch(limit=_DISPLAY_LIMIT)
     83     self._ShowAlerts(anomalies + stoppage_alerts, bug_id)
     84 
     85   def _ShowAlertsAroundRevision(self, rev):
     86     """Shows a alerts whose revision range includes the given revision.
     87 
     88     Args:
     89       rev: A revision number, as a string.
     90     """
     91     if not _IsInt(rev):
     92       raise request_handler.InvalidInputError('Invalid rev "%s".' % rev)
     93     rev = int(rev)
     94 
     95     # We can't make a query that has two inequality filters on two different
     96     # properties (start_revision and end_revision). Therefore we first query
     97     # Anomaly entities based on one of these, then filter the resulting list.
     98     anomaly_query = anomaly.Anomaly.query(anomaly.Anomaly.end_revision >= rev)
     99     anomaly_query = anomaly_query.order(anomaly.Anomaly.end_revision)
    100     anomalies = anomaly_query.fetch(limit=_QUERY_LIMIT)
    101     anomalies = [a for a in anomalies if a.start_revision <= rev]
    102     stoppage_alert_query = stoppage_alert.StoppageAlert.query(
    103         stoppage_alert.StoppageAlert.end_revision == rev)
    104     stoppage_alerts = stoppage_alert_query.fetch(limit=_DISPLAY_LIMIT)
    105     self._ShowAlerts(anomalies + stoppage_alerts)
    106 
    107   def _ShowAlertsForKeys(self, keys):
    108     """Show alerts for |keys|.
    109 
    110     Query for anomalies with overlapping revision. The |keys|
    111     parameter for group_report is a comma-separated list of urlsafe strings
    112     for Keys for Anomaly entities. (Each key corresponds to an alert)
    113 
    114     Args:
    115       keys: Comma-separated list of urlsafe strings for Anomaly keys.
    116     """
    117     urlsafe_keys = keys.split(',')
    118     try:
    119       keys = [ndb.Key(urlsafe=k) for k in urlsafe_keys]
    120     # Errors that can be thrown here include ProtocolBufferDecodeError
    121     # in google.net.proto.ProtocolBuffer. We want to catch any errors here
    122     # because they're almost certainly urlsafe key decoding errors.
    123     except Exception:
    124       raise request_handler.InvalidInputError('Invalid Anomaly key given.')
    125 
    126     requested_anomalies = utils.GetMulti(keys)
    127 
    128     for i, anomaly_entity in enumerate(requested_anomalies):
    129       if anomaly_entity is None:
    130         raise request_handler.InvalidInputError(
    131             'No Anomaly found for key %s.' % urlsafe_keys[i])
    132 
    133     if not requested_anomalies:
    134       raise request_handler.InvalidInputError('No anomalies found.')
    135 
    136     sheriff_key = requested_anomalies[0].sheriff
    137     min_range = utils.MinimumAlertRange(requested_anomalies)
    138     if min_range:
    139       query = anomaly.Anomaly.query(
    140           anomaly.Anomaly.sheriff == sheriff_key)
    141       query = query.order(-anomaly.Anomaly.timestamp)
    142       anomalies = query.fetch(limit=_QUERY_LIMIT)
    143 
    144       # Filter out anomalies that have been marked as invalid or ignore.
    145       # Include all anomalies with an overlapping revision range that have
    146       # been associated with a bug, or are not yet triaged.
    147       anomalies = [a for a in anomalies if a.bug_id is None or a.bug_id > 0]
    148       anomalies = _GetOverlaps(anomalies, min_range[0], min_range[1])
    149 
    150       # Make sure alerts in specified param "keys" are included.
    151       key_set = {a.key for a in anomalies}
    152       for anomaly_entity in requested_anomalies:
    153         if anomaly_entity.key not in key_set:
    154           anomalies.append(anomaly_entity)
    155     else:
    156       anomalies = requested_anomalies
    157     self._ShowAlerts(anomalies)
    158 
    159   def _ShowAlerts(self, alert_list, bug_id=None):
    160     """Responds to an XHR from /group_report page with a JSON list of alerts.
    161 
    162     Args:
    163       alert_list: A list of Anomaly and/or StoppageAlert entities.
    164       bug_id: An integer bug ID.
    165     """
    166     anomaly_dicts = alerts.AnomalyDicts(
    167         [a for a in alert_list if a.key.kind() == 'Anomaly'])
    168     stoppage_alert_dicts = alerts.StoppageAlertDicts(
    169         [a for a in alert_list if a.key.kind() == 'StoppageAlert'])
    170     alert_dicts = anomaly_dicts + stoppage_alert_dicts
    171     owner_info = None
    172     if bug_id and ndb.Key('Bug', bug_id).get():
    173       owner_info = _GetOwnerInfo(alert_dicts)
    174 
    175     values = {
    176         'alert_list': alert_dicts[:_DISPLAY_LIMIT],
    177         'subtests': _GetSubTestsForAlerts(alert_dicts),
    178         'bug_id': bug_id,
    179         'owner_info': owner_info,
    180         'test_suites': update_test_suites.FetchCachedTestSuites(),
    181     }
    182     self.GetDynamicVariables(values)
    183 
    184     self.response.out.write(json.dumps(values))
    185 
    186 
    187 def _IsInt(x):
    188   """Returns True if the input can be parsed as an int."""
    189   try:
    190     int(x)
    191     return True
    192   except ValueError:
    193     return False
    194 
    195 
    196 def _GetSubTestsForAlerts(alert_list):
    197   """Gets subtest dict for list of alerts."""
    198   subtests = {}
    199   for alert in alert_list:
    200     bot_name = alert['master'] + '/' + alert['bot']
    201     testsuite = alert['testsuite']
    202     if bot_name not in subtests:
    203       subtests[bot_name] = {}
    204     if testsuite not in subtests[bot_name]:
    205       subtests[bot_name][testsuite] = list_tests.GetSubTests(
    206           testsuite, [bot_name])
    207   return subtests
    208 
    209 
    210 def _GetOverlaps(anomalies, start, end):
    211   """Gets the minimum range for the list of anomalies.
    212 
    213   Args:
    214     anomalies: The list of anomalies.
    215     start: The start revision.
    216     end: The end revision.
    217 
    218   Returns:
    219     A list of anomalies.
    220   """
    221   return [a for a in anomalies
    222           if a.start_revision <= end and a.end_revision >= start]
    223 
    224 
    225 def _GetOwnerInfo(alert_dicts):
    226   """Gets a list of owner info for list of alerts for bug with bisect result.
    227 
    228   Test owners are retrieved by a set of master and test suite name from each
    229   alert in alert_dicts.
    230 
    231   Args:
    232     alert_dicts: List of alert data dictionaries.
    233 
    234   Returns:
    235     A list of dictionary containing owner information.
    236   """
    237   test_suite_paths = {'%s/%s' % (a['master'], a['testsuite'])
    238                       for a in alert_dicts}
    239   owners = test_owner.GetOwners(test_suite_paths)
    240   return [{'email': owner} for owner in owners]
    241