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