blob: c1fb202f39d56d3a3cf8e9b2fc0062f46f9c36d0 [file] [log] [blame]
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Provides the web interface for filing a bug on the issue tracker."""
import json
import logging
from google.appengine.api import app_identity
from google.appengine.api import urlfetch
from google.appengine.api import users
from google.appengine.ext import ndb
from dashboard import auto_bisect
from dashboard import issue_tracker_service
from dashboard import oauth2_decorator
from dashboard import request_handler
from dashboard import utils
from dashboard.models import alert
from dashboard.models import bug_data
from dashboard.models import bug_label_patterns
# A list of bug labels to suggest for all performance regression bugs.
_DEFAULT_LABELS = [
'Type-Bug-Regression',
'Pri-2',
'Performance-Sheriff'
]
_OMAHA_PROXY_URL = 'https://omahaproxy.appspot.com/all.json'
class FileBugHandler(request_handler.RequestHandler):
"""Uses oauth2 to file a new bug with a set of alerts."""
def post(self):
"""Make all the functions available via POST as well as GET."""
self.get()
@oauth2_decorator.decorator.oauth_required
def get(self):
"""Either shows the form to file a bug, or if filled in, files the bug.
The form to file a bug is popped up from the triage-dialog polymer element.
The default summary, description and label strings are constructed there.
Request parameters:
summary: Bug summary string.
description: Bug full description string.
owner: Bug owner email address.
keys: Comma-separated Alert keys in urlsafe format.
Outputs:
HTML, using the template 'bug_result.html'.
"""
if not utils.IsValidSheriffUser():
user = users.get_current_user()
self.ReportError('User "%s" not authorized.' % user, status=403)
return
summary = self.request.get('summary')
description = self.request.get('description')
labels = self.request.get_all('label')
keys = self.request.get('keys')
if not keys:
self.RenderHtml('bug_result.html', {
'error': 'No alerts specified to add bugs to.'
})
return
if self.request.get('finish'):
self._CreateBug(summary, description, labels, keys)
else:
self._ShowBugDialog(summary, description, keys)
def _ShowBugDialog(self, summary, description, urlsafe_keys):
"""Sends a HTML page with a form for filing the bug.
Args:
summary: The default bug summary string.
description: The default bug description string.
urlsafe_keys: Comma-separated Alert keys in urlsafe format.
"""
# Fill in the owner field with the logged in user's email. For convenience,
# if it's a @google.com account, swap @google.com with @chromium.org.
user_email = users.get_current_user().email()
if user_email.endswith('@google.com'):
user_email = user_email.replace('@google.com', '@chromium.org')
alert_keys = [ndb.Key(urlsafe=k) for k in urlsafe_keys.split(',')]
labels = _FetchLabels(alert_keys)
self.RenderHtml('bug_result.html', {
'bug_create_form': True,
'keys': urlsafe_keys,
'summary': summary,
'description': description,
'labels': labels,
'owner': user_email,
})
def _CreateBug(self, summary, description, labels, urlsafe_keys):
"""Creates a bug, associates it with the alerts, sends a HTML response.
Args:
summary: The new bug summary string.
description: The new bug description string.
labels: List of label strings for the new bug.
urlsafe_keys: Comma-separated alert keys in urlsafe format.
"""
alert_keys = [ndb.Key(urlsafe=k) for k in urlsafe_keys.split(',')]
alerts = ndb.get_multi(alert_keys)
if not description:
description = 'See the link to graphs below.'
milestone_label = _MilestoneLabel(alerts)
if milestone_label:
labels.append(milestone_label)
# Only project members (@chromium.org accounts) can be owners of bugs.
owner = self.request.get('owner')
if owner and not owner.endswith('@chromium.org'):
self.RenderHtml('bug_result.html', {
'error': 'Owner email address must end with @chromium.org.'
})
return
http = oauth2_decorator.decorator.http()
service = issue_tracker_service.IssueTrackerService(http=http)
bug_id = service.NewBug(summary, description, labels=labels, owner=owner)
if not bug_id:
self.RenderHtml('bug_result.html', {'error': 'Error creating bug!'})
return
bug_data.Bug(id=bug_id).put()
for alert_entity in alerts:
alert_entity.bug_id = bug_id
ndb.put_multi(alerts)
self._AddAdditionalDetailsToBug(bug_id, alerts)
template_params = {'bug_id': bug_id}
if all(k.kind() == 'Anomaly' for k in alert_keys):
bisect_result = auto_bisect.StartNewBisectForBug(bug_id)
if 'error' in bisect_result:
template_params['bisect_error'] = bisect_result['error']
else:
template_params.update(bisect_result)
self.RenderHtml('bug_result.html', template_params)
def _AddAdditionalDetailsToBug(self, bug_id, alerts):
"""Adds additional data to the bug as a comment.
Adds the link to /group_report and bug_id as well as the names of the bots
that triggered the alerts, and a milestone label.
Args:
bug_id: Bug ID number.
alerts: The Alert entities being associated with this bug.
"""
base_url = '%s/group_report' % _GetServerURL()
bug_page_url = '%s?bug_id=%s' % (base_url, bug_id)
alerts_url = '%s?keys=%s' % (base_url, _UrlsafeKeys(alerts))
comment = 'All graphs for this bug:\n %s\n\n' % bug_page_url
comment += 'Original alerts at time of bug-filing:\n %s\n' % alerts_url
bot_names = alert.GetBotNamesFromAlerts(alerts)
if bot_names:
comment += '\n\nBot(s) for this bug\'s original alert(s):\n\n'
comment += '\n'.join(sorted(bot_names))
else:
comment += '\nCould not extract bot names from the list of alerts.'
http = oauth2_decorator.decorator.http()
service = issue_tracker_service.IssueTrackerService(http=http)
service.AddBugComment(bug_id, comment)
def _GetServerURL():
return 'https://' + app_identity.get_default_version_hostname()
def _UrlsafeKeys(alerts):
return ','.join(a.key.urlsafe() for a in alerts)
def _FetchLabels(alert_keys):
"""Fetches a list of bug labels for the given list of Alert keys."""
labels = set(_DEFAULT_LABELS)
alerts = ndb.get_multi(alert_keys)
if any(a.internal_only for a in alerts):
# This is a Chrome-specific behavior, and should ideally be made
# more general (maybe there should be a list in datastore of bug
# labels to add for internal bugs).
labels.add('Restrict-View-Google')
for test in {a.test for a in alerts}:
labels.update(bug_label_patterns.GetBugLabelsForTest(test))
return labels
def _MilestoneLabel(alerts):
"""Returns a milestone label string, or None."""
revisions = [a.start_revision for a in alerts if hasattr(a, 'start_revision')]
if not revisions:
return None
start_revision = min(revisions)
try:
milestone = _GetMilestoneForRevision(start_revision)
except KeyError:
logging.error('List of versions not in the expected format')
if not milestone:
return None
logging.info('Matched rev %s to milestone %s.', start_revision, milestone)
return 'M-%d' % milestone
def _GetMilestoneForRevision(revision):
"""Finds the oldest milestone for a given revision from OmahaProxy.
The purpose of this function is to resolve the milestone that would be blocked
by a suspected regression. We do this by locating in the list of current
versions, regardless of platform and channel, all the version strings (e.g.
36.0.1234.56) that match revisions (commit positions) later than the earliest
possible start_revision of the suspected regression; we then parse out the
first numeric part of such strings, assume it to be the corresponding
milestone, and return the lowest one in the set.
Args:
revision: An integer or string containing an integer.
Returns:
An integer representing the lowest milestone matching the given revision or
the highest milestone if the given revision exceeds all defined milestones.
Note that the default is 0 when no milestones at all are found. If the
given revision is None, then None is returned.
"""
if revision is None:
return None
milestones = set()
default_milestone = 0
all_versions = _GetAllCurrentVersionsFromOmahaProxy()
for os in all_versions:
for version in os['versions']:
try:
milestone = int(version['current_version'].split('.')[0])
version_commit = version.get('branch_base_position')
if version_commit and int(revision) < int(version_commit):
milestones.add(milestone)
if milestone > default_milestone:
default_milestone = milestone
except ValueError:
# Sometimes 'N/A' is given. We ignore these entries.
logging.warn('Could not cast one of: %s, %s, %s as an int',
revision, version['branch_base_position'],
version['current_version'].split('.')[0])
if milestones:
return min(milestones)
return default_milestone
def _GetAllCurrentVersionsFromOmahaProxy():
"""Retrieves a the list current versions from OmahaProxy and parses it."""
try:
response = urlfetch.fetch(_OMAHA_PROXY_URL)
if response.status_code == 200:
return json.loads(response.content)
except urlfetch.Error:
logging.error('Error pulling list of current versions (omahaproxy).')
except ValueError:
logging.error('OmahaProxy did not return valid JSON.')
return []