blob: 8fafbd778eefe08afa37a0db9ddd2e665bb00d25 [file] [log] [blame]
# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Helpers for handling Buganizer migration.
Monorail is being deprecated and replaced by Buganizer in 2024. Migration
happens by projects. Chromeperf needs to support both Monorail and Buganizer
during the migration until the last project we supported is fully migrated.
Before the first migration happens, we will keep the consumers untouched
in the Monorail fashion. The requests and responses to and from Buganizer will
be reconciled to the Monorail format on perf_issue_service.
"""
import logging
from application.clients import monorail_client
# ============ mapping helpers start ============
# The mapping here are for ad hoc testing. The real mappings will be different
# from project to project and will be added in future CLs.
# components, labels and hotlists for testing:
# Buganizer component 1325852 is "ChromePerf testing"
# Monorail labels "chromeperf-test" and "chromeperf-test-2"
# Buganizer hotlists 5141966 and 5142065
# Mappings from a monorail component to a buganizer component id.
COMPONENT_MAP_CR2B = {
# Chromium
'Blink>Accessibility':
1456587,
'Blink>Bindings':
1456743,
'Blink>CSS':
1456329,
'Blink>DOM':
1456718,
'Blink>DOM>ShadowDOM':
1456822,
'Blink>HTML>Parser':
1456175,
'Blink>JavaScript':
1456824,
'Blink>JavaScript>GarbageCollection':
1456490,
'Blink>JavaScript>WebAssembly':
1456332,
'Blink>Layout':
1456721,
'Blink>Paint':
1456440,
'Blink>ServiceWorker':
1456150,
'Blink>SVG':
1456414,
'Blink>Storage':
1456519,
'Blink>Storage>IndexedDB':
1456771,
'Blink>WebRTC>Perf':
1456207,
'Fuchsia':
1456675,
'Internals>GPU>ANGLE':
1456215,
'Internals>GPU>Metrics':
1456424,
'Internals>Network>Library':
1456244,
'Internals>Power':
1456482,
'Internals>Skia':
1457031,
'Internals>XR':
1456683,
'Mobile>Fundamentals>SystemHealth':
1457150,
'Platform>Apps>ARC':
1457293,
'Platform>DevTools>WebAssembly':
1456350,
'Public Trackers > Chromium Public Trackers > Chromium > Infra > PGO':
1456420,
'Speed>BinarySize>Android':
1457059,
'Speed>BinarySize>Desktop':
1456597,
'Speed>Dashboard':
1457390,
'Speed>Regressions':
1457332,
'Test>Telemetry':
1456742,
'Speed>Tracing':
1457213,
'UI>Browser>AdFilter':
1456114,
'UI>Browser>NewTabPage':
1457163,
'UI>Browser>Omnibox':
1457180,
'UI>Browser>TopChrome':
1457234,
# A new component is required in Buganizer after migration.
# This mapping is used for migration time and will be removed.
'Internals>Media':
1456190,
# Fuchsia
# Trackers > Fuchsia > UntriagedPerformanceAlerts
'UntriagedPerformanceAlerts':
1454999,
# WebRTC
'Audio':
1565681,
'Perf>Arcturus':
1565993,
'Perf>Canopus':
1565890,
'Perf>Migration':
1565319,
'Perf>Video':
1565864,
'Stats':
1565682,
# Test only. Remove after migration.
'ChromePerf testing':
1325852
}
# Mappings from a buganizer component id to a monorail project
COMPONENT_MAP_B2CR = {
# Chromium
'1456587': 'chromium',
'1456743': 'chromium',
'1456329': 'chromium',
'1456718': 'chromium',
'1456822': 'chromium',
'1456175': 'chromium',
'1456824': 'chromium',
'1456490': 'chromium',
'1456332': 'chromium',
'1456721': 'chromium',
'1456440': 'chromium',
'1456414': 'chromium',
'1456519': 'chromium',
'1456771': 'chromium',
'1456207': 'chromium',
'1456675': 'chromium',
'1456215': 'chromium',
'1456424': 'chromium',
'1456244': 'chromium',
'1456482': 'chromium',
'1457031': 'chromium',
'1456683': 'chromium',
'1457150': 'chromium',
'1457293': 'chromium',
'1456350': 'chromium',
'1457059': 'chromium',
'1456597': 'chromium',
'1457332': 'chromium',
'1456742': 'chromium',
'1457213': 'chromium',
'1456114': 'chromium',
'1457163': 'chromium',
'1457180': 'chromium',
'1457234': 'chromium',
'1456150': 'chromium',
'1456190': 'chromium',
'1456420': 'chromium',
'1457390': 'chromium',
# Fuchsia
'1454999' : 'fuchsia',
# webrtc
'1565681': 'webrtc',
'1565993': 'webrtc',
'1565890': 'webrtc',
'1565319': 'webrtc',
'1565864': 'webrtc',
'1565682': 'webrtc',
# Test only. Remove after migration.
'1325852' : 'chromium'
}
# Mapping from monorail project to all the related components in buganizer
PROJECT_MAP_CR2B = {
# Chromium
'chromium': [
'1456114', '1456150', '1456175', '1456207', '1456215', '1456244', '1456329',
'1456332', '1456350', '1456414', '1456424', '1456440', '1456482', '1456490',
'1456519', '1456587', '1456597', '1456675', '1456683', '1456718', '1456721',
'1456742', '1456743', '1456771', '1456822', '1456824', '1457031', '1457059',
'1457150', '1457163', '1457180', '1457213', '1457234', '1457293', '1457332',
'1456190', '1457390', '1456420', '1325852'
],
# Fuchsia
'fuchsia': ['1454999'],
# webrtc
'webrtc': ['1565681', '1565993', '1565890', '1565319', '1565864', '1565682'],
}
# Mappings from a monorail label to a buganizer hotlist, if any.
LABEL_MAP_CR2B = {
# Chromium
'Type-Bug-Regression': '5438261',
'Chromeperf-Auto-NeedsAttention': '5670048',
# Fuchsia
'Performance': '5424295', # Performance
# Test only. Remove after migration.
'chromeperf-test': '5141966',
'chromeperf-test-2': '5142065'
}
# Mappings from a buganizer hotlist to a monorail label.
HOTLIST_MAP_B2CR = {
hotlist: label for label, hotlist in LABEL_MAP_CR2B.items()
}
def FindBuganizerComponentId(monorail_component):
return COMPONENT_MAP_CR2B.get(monorail_component, 1325852)
def FindBuganizerComponents(monorail_project_name):
"""return a list of components in buganizer based on the monorail project
The current implementation is ad hoc as the component mappings are not
fully set up on buganizer yet.
"""
return PROJECT_MAP_CR2B.get(monorail_project_name, [])
def FindMonorailProject(buganizer_component_id):
return COMPONENT_MAP_B2CR.get(buganizer_component_id, '')
def FindBuganizerHotlists(monorail_labels):
'''Find the hotlist mappings for monorail labels.
Some of the Monorail labels are mapped to Buganizer hotlists. However,
some of them are not, and they will be copied over to a custome field
in Buganizer. For Fuchsia project, the custome field is 'Monorail labels',
for other project, the custome field is 'Chromium labels'.
Args:
monorail_labels: the labels in Monorail
Returns:
hotlists: the hotlists in Buganizer
extra_labels: the Monorail labels with no mapping in Buganizer
'''
hotlists = []
extra_labels = []
for label in monorail_labels:
hotlist = _FindBuganizerHotlist(label)
if hotlist:
hotlists.append(hotlist)
else:
extra_labels.append(label)
logging.debug(
'[PerfIssueService] labels (%s) -> hotlists (%s). Leftover: %s',
monorail_labels, hotlists, extra_labels)
return hotlists, extra_labels
def _FindBuganizerHotlist(monorail_label):
return LABEL_MAP_CR2B.get(monorail_label, None)
def _FindMonorailLabel(buganizer_hotlist):
return HOTLIST_MAP_B2CR.get(buganizer_hotlist, None)
def _FindMonorailStatus(buganizer_status):
if buganizer_status == 'NEW':
return 'Unconfirmed'
elif buganizer_status == 'ASSIGNED':
return "Assigned"
elif buganizer_status == 'ACCEPTED':
return 'Started'
elif buganizer_status == 'FIXED':
return 'Fixed'
elif buganizer_status == 'VERIFIED':
return 'Verified'
elif buganizer_status == 'DUPLICATE':
return 'Duplicate'
elif buganizer_status == 'OBSOLETE':
return 'WontFix'
return 'Untriaged'
def FindBuganizerStatus(monorail_status):
if monorail_status in ('Unconfirmed', 'Untriaged', 'Available'):
return 'NEW'
elif monorail_status == 'Assigned':
return "ASSIGNED"
elif monorail_status == 'Started':
return 'ACCEPTED'
elif monorail_status == 'Fixed':
return 'FIXED'
elif monorail_status == 'Verified':
return 'VERIFIED'
elif monorail_status == 'WontFix':
return 'OBSOLETE'
elif monorail_status == 'Duplicate':
return 'DUPLICATE'
return 'STATUS_UNSPECIFIED'
# ============ mapping helpers end ============
def GetBuganizerStatusUpdate(issue_update, status_enum):
''' Get the status from an issue update
In Buganizer, each update event is saved in an issueUpdate struct like:
{
"author": {
"emailAddress": "wenbinzhang@google.com"
},
"timestamp": "2023-07-28T22: 17: 34.721Z",
...
"fieldUpdates": [
{
"field": "status",
"singleValueUpdate": {
"oldValue": {
"@type": "type.googleapis.com/google.protobuf.Int32Value",
"value": 2
},
"newValue": {
"@type": "type.googleapis.com/google.protobuf.Int32Value",
"value": 4
}
}
}
],
...
"version": 5,
"issueId": "285172796"
}
Noticed that each update on the UI can have multiple operations, but each of
operations is a single issue update. So far we only use the 'status' value,
thus the other updates like priority change are not returned here.
'''
for field_update in issue_update.get('fieldUpdates', []):
if field_update.get('field') == 'status':
new_status = field_update.get('singleValueUpdate').get('newValue').get('value')
# The current status returned from issueUpdate is integer, which is
# different from the definition in the discovery doc.
# I'm using service._schema.get() to load the schema in JSON.
if isinstance(new_status, int):
new_status = status_enum[new_status]
status_update = {
'status': _FindMonorailStatus(new_status)
}
return status_update
return None
def LoadPriorityFromMonorailLabels(monorail_labels):
''' Load the priority from monorail labels if it is set
In Monorail, a label like 'Pri-X' is used set the issue's priority to X.
X can be range from 0 to 4. Currently Monorail scan the labels in order
and set the priority as it sees any. Thus, the last X will be kept. Here
we changed the logic to keep the highest level (lowest value.)
Args:
monorail_labels: a list of labels in monorail fashion.
Returns:
the priority from the label if any, otherwise 2 by default.
'''
if monorail_labels:
for label in monorail_labels:
if label.startswith('Pri-'):
label_priority = int(label[4])
if 0 <= label_priority < 5:
return label_priority
return 2
def ReconcileBuganizerIssue(buganizer_issue):
'''Reconcile a Buganizer issue into the Monorail format
During the Buganizer migration, we try to minimize the changes on the
consumers of the issues. Thus, we will reconcile the results into the
exising Monorail format before returning the results.
'''
monorail_issue = {}
issue_state = buganizer_issue.get('issueState')
project_id = FindMonorailProject(issue_state['componentId'])
monorail_issue['projectId'] = project_id
monorail_issue['id'] = buganizer_issue['issueId']
buganizer_status = issue_state.get('status')
if buganizer_status in ('NEW', 'ASSIGNED', 'ACCEPTED'):
monorail_issue['state'] = 'open'
else:
monorail_issue['state'] = 'closed'
monorail_issue['status'] = _FindMonorailStatus(buganizer_status)
monorail_author = {
'name': issue_state['reporter']
}
monorail_issue['author'] = monorail_author
monorail_issue['summary'] = issue_state['title']
monorail_issue['owner'] = issue_state.get('assignee', None)
hotlist_ids = buganizer_issue.get('hotlistIds', [])
label_names = [_FindMonorailLabel(hotlist_id) for hotlist_id in hotlist_ids]
custom_field_id = GetCustomFieldId(project_id)
all_custom_fields = issue_state.get('customFields', [])
custom_labels = []
for custom_field in all_custom_fields:
if custom_field['customFieldId'] == str(custom_field_id):
custom_labels = custom_field['repeatedTextValue'].get('values', [])
label_names = list(set(label_names) | set(custom_labels))
monorail_issue['labels'] = [label for label in label_names if label]
return monorail_issue
def FindBuganizerIdByMonorailId(monorail_project, monorail_id):
'''Try to find the buganizer id using the monorail id
After a monorail issue is migrated to buganizer, the buganizer id will
be populated to the monorail issue record, in a property 'migratedId'.
'''
logging.debug('Looking for b/ id for crbug %s in %s', monorail_id, monorail_project)
if int(monorail_id) < 2000000:
# This is a hack to handle the use case that:
# - we have the monorail issue id in our database
# - the issue is migrated to buganizer
# - we need to update the issue but we don't know the id on buganizer
# Assuming all monorail id are less than 2000000, trying to access an
# issue using buganizer client and a monorail id means the project has
# been migrated.
# In this case, we should find the buganizer id first.
client = monorail_client.MonorailClient()
issue = client.GetIssue(
issue_id=monorail_id,
project=monorail_project)
buganizer_id = issue.get('migrated_id', None)
logging.debug('Migrated ID %s found for %s/%s.',
buganizer_id, monorail_project, monorail_id)
if not buganizer_id:
err_msg = 'Cannot find the migrated id for crbug %s in %s' % (
monorail_id, monorail_project)
logging.error(err_msg)
return buganizer_id
return monorail_id
def GetCustomField(monorail_project):
'''Get the custom field name based on monorail project.
More context in FindBuganizerHotlists()
'''
if monorail_project == 'fuchsia':
return 'customfield1241047'
elif monorail_project == 'webrtc':
return 'customfield1308691'
return 'customfield1223031'
def GetCustomFieldId(monorail_project):
'''Get the custom field ID based on monorail project.
More context in FindBuganizerHotlists()
'''
if monorail_project == 'fuchsia':
return 1241047
elif monorail_project == 'webrtc':
return 1308691
return 1223031