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