blob: b9106c506914b5eb95d4eaba21646b53b178e373 [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 a set of alerts and their graphs."""
import json
from google.appengine.ext import ndb
from dashboard import alerts
from dashboard import chart_handler
from dashboard import list_tests
from dashboard import request_handler
from dashboard import test_owner
from dashboard import update_test_suites
from dashboard import utils
from dashboard.models import anomaly
from dashboard.models import stoppage_alert
# This is the max number of alerts to query at once. This is used in cases
# when we may want to query more many more alerts than actually get displayed.
_QUERY_LIMIT = 2000
# Maximum number of alerts that we might want to try display in one table.
_DISPLAY_LIMIT = 500
class GroupReportHandler(chart_handler.ChartHandler):
"""Request handler for requests for group report page."""
def get(self):
"""Renders the UI for /group_report with some set of alerts.
The set of alerts is determined by the keys, bug ID or revision given.
Request parameters:
keys: A comma-separated list of urlsafe Anomaly keys (optional).
bug_id: A bug number on the Chromium issue tracker (optional).
rev: A revision number (optional).
Outputs:
HTML for the /group_report page.
"""
keys = self.request.get('keys')
bug_id = self.request.get('bug_id')
rev = self.request.get('rev')
try:
if bug_id:
self._ShowAlertsWithBugId(bug_id)
elif keys:
self._ShowAlertsForKeys(keys)
elif rev:
self._ShowAlertsAroundRevision(rev)
else:
# TODO(qyearsley): Instead of just showing an error here, show a form
# where the user can input a bug ID or revision.
raise request_handler.InvalidInputError('No anomalies specified.')
except request_handler.InvalidInputError as error:
self.RenderHtml('bug_result.html', {'error': str(error)})
def _ShowAlertsWithBugId(self, bug_id):
"""Show alerts for |bug_id|.
Args:
bug_id: A bug ID (as an int or string). Could be also be a pseudo-bug ID,
such as -1 or -2 indicating invalid or ignored.
"""
if not _IsInt(bug_id):
raise request_handler.InvalidInputError('Invalid bug ID "%s".' % bug_id)
bug_id = int(bug_id)
anomaly_query = anomaly.Anomaly.query(
anomaly.Anomaly.bug_id == bug_id)
anomalies = anomaly_query.fetch(limit=_DISPLAY_LIMIT)
stoppage_alert_query = stoppage_alert.StoppageAlert.query(
stoppage_alert.StoppageAlert.bug_id == bug_id)
stoppage_alerts = stoppage_alert_query.fetch(limit=_DISPLAY_LIMIT)
self._ShowAlerts(anomalies + stoppage_alerts, bug_id)
def _ShowAlertsAroundRevision(self, rev):
"""Shows a alerts whose revision range includes the given revision.
Args:
rev: A revision number, as a string.
"""
if not _IsInt(rev):
raise request_handler.InvalidInputError('Invalid rev "%s".' % rev)
rev = int(rev)
# We can't make a query that has two inequality filters on two different
# properties (start_revision and end_revision). Therefore we first query
# Anomaly entities based on one of these, then filter the resulting list.
anomaly_query = anomaly.Anomaly.query(anomaly.Anomaly.end_revision >= rev)
anomaly_query = anomaly_query.order(anomaly.Anomaly.end_revision)
anomalies = anomaly_query.fetch(limit=_QUERY_LIMIT)
anomalies = [a for a in anomalies if a.start_revision <= rev]
stoppage_alert_query = stoppage_alert.StoppageAlert.query(
stoppage_alert.StoppageAlert.end_revision == rev)
stoppage_alerts = stoppage_alert_query.fetch(limit=_DISPLAY_LIMIT)
self._ShowAlerts(anomalies + stoppage_alerts)
def _ShowAlertsForKeys(self, keys):
"""Show alerts for |keys|.
Query for anomalies with overlapping revision. The |keys|
parameter for group_report is a comma-separated list of urlsafe strings
for Keys for Anomaly entities. (Each key corresponds to an alert)
Args:
keys: Comma-separated list of urlsafe strings for Anomaly keys.
"""
urlsafe_keys = keys.split(',')
try:
keys = [ndb.Key(urlsafe=k) for k in urlsafe_keys]
# Errors that can be thrown here include ProtocolBufferDecodeError
# in google.net.proto.ProtocolBuffer. We want to catch any errors here
# because they're almost certainly urlsafe key decoding errors.
except Exception:
raise request_handler.InvalidInputError('Invalid Anomaly key given.')
requested_anomalies = utils.GetMulti(keys)
for i, anomaly_entity in enumerate(requested_anomalies):
if anomaly_entity is None:
raise request_handler.InvalidInputError(
'No Anomaly found for key %s.' % urlsafe_keys[i])
if not requested_anomalies:
raise request_handler.InvalidInputError('No anomalies found.')
sheriff_key = requested_anomalies[0].sheriff
min_range = utils.MinimumAlertRange(requested_anomalies)
if min_range:
query = anomaly.Anomaly.query(
anomaly.Anomaly.sheriff == sheriff_key)
query = query.order(-anomaly.Anomaly.timestamp)
anomalies = query.fetch(limit=_QUERY_LIMIT)
# Filter out anomalies that have been marked as invalid or ignore.
# Include all anomalies with an overlapping revision range that have
# been associated with a bug, or are not yet triaged.
anomalies = [a for a in anomalies if a.bug_id is None or a.bug_id > 0]
anomalies = _GetOverlaps(anomalies, min_range[0], min_range[1])
# Make sure alerts in specified param "keys" are included.
key_set = {a.key for a in anomalies}
for anomaly_entity in requested_anomalies:
if anomaly_entity.key not in key_set:
anomalies.append(anomaly_entity)
else:
anomalies = requested_anomalies
self._ShowAlerts(anomalies)
def _ShowAlerts(self, alert_list, bug_id=None):
"""Renders the template group_report.html with a list of alerts.
Args:
alert_list: A list of Anomaly and/or StoppageAlert entities.
bug_id: An integer bug ID.
"""
anomaly_dicts = alerts.AnomalyDicts(
[a for a in alert_list if a.key.kind() == 'Anomaly'])
stoppage_alert_dicts = alerts.StoppageAlertDicts(
[a for a in alert_list if a.key.kind() == 'StoppageAlert'])
alert_dicts = anomaly_dicts + stoppage_alert_dicts
owner_info = None
if bug_id and ndb.Key('Bug', bug_id).get():
owner_info = _GetOwnerInfo(alert_dicts)
self.RenderHtml('group_report.html', {
'alert_list': json.dumps(alert_dicts[:_DISPLAY_LIMIT]),
'subtests': json.dumps(_GetSubTestsForAlerts(alert_dicts)),
'bug_id': bug_id,
'owner_info': json.dumps(owner_info),
'test_suites': json.dumps(update_test_suites.FetchCachedTestSuites()),
})
def _IsInt(x):
"""Returns True if the input can be parsed as an int."""
try:
int(x)
return True
except ValueError:
return False
def _GetSubTestsForAlerts(alert_list):
"""Gets subtest dict for list of alerts."""
subtests = {}
for alert in alert_list:
bot_name = alert['master'] + '/' + alert['bot']
testsuite = alert['testsuite']
if bot_name not in subtests:
subtests[bot_name] = {}
if testsuite not in subtests[bot_name]:
subtests[bot_name][testsuite] = list_tests.GetSubTests(
testsuite, [bot_name])
return subtests
def _GetOverlaps(anomalies, start, end):
"""Gets the minimum range for the list of anomalies.
Args:
anomalies: The list of anomalies.
start: The start revision.
end: The end revision.
Returns:
A list of anomalies.
"""
return [a for a in anomalies
if a.start_revision <= end and a.end_revision >= start]
def _GetOwnerInfo(alert_dicts):
"""Gets a list of owner info for list of alerts for bug with bisect result.
Test owners are retrieved by a set of master and test suite name from each
alert in alert_dicts.
Args:
alert_dicts: List of alert data dictionaries.
Returns:
A list of dictionary containing owner information.
"""
test_suite_paths = {'%s/%s' % (a['master'], a['testsuite'])
for a in alert_dicts}
owners = test_owner.GetOwners(test_suite_paths)
return [{'email': owner} for owner in owners]