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 displaying an overview of alerts."""
6
7import json
8import logging
9
10from google.appengine.ext import ndb
11
12from dashboard import email_template
13from dashboard import request_handler
14from dashboard import utils
15from dashboard.models import anomaly
16from dashboard.models import sheriff
17from dashboard.models import stoppage_alert
18
19_MAX_ANOMALIES_TO_COUNT = 5000
20_MAX_ANOMALIES_TO_SHOW = 500
21_MAX_STOPPAGE_ALERTS = 500
22
23
24class AlertsHandler(request_handler.RequestHandler):
25  """Shows an overview of recent anomalies for perf sheriffing."""
26
27  def get(self):
28    """Renders the UI for listing alerts."""
29    self.RenderStaticHtml('alerts.html')
30
31  def post(self):
32    """Returns dynamic data for listing alerts in response to XHR.
33
34    Request parameters:
35      sheriff: The name of a sheriff (optional).
36      triaged: Whether to include triaged alerts (i.e. with a bug ID).
37      improvements: Whether to include improvement anomalies.
38
39    Outputs:
40      JSON data for an XHR request to show a table of alerts.
41    """
42    sheriff_name = self.request.get('sheriff', 'Chromium Perf Sheriff')
43    sheriff_key = ndb.Key('Sheriff', sheriff_name)
44    if not _SheriffIsFound(sheriff_key):
45      self.response.out.write(json.dumps({
46          'error': 'Sheriff "%s" not found.' % sheriff_name
47      }))
48      return
49
50    include_improvements = bool(self.request.get('improvements'))
51    include_triaged = bool(self.request.get('triaged'))
52
53    anomaly_keys = _FetchAnomalyKeys(
54        sheriff_key, include_improvements, include_triaged)
55    anomalies = ndb.get_multi(anomaly_keys[:_MAX_ANOMALIES_TO_SHOW])
56    stoppage_alerts = _FetchStoppageAlerts(sheriff_key, include_triaged)
57
58    values = {
59        'anomaly_list': AnomalyDicts(anomalies),
60        'stoppage_alert_list': StoppageAlertDicts(stoppage_alerts),
61        'sheriff_list': _GetSheriffList(),
62    }
63    self.GetDynamicVariables(values)
64    self.response.out.write(json.dumps(values))
65
66
67def _SheriffIsFound(sheriff_key):
68  """Checks whether the sheriff can be found for the current user."""
69  try:
70    sheriff_entity = sheriff_key.get()
71  except AssertionError:
72    # This assertion is raised in InternalOnlyModel._post_get_hook,
73    # and indicates an internal-only Sheriff but an external user.
74    return False
75  return sheriff_entity is not None
76
77
78def _FetchAnomalyKeys(sheriff_key, include_improvements, include_triaged):
79  """Fetches the list of Anomaly keys that may be shown.
80
81  Args:
82    sheriff_key: The ndb.Key for the Sheriff to fetch alerts for.
83    include_improvements: Whether to include improvement Anomalies.
84    include_triaged: Whether to include Anomalies with a bug ID already set.
85
86  Returns:
87    A list of Anomaly keys, in reverse-chronological order.
88  """
89  query = anomaly.Anomaly.query(
90      anomaly.Anomaly.sheriff == sheriff_key)
91
92  if not include_improvements:
93    query = query.filter(
94        anomaly.Anomaly.is_improvement == False)
95
96  if not include_triaged:
97    query = query.filter(
98        anomaly.Anomaly.bug_id == None)
99    query = query.filter(
100        anomaly.Anomaly.recovered == False)
101
102  query = query.order(-anomaly.Anomaly.timestamp)
103  return query.fetch(limit=_MAX_ANOMALIES_TO_COUNT, keys_only=True)
104
105
106def _FetchStoppageAlerts(sheriff_key, include_triaged):
107  """Fetches the list of Anomaly keys that may be shown.
108
109  Args:
110    sheriff_key: The ndb.Key for the Sheriff to fetch alerts for.
111    include_triaged: Whether to include alerts with a bug ID.
112
113  Returns:
114    A list of StoppageAlert entities, in reverse-chronological order.
115  """
116  query = stoppage_alert.StoppageAlert.query(
117      stoppage_alert.StoppageAlert.sheriff == sheriff_key)
118
119  if not include_triaged:
120    query = query.filter(
121        stoppage_alert.StoppageAlert.bug_id == None)
122    query = query.filter(
123        stoppage_alert.StoppageAlert.recovered == False)
124
125  query = query.order(-stoppage_alert.StoppageAlert.timestamp)
126  return query.fetch(limit=_MAX_STOPPAGE_ALERTS)
127
128
129def _GetSheriffList():
130  """Returns a list of sheriff names for all sheriffs in the datastore."""
131  sheriff_keys = sheriff.Sheriff.query().fetch(keys_only=True)
132  return [key.string_id() for key in sheriff_keys]
133
134
135def AnomalyDicts(anomalies):
136  """Makes a list of dicts with properties of Anomaly entities."""
137  bisect_statuses = _GetBisectStatusDict(anomalies)
138  return [GetAnomalyDict(a, bisect_statuses.get(a.bug_id)) for a in anomalies]
139
140
141def StoppageAlertDicts(stoppage_alerts):
142  """Makes a list of dicts with properties of StoppageAlert entities."""
143  return [_GetStoppageAlertDict(a) for a in stoppage_alerts]
144
145
146def GetAnomalyDict(anomaly_entity, bisect_status=None):
147  """Returns a dictionary for an Anomaly which can be encoded as JSON.
148
149  Args:
150    anomaly_entity: An Anomaly entity.
151    bisect_status: String status of bisect run.
152
153  Returns:
154    A dictionary which is safe to be encoded as JSON.
155  """
156  alert_dict = _AlertDict(anomaly_entity)
157  alert_dict.update({
158      'median_after_anomaly': anomaly_entity.median_after_anomaly,
159      'median_before_anomaly': anomaly_entity.median_before_anomaly,
160      'percent_changed': '%s' % anomaly_entity.GetDisplayPercentChanged(),
161      'improvement': anomaly_entity.is_improvement,
162      'bisect_status': bisect_status,
163      'recovered': anomaly_entity.recovered,
164  })
165  return alert_dict
166
167
168def _GetStoppageAlertDict(stoppage_alert_entity):
169  """Returns a dictionary of properties of a stoppage alert."""
170  alert_dict = _AlertDict(stoppage_alert_entity)
171  last_row_date = stoppage_alert_entity.last_row_date
172  if not last_row_date:
173    logging.error('No date for StoppageAlert:\n%s', stoppage_alert_entity)
174  alert_dict.update({
175      'mail_sent': stoppage_alert_entity.mail_sent,
176      'last_row_date': str(last_row_date.date()) if last_row_date else 'N/A',
177      'recovered': stoppage_alert_entity.recovered,
178  })
179  return alert_dict
180
181
182def _AlertDict(alert_entity):
183  """Returns a base dictionary with properties common to all alerts."""
184  test_path = utils.TestPath(alert_entity.test)
185  test_path_parts = test_path.split('/')
186  dashboard_link = email_template.GetReportPageLink(
187      test_path, rev=alert_entity.end_revision, add_protocol_and_host=False)
188  return {
189      'key': alert_entity.key.urlsafe(),
190      'group': alert_entity.group.urlsafe() if alert_entity.group else None,
191      'start_revision': alert_entity.start_revision,
192      'end_revision': alert_entity.end_revision,
193      'date': str(alert_entity.timestamp.date()),
194      'master': test_path_parts[0],
195      'bot': test_path_parts[1],
196      'testsuite': test_path_parts[2],
197      'test': '/'.join(test_path_parts[3:]),
198      'bug_id': alert_entity.bug_id,
199      'dashboard_link': dashboard_link,
200  }
201
202
203def _GetBisectStatusDict(anomalies):
204  """Returns a dictionary of bug ID to bisect status string."""
205  bug_id_list = {a.bug_id for a in anomalies if a.bug_id > 0}
206  bugs = ndb.get_multi(ndb.Key('Bug', b) for b in bug_id_list)
207  return {b.key.id(): b.latest_bisect_status for b in bugs if b}
208