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 filing a bug on the issue tracker.""" 6 7import json 8import logging 9 10from google.appengine.api import app_identity 11from google.appengine.api import urlfetch 12from google.appengine.api import users 13from google.appengine.ext import ndb 14 15from dashboard import auto_bisect 16from dashboard import issue_tracker_service 17from dashboard import oauth2_decorator 18from dashboard import request_handler 19from dashboard import utils 20from dashboard.models import alert 21from dashboard.models import bug_data 22from dashboard.models import bug_label_patterns 23 24# A list of bug labels to suggest for all performance regression bugs. 25_DEFAULT_LABELS = [ 26 'Type-Bug-Regression', 27 'Pri-2', 28 'Performance-Sheriff' 29] 30_OMAHA_PROXY_URL = 'https://omahaproxy.appspot.com/all.json' 31 32 33class FileBugHandler(request_handler.RequestHandler): 34 """Uses oauth2 to file a new bug with a set of alerts.""" 35 36 def post(self): 37 """A POST request for this endpoint is the same as a GET request.""" 38 self.get() 39 40 @oauth2_decorator.DECORATOR.oauth_required 41 def get(self): 42 """Either shows the form to file a bug, or if filled in, files the bug. 43 44 The form to file a bug is popped up from the triage-dialog polymer element. 45 The default summary, description and label strings are constructed there. 46 47 Request parameters: 48 summary: Bug summary string. 49 description: Bug full description string. 50 owner: Bug owner email address. 51 keys: Comma-separated Alert keys in urlsafe format. 52 53 Outputs: 54 HTML, using the template 'bug_result.html'. 55 """ 56 if not utils.IsValidSheriffUser(): 57 # TODO(qyearsley): Simplify this message (after a couple months). 58 self.RenderHtml('bug_result.html', { 59 'error': ('You must be logged in with a chromium.org account ' 60 'in order to file bugs here! This is the case ever ' 61 'since we switched to the Monorail issue tracker. ' 62 'Note, viewing internal data should work for Googlers ' 63 'that are logged in with the Chromium accounts. See ' 64 'https://github.com/catapult-project/catapult/issues/2042') 65 }) 66 return 67 68 summary = self.request.get('summary') 69 description = self.request.get('description') 70 labels = self.request.get_all('label') 71 components = self.request.get_all('component') 72 keys = self.request.get('keys') 73 if not keys: 74 self.RenderHtml('bug_result.html', { 75 'error': 'No alerts specified to add bugs to.' 76 }) 77 return 78 79 if self.request.get('finish'): 80 self._CreateBug(summary, description, labels, components, keys) 81 else: 82 self._ShowBugDialog(summary, description, keys) 83 84 def _ShowBugDialog(self, summary, description, urlsafe_keys): 85 """Sends a HTML page with a form for filing the bug. 86 87 Args: 88 summary: The default bug summary string. 89 description: The default bug description string. 90 urlsafe_keys: Comma-separated Alert keys in urlsafe format. 91 """ 92 alert_keys = [ndb.Key(urlsafe=k) for k in urlsafe_keys.split(',')] 93 labels, components = _FetchLabelsAndComponents(alert_keys) 94 self.RenderHtml('bug_result.html', { 95 'bug_create_form': True, 96 'keys': urlsafe_keys, 97 'summary': summary, 98 'description': description, 99 'labels': labels, 100 'components': components, 101 'owner': users.get_current_user(), 102 }) 103 104 def _CreateBug(self, summary, description, labels, components, urlsafe_keys): 105 """Creates a bug, associates it with the alerts, sends a HTML response. 106 107 Args: 108 summary: The new bug summary string. 109 description: The new bug description string. 110 labels: List of label strings for the new bug. 111 components: List of component strings for the new bug. 112 urlsafe_keys: Comma-separated alert keys in urlsafe format. 113 """ 114 alert_keys = [ndb.Key(urlsafe=k) for k in urlsafe_keys.split(',')] 115 alerts = ndb.get_multi(alert_keys) 116 117 if not description: 118 description = 'See the link to graphs below.' 119 120 milestone_label = _MilestoneLabel(alerts) 121 if milestone_label: 122 labels.append(milestone_label) 123 124 # Only project members (@chromium.org accounts) can be owners of bugs. 125 owner = self.request.get('owner') 126 if owner and not owner.endswith('@chromium.org'): 127 self.RenderHtml('bug_result.html', { 128 'error': 'Owner email address must end with @chromium.org.' 129 }) 130 return 131 132 http = oauth2_decorator.DECORATOR.http() 133 service = issue_tracker_service.IssueTrackerService(http=http) 134 135 bug_id = service.NewBug( 136 summary, description, labels=labels, components=components, owner=owner) 137 if not bug_id: 138 self.RenderHtml('bug_result.html', {'error': 'Error creating bug!'}) 139 return 140 141 bug_data.Bug(id=bug_id).put() 142 for alert_entity in alerts: 143 alert_entity.bug_id = bug_id 144 ndb.put_multi(alerts) 145 146 comment_body = _AdditionalDetails(bug_id, alerts) 147 service.AddBugComment(bug_id, comment_body) 148 149 template_params = {'bug_id': bug_id} 150 if all(k.kind() == 'Anomaly' for k in alert_keys): 151 bisect_result = auto_bisect.StartNewBisectForBug(bug_id) 152 if 'error' in bisect_result: 153 template_params['bisect_error'] = bisect_result['error'] 154 else: 155 template_params.update(bisect_result) 156 self.RenderHtml('bug_result.html', template_params) 157 158 159def _AdditionalDetails(bug_id, alerts): 160 """Returns a message with additional information to add to a bug.""" 161 base_url = '%s/group_report' % _GetServerURL() 162 bug_page_url = '%s?bug_id=%s' % (base_url, bug_id) 163 alerts_url = '%s?keys=%s' % (base_url, _UrlsafeKeys(alerts)) 164 comment = 'All graphs for this bug:\n %s\n\n' % bug_page_url 165 comment += 'Original alerts at time of bug-filing:\n %s\n' % alerts_url 166 bot_names = alert.GetBotNamesFromAlerts(alerts) 167 if bot_names: 168 comment += '\n\nBot(s) for this bug\'s original alert(s):\n\n' 169 comment += '\n'.join(sorted(bot_names)) 170 else: 171 comment += '\nCould not extract bot names from the list of alerts.' 172 return comment 173 174 175def _GetServerURL(): 176 return 'https://' + app_identity.get_default_version_hostname() 177 178 179def _UrlsafeKeys(alerts): 180 return ','.join(a.key.urlsafe() for a in alerts) 181 182 183def _FetchLabelsAndComponents(alert_keys): 184 """Fetches a list of bug labels and components for the given Alert keys.""" 185 labels = set(_DEFAULT_LABELS) 186 components = set() 187 alerts = ndb.get_multi(alert_keys) 188 if any(a.internal_only for a in alerts): 189 # This is a Chrome-specific behavior, and should ideally be made 190 # more general (maybe there should be a list in datastore of bug 191 # labels to add for internal bugs). 192 labels.add('Restrict-View-Google') 193 for test in {a.test for a in alerts}: 194 labels_components = bug_label_patterns.GetBugLabelsForTest(test) 195 for item in labels_components: 196 if item.startswith('Cr-'): 197 components.add(item.replace('Cr-', '').replace('-', '>')) 198 else: 199 labels.add(item) 200 return labels, components 201 202 203def _MilestoneLabel(alerts): 204 """Returns a milestone label string, or None.""" 205 revisions = [a.start_revision for a in alerts if hasattr(a, 'start_revision')] 206 if not revisions: 207 return None 208 start_revision = min(revisions) 209 try: 210 milestone = _GetMilestoneForRevision(start_revision) 211 except KeyError: 212 logging.error('List of versions not in the expected format') 213 if not milestone: 214 return None 215 logging.info('Matched rev %s to milestone %s.', start_revision, milestone) 216 return 'M-%d' % milestone 217 218 219def _GetMilestoneForRevision(revision): 220 """Finds the oldest milestone for a given revision from OmahaProxy. 221 222 The purpose of this function is to resolve the milestone that would be blocked 223 by a suspected regression. We do this by locating in the list of current 224 versions, regardless of platform and channel, all the version strings (e.g. 225 36.0.1234.56) that match revisions (commit positions) later than the earliest 226 possible start_revision of the suspected regression; we then parse out the 227 first numeric part of such strings, assume it to be the corresponding 228 milestone, and return the lowest one in the set. 229 230 Args: 231 revision: An integer or string containing an integer. 232 233 Returns: 234 An integer representing the lowest milestone matching the given revision or 235 the highest milestone if the given revision exceeds all defined milestones. 236 Note that the default is 0 when no milestones at all are found. If the 237 given revision is None, then None is returned. 238 """ 239 if revision is None: 240 return None 241 milestones = set() 242 default_milestone = 0 243 all_versions = _GetAllCurrentVersionsFromOmahaProxy() 244 for os in all_versions: 245 for version in os['versions']: 246 try: 247 milestone = int(version['current_version'].split('.')[0]) 248 version_commit = version.get('branch_base_position') 249 if version_commit and int(revision) < int(version_commit): 250 milestones.add(milestone) 251 if milestone > default_milestone: 252 default_milestone = milestone 253 except ValueError: 254 # Sometimes 'N/A' is given. We ignore these entries. 255 logging.warn('Could not cast one of: %s, %s, %s as an int', 256 revision, version['branch_base_position'], 257 version['current_version'].split('.')[0]) 258 if milestones: 259 return min(milestones) 260 return default_milestone 261 262 263def _GetAllCurrentVersionsFromOmahaProxy(): 264 """Retrieves a the list current versions from OmahaProxy and parses it.""" 265 try: 266 response = urlfetch.fetch(_OMAHA_PROXY_URL) 267 if response.status_code == 200: 268 return json.loads(response.content) 269 except urlfetch.Error: 270 logging.error('Error pulling list of current versions (omahaproxy).') 271 except ValueError: 272 logging.error('OmahaProxy did not return valid JSON.') 273 return [] 274