14a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# Copyright 2015 The Chromium Authors. All rights reserved.
24a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# Use of this source code is governed by a BSD-style license that can be
34a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# found in the LICENSE file.
44a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
54a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair"""Provides the web interface for displaying an overview of alerts."""
64a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
74a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairimport json
8972bd9a9d2c6597a0145a675cbfa527d0510b048Martijn Coenenimport logging
94a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
104a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairfrom google.appengine.ext import ndb
114a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
124a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairfrom dashboard import email_template
134a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairfrom dashboard import request_handler
144a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairfrom dashboard import utils
154a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairfrom dashboard.models import anomaly
164a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairfrom dashboard.models import sheriff
174a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairfrom dashboard.models import stoppage_alert
184a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
194a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair_MAX_ANOMALIES_TO_COUNT = 5000
204a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair_MAX_ANOMALIES_TO_SHOW = 500
214a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair_MAX_STOPPAGE_ALERTS = 500
224a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
234a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
244a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairclass AlertsHandler(request_handler.RequestHandler):
254a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Shows an overview of recent anomalies for perf sheriffing."""
264a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
274a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def get(self):
28cef7893435aa41160dd1255c43cb8498279738ccChris Craik    """Renders the UI for listing alerts."""
29cef7893435aa41160dd1255c43cb8498279738ccChris Craik    self.RenderStaticHtml('alerts.html')
30cef7893435aa41160dd1255c43cb8498279738ccChris Craik
31cef7893435aa41160dd1255c43cb8498279738ccChris Craik  def post(self):
32cef7893435aa41160dd1255c43cb8498279738ccChris Craik    """Returns dynamic data for listing alerts in response to XHR.
334a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
344a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    Request parameters:
354a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      sheriff: The name of a sheriff (optional).
364a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      triaged: Whether to include triaged alerts (i.e. with a bug ID).
374a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      improvements: Whether to include improvement anomalies.
384a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
394a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    Outputs:
40cef7893435aa41160dd1255c43cb8498279738ccChris Craik      JSON data for an XHR request to show a table of alerts.
414a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """
424a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    sheriff_name = self.request.get('sheriff', 'Chromium Perf Sheriff')
434a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    sheriff_key = ndb.Key('Sheriff', sheriff_name)
44cef7893435aa41160dd1255c43cb8498279738ccChris Craik    if not _SheriffIsFound(sheriff_key):
45cef7893435aa41160dd1255c43cb8498279738ccChris Craik      self.response.out.write(json.dumps({
46cef7893435aa41160dd1255c43cb8498279738ccChris Craik          'error': 'Sheriff "%s" not found.' % sheriff_name
47cef7893435aa41160dd1255c43cb8498279738ccChris Craik      }))
48cef7893435aa41160dd1255c43cb8498279738ccChris Craik      return
49cef7893435aa41160dd1255c43cb8498279738ccChris Craik
504a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    include_improvements = bool(self.request.get('improvements'))
514a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    include_triaged = bool(self.request.get('triaged'))
524a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
534a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    anomaly_keys = _FetchAnomalyKeys(
544a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair        sheriff_key, include_improvements, include_triaged)
554a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    anomalies = ndb.get_multi(anomaly_keys[:_MAX_ANOMALIES_TO_SHOW])
564a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    stoppage_alerts = _FetchStoppageAlerts(sheriff_key, include_triaged)
574a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
58cef7893435aa41160dd1255c43cb8498279738ccChris Craik    values = {
59cef7893435aa41160dd1255c43cb8498279738ccChris Craik        'anomaly_list': AnomalyDicts(anomalies),
60cef7893435aa41160dd1255c43cb8498279738ccChris Craik        'stoppage_alert_list': StoppageAlertDicts(stoppage_alerts),
61cef7893435aa41160dd1255c43cb8498279738ccChris Craik        'sheriff_list': _GetSheriffList(),
62cef7893435aa41160dd1255c43cb8498279738ccChris Craik    }
63cef7893435aa41160dd1255c43cb8498279738ccChris Craik    self.GetDynamicVariables(values)
64cef7893435aa41160dd1255c43cb8498279738ccChris Craik    self.response.out.write(json.dumps(values))
65cef7893435aa41160dd1255c43cb8498279738ccChris Craik
66cef7893435aa41160dd1255c43cb8498279738ccChris Craik
67cef7893435aa41160dd1255c43cb8498279738ccChris Craikdef _SheriffIsFound(sheriff_key):
68cef7893435aa41160dd1255c43cb8498279738ccChris Craik  """Checks whether the sheriff can be found for the current user."""
69cef7893435aa41160dd1255c43cb8498279738ccChris Craik  try:
70cef7893435aa41160dd1255c43cb8498279738ccChris Craik    sheriff_entity = sheriff_key.get()
71cef7893435aa41160dd1255c43cb8498279738ccChris Craik  except AssertionError:
72cef7893435aa41160dd1255c43cb8498279738ccChris Craik    # This assertion is raised in InternalOnlyModel._post_get_hook,
73cef7893435aa41160dd1255c43cb8498279738ccChris Craik    # and indicates an internal-only Sheriff but an external user.
74cef7893435aa41160dd1255c43cb8498279738ccChris Craik    return False
75cef7893435aa41160dd1255c43cb8498279738ccChris Craik  return sheriff_entity is not None
764a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
774a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
784a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairdef _FetchAnomalyKeys(sheriff_key, include_improvements, include_triaged):
794a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Fetches the list of Anomaly keys that may be shown.
804a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
814a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  Args:
824a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    sheriff_key: The ndb.Key for the Sheriff to fetch alerts for.
834a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    include_improvements: Whether to include improvement Anomalies.
844a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    include_triaged: Whether to include Anomalies with a bug ID already set.
854a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
864a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  Returns:
874a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    A list of Anomaly keys, in reverse-chronological order.
884a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """
894a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  query = anomaly.Anomaly.query(
904a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      anomaly.Anomaly.sheriff == sheriff_key)
914a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
924a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  if not include_improvements:
934a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    query = query.filter(
944a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair        anomaly.Anomaly.is_improvement == False)
954a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
964a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  if not include_triaged:
974a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    query = query.filter(
984a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair        anomaly.Anomaly.bug_id == None)
994a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    query = query.filter(
1004a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair        anomaly.Anomaly.recovered == False)
1014a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1024a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  query = query.order(-anomaly.Anomaly.timestamp)
1034a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  return query.fetch(limit=_MAX_ANOMALIES_TO_COUNT, keys_only=True)
1044a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1054a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1064a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairdef _FetchStoppageAlerts(sheriff_key, include_triaged):
1074a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Fetches the list of Anomaly keys that may be shown.
1084a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1094a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  Args:
1104a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    sheriff_key: The ndb.Key for the Sheriff to fetch alerts for.
1114a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    include_triaged: Whether to include alerts with a bug ID.
1124a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1134a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  Returns:
1144a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    A list of StoppageAlert entities, in reverse-chronological order.
1154a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """
1164a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  query = stoppage_alert.StoppageAlert.query(
1174a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      stoppage_alert.StoppageAlert.sheriff == sheriff_key)
1184a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1194a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  if not include_triaged:
1204a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    query = query.filter(
1214a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair        stoppage_alert.StoppageAlert.bug_id == None)
1224a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    query = query.filter(
1234a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair        stoppage_alert.StoppageAlert.recovered == False)
1244a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1254a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  query = query.order(-stoppage_alert.StoppageAlert.timestamp)
1264a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  return query.fetch(limit=_MAX_STOPPAGE_ALERTS)
1274a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1284a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1294a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairdef _GetSheriffList():
1304a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Returns a list of sheriff names for all sheriffs in the datastore."""
1314a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  sheriff_keys = sheriff.Sheriff.query().fetch(keys_only=True)
1324a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  return [key.string_id() for key in sheriff_keys]
1334a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1344a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1354a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairdef AnomalyDicts(anomalies):
1364a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Makes a list of dicts with properties of Anomaly entities."""
1374a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  bisect_statuses = _GetBisectStatusDict(anomalies)
1384a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  return [GetAnomalyDict(a, bisect_statuses.get(a.bug_id)) for a in anomalies]
1394a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1404a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1414a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairdef StoppageAlertDicts(stoppage_alerts):
1424a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Makes a list of dicts with properties of StoppageAlert entities."""
1434a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  return [_GetStoppageAlertDict(a) for a in stoppage_alerts]
1444a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1454a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1464a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairdef GetAnomalyDict(anomaly_entity, bisect_status=None):
1474a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Returns a dictionary for an Anomaly which can be encoded as JSON.
1484a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1494a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  Args:
1504a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    anomaly_entity: An Anomaly entity.
1514a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    bisect_status: String status of bisect run.
1524a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1534a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  Returns:
1544a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    A dictionary which is safe to be encoded as JSON.
1554a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """
1564a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  alert_dict = _AlertDict(anomaly_entity)
1574a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  alert_dict.update({
1584a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'median_after_anomaly': anomaly_entity.median_after_anomaly,
1594a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'median_before_anomaly': anomaly_entity.median_before_anomaly,
1604a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'percent_changed': '%s' % anomaly_entity.GetDisplayPercentChanged(),
1614a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'improvement': anomaly_entity.is_improvement,
1624a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'bisect_status': bisect_status,
1634a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'recovered': anomaly_entity.recovered,
1644a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  })
1654a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  return alert_dict
1664a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1674a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1684a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairdef _GetStoppageAlertDict(stoppage_alert_entity):
1694a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Returns a dictionary of properties of a stoppage alert."""
1704a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  alert_dict = _AlertDict(stoppage_alert_entity)
171972bd9a9d2c6597a0145a675cbfa527d0510b048Martijn Coenen  last_row_date = stoppage_alert_entity.last_row_date
172972bd9a9d2c6597a0145a675cbfa527d0510b048Martijn Coenen  if not last_row_date:
173972bd9a9d2c6597a0145a675cbfa527d0510b048Martijn Coenen    logging.error('No date for StoppageAlert:\n%s', stoppage_alert_entity)
1744a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  alert_dict.update({
1754a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'mail_sent': stoppage_alert_entity.mail_sent,
176972bd9a9d2c6597a0145a675cbfa527d0510b048Martijn Coenen      'last_row_date': str(last_row_date.date()) if last_row_date else 'N/A',
1774a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'recovered': stoppage_alert_entity.recovered,
1784a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  })
1794a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  return alert_dict
1804a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1814a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1824a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairdef _AlertDict(alert_entity):
1834a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Returns a base dictionary with properties common to all alerts."""
1844a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  test_path = utils.TestPath(alert_entity.test)
1854a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  test_path_parts = test_path.split('/')
1864a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  dashboard_link = email_template.GetReportPageLink(
1874a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      test_path, rev=alert_entity.end_revision, add_protocol_and_host=False)
1884a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  return {
1894a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'key': alert_entity.key.urlsafe(),
1904a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'group': alert_entity.group.urlsafe() if alert_entity.group else None,
1914a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'start_revision': alert_entity.start_revision,
1924a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'end_revision': alert_entity.end_revision,
1934a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'date': str(alert_entity.timestamp.date()),
1944a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'master': test_path_parts[0],
1954a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'bot': test_path_parts[1],
1964a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'testsuite': test_path_parts[2],
1974a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'test': '/'.join(test_path_parts[3:]),
1984a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'bug_id': alert_entity.bug_id,
1994a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      'dashboard_link': dashboard_link,
2004a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  }
2014a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2024a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2034a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairdef _GetBisectStatusDict(anomalies):
2044a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Returns a dictionary of bug ID to bisect status string."""
2054a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  bug_id_list = {a.bug_id for a in anomalies if a.bug_id > 0}
2064a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  bugs = ndb.get_multi(ndb.Key('Bug', b) for b in bug_id_list)
2074a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  return {b.key.id(): b.latest_bisect_status for b in bugs if b}
208