1 #!/usr/bin/env python 2 # 3 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 4 # Use of this source code is governed by a BSD-style license that can be 5 # found in the LICENSE file. 6 7 """Extract UserMetrics "actions" strings from the Chrome source. 8 9 This program generates the list of known actions we expect to see in the 10 user behavior logs. It walks the Chrome source, looking for calls to 11 UserMetrics functions, extracting actions and warning on improper calls, 12 as well as generating the lists of possible actions in situations where 13 there are many possible actions. 14 15 See also: 16 base/metrics/user_metrics.h 17 http://wiki.corp.google.com/twiki/bin/view/Main/ChromeUserExperienceMetrics 18 19 After extracting all actions, the content will go through a pretty print 20 function to make sure it's well formatted. If the file content needs to be 21 changed, a window will be prompted asking for user's consent. The old version 22 will also be saved in a backup file. 23 """ 24 25 __author__ = 'evanm (Evan Martin)' 26 27 from HTMLParser import HTMLParser 28 import logging 29 import os 30 import re 31 import shutil 32 import sys 33 from xml.dom import minidom 34 35 import print_style 36 37 sys.path.insert(1, os.path.join(sys.path[0], '..', '..', 'python')) 38 from google import path_utils 39 40 # Import the metrics/common module for pretty print xml. 41 sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'common')) 42 import diff_util 43 import pretty_print_xml 44 45 # Files that are known to use content::RecordComputedAction(), which means 46 # they require special handling code in this script. 47 # To add a new file, add it to this list and add the appropriate logic to 48 # generate the known actions to AddComputedActions() below. 49 KNOWN_COMPUTED_USERS = ( 50 'back_forward_menu_model.cc', 51 'options_page_view.cc', 52 'render_view_host.cc', # called using webkit identifiers 53 'user_metrics.cc', # method definition 54 'new_tab_ui.cc', # most visited clicks 1-9 55 'extension_metrics_module.cc', # extensions hook for user metrics 56 'safe_browsing_blocking_page.cc', # various interstitial types and actions 57 'language_options_handler_common.cc', # languages and input methods in CrOS 58 'cros_language_options_handler.cc', # languages and input methods in CrOS 59 'about_flags.cc', # do not generate a warning; see AddAboutFlagsActions() 60 'external_metrics.cc', # see AddChromeOSActions() 61 'core_options_handler.cc', # see AddWebUIActions() 62 'browser_render_process_host.cc', # see AddRendererActions() 63 'render_thread_impl.cc', # impl of RenderThread::RecordComputedAction() 64 'render_process_host_impl.cc', # browser side impl for 65 # RenderThread::RecordComputedAction() 66 'mock_render_thread.cc', # mock of RenderThread::RecordComputedAction() 67 'ppb_pdf_impl.cc', # see AddClosedSourceActions() 68 'pepper_pdf_host.cc', # see AddClosedSourceActions() 69 'key_systems_support_uma.cc', # See AddKeySystemSupportActions() 70 ) 71 72 # Language codes used in Chrome. The list should be updated when a new 73 # language is added to app/l10n_util.cc, as follows: 74 # 75 # % (cat app/l10n_util.cc | \ 76 # perl -n0e 'print $1 if /kAcceptLanguageList.*?\{(.*?)\}/s' | \ 77 # perl -nle 'print $1, if /"(.*)"/'; echo 'es-419') | \ 78 # sort | perl -pe "s/(.*)\n/'\$1', /" | \ 79 # fold -w75 -s | perl -pe 's/^/ /;s/ $//'; echo 80 # 81 # The script extracts language codes from kAcceptLanguageList, but es-419 82 # (Spanish in Latin America) is an exception. 83 LANGUAGE_CODES = ( 84 'af', 'am', 'ar', 'az', 'be', 'bg', 'bh', 'bn', 'br', 'bs', 'ca', 'co', 85 'cs', 'cy', 'da', 'de', 'de-AT', 'de-CH', 'de-DE', 'el', 'en', 'en-AU', 86 'en-CA', 'en-GB', 'en-NZ', 'en-US', 'en-ZA', 'eo', 'es', 'es-419', 'et', 87 'eu', 'fa', 'fi', 'fil', 'fo', 'fr', 'fr-CA', 'fr-CH', 'fr-FR', 'fy', 88 'ga', 'gd', 'gl', 'gn', 'gu', 'ha', 'haw', 'he', 'hi', 'hr', 'hu', 'hy', 89 'ia', 'id', 'is', 'it', 'it-CH', 'it-IT', 'ja', 'jw', 'ka', 'kk', 'km', 90 'kn', 'ko', 'ku', 'ky', 'la', 'ln', 'lo', 'lt', 'lv', 'mk', 'ml', 'mn', 91 'mo', 'mr', 'ms', 'mt', 'nb', 'ne', 'nl', 'nn', 'no', 'oc', 'om', 'or', 92 'pa', 'pl', 'ps', 'pt', 'pt-BR', 'pt-PT', 'qu', 'rm', 'ro', 'ru', 'sd', 93 'sh', 'si', 'sk', 'sl', 'sn', 'so', 'sq', 'sr', 'st', 'su', 'sv', 'sw', 94 'ta', 'te', 'tg', 'th', 'ti', 'tk', 'to', 'tr', 'tt', 'tw', 'ug', 'uk', 95 'ur', 'uz', 'vi', 'xh', 'yi', 'yo', 'zh', 'zh-CN', 'zh-TW', 'zu', 96 ) 97 98 # Input method IDs used in Chrome OS. The list should be updated when a 99 # new input method is added to 100 # chromeos/ime/input_methods.txt in the Chrome tree, as 101 # follows: 102 # 103 # % sort chromeos/ime/input_methods.txt | \ 104 # perl -ne "print \"'\$1', \" if /^([^#]+?)\s/" | \ 105 # fold -w75 -s | perl -pe 's/^/ /;s/ $//'; echo 106 # 107 # The script extracts input method IDs from input_methods.txt. 108 INPUT_METHOD_IDS = ( 109 'xkb:am:phonetic:arm', 'xkb:be::fra', 'xkb:be::ger', 'xkb:be::nld', 110 'xkb:bg::bul', 'xkb:bg:phonetic:bul', 'xkb:br::por', 'xkb:by::bel', 111 'xkb:ca::fra', 'xkb:ca:eng:eng', 'xkb:ca:multix:fra', 'xkb:ch::ger', 112 'xkb:ch:fr:fra', 'xkb:cz::cze', 'xkb:cz:qwerty:cze', 'xkb:de::ger', 113 'xkb:de:neo:ger', 'xkb:dk::dan', 'xkb:ee::est', 'xkb:es::spa', 114 'xkb:es:cat:cat', 'xkb:fi::fin', 'xkb:fr::fra', 'xkb:gb:dvorak:eng', 115 'xkb:gb:extd:eng', 'xkb:ge::geo', 'xkb:gr::gre', 'xkb:hr::scr', 116 'xkb:hu::hun', 'xkb:il::heb', 'xkb:is::ice', 'xkb:it::ita', 'xkb:jp::jpn', 117 'xkb:latam::spa', 'xkb:lt::lit', 'xkb:lv:apostrophe:lav', 'xkb:mn::mon', 118 'xkb:no::nob', 'xkb:pl::pol', 'xkb:pt::por', 'xkb:ro::rum', 'xkb:rs::srp', 119 'xkb:ru::rus', 'xkb:ru:phonetic:rus', 'xkb:se::swe', 'xkb:si::slv', 120 'xkb:sk::slo', 'xkb:tr::tur', 'xkb:ua::ukr', 'xkb:us::eng', 121 'xkb:us:altgr-intl:eng', 'xkb:us:colemak:eng', 'xkb:us:dvorak:eng', 122 'xkb:us:intl:eng', 123 ) 124 125 # The path to the root of the repository. 126 REPOSITORY_ROOT = os.path.join(path_utils.ScriptDir(), '..', '..', '..') 127 128 number_of_files_total = 0 129 130 # Tags that need to be inserted to each 'action' tag and their default content. 131 TAGS = {'description': 'Please enter the description of the metric.', 132 'owner': ('Please list the metric\'s owners. Add more owner tags as ' 133 'needed.')} 134 135 136 def AddComputedActions(actions): 137 """Add computed actions to the actions list. 138 139 Arguments: 140 actions: set of actions to add to. 141 """ 142 143 # Actions for back_forward_menu_model.cc. 144 for dir in ('BackMenu_', 'ForwardMenu_'): 145 actions.add(dir + 'ShowFullHistory') 146 actions.add(dir + 'Popup') 147 for i in range(1, 20): 148 actions.add(dir + 'HistoryClick' + str(i)) 149 actions.add(dir + 'ChapterClick' + str(i)) 150 151 # Actions for new_tab_ui.cc. 152 for i in range(1, 10): 153 actions.add('MostVisited%d' % i) 154 155 # Actions for safe_browsing_blocking_page.cc. 156 for interstitial in ('Phishing', 'Malware', 'Multiple'): 157 for action in ('Show', 'Proceed', 'DontProceed', 'ForcedDontProceed'): 158 actions.add('SBInterstitial%s%s' % (interstitial, action)) 159 160 # Actions for language_options_handler.cc (Chrome OS specific). 161 for input_method_id in INPUT_METHOD_IDS: 162 actions.add('LanguageOptions_DisableInputMethod_%s' % input_method_id) 163 actions.add('LanguageOptions_EnableInputMethod_%s' % input_method_id) 164 for language_code in LANGUAGE_CODES: 165 actions.add('LanguageOptions_UiLanguageChange_%s' % language_code) 166 actions.add('LanguageOptions_SpellCheckLanguageChange_%s' % language_code) 167 168 def AddWebKitEditorActions(actions): 169 """Add editor actions from editor_client_impl.cc. 170 171 Arguments: 172 actions: set of actions to add to. 173 """ 174 action_re = re.compile(r'''\{ [\w']+, +\w+, +"(.*)" +\},''') 175 176 editor_file = os.path.join(REPOSITORY_ROOT, 'webkit', 'api', 'src', 177 'EditorClientImpl.cc') 178 for line in open(editor_file): 179 match = action_re.search(line) 180 if match: # Plain call to RecordAction 181 actions.add(match.group(1)) 182 183 def AddClosedSourceActions(actions): 184 """Add actions that are in code which is not checked out by default 185 186 Arguments 187 actions: set of actions to add to. 188 """ 189 actions.add('PDF.FitToHeightButton') 190 actions.add('PDF.FitToWidthButton') 191 actions.add('PDF.LoadFailure') 192 actions.add('PDF.LoadSuccess') 193 actions.add('PDF.PreviewDocumentLoadFailure') 194 actions.add('PDF.PrintButton') 195 actions.add('PDF.PrintPage') 196 actions.add('PDF.SaveButton') 197 actions.add('PDF.ZoomFromBrowser') 198 actions.add('PDF.ZoomInButton') 199 actions.add('PDF.ZoomOutButton') 200 actions.add('PDF_Unsupported_3D') 201 actions.add('PDF_Unsupported_Attachment') 202 actions.add('PDF_Unsupported_Bookmarks') 203 actions.add('PDF_Unsupported_Digital_Signature') 204 actions.add('PDF_Unsupported_Movie') 205 actions.add('PDF_Unsupported_Portfolios_Packages') 206 actions.add('PDF_Unsupported_Rights_Management') 207 actions.add('PDF_Unsupported_Screen') 208 actions.add('PDF_Unsupported_Shared_Form') 209 actions.add('PDF_Unsupported_Shared_Review') 210 actions.add('PDF_Unsupported_Sound') 211 actions.add('PDF_Unsupported_XFA') 212 213 def AddAndroidActions(actions): 214 """Add actions that are used by Chrome on Android. 215 216 Arguments 217 actions: set of actions to add to. 218 """ 219 actions.add('Cast_Sender_CastDeviceSelected'); 220 actions.add('Cast_Sender_CastEnterFullscreen'); 221 actions.add('Cast_Sender_CastMediaType'); 222 actions.add('Cast_Sender_CastPlayRequested'); 223 actions.add('Cast_Sender_YouTubeDeviceSelected'); 224 actions.add('DataReductionProxy_PromoDisplayed'); 225 actions.add('DataReductionProxy_PromoLearnMore'); 226 actions.add('DataReductionProxy_TurnedOn'); 227 actions.add('DataReductionProxy_TurnedOnFromPromo'); 228 actions.add('DataReductionProxy_TurnedOff'); 229 actions.add('MobileActionBarShown') 230 actions.add('MobileBeamCallbackSuccess') 231 actions.add('MobileBeamInvalidAppState') 232 actions.add('MobileBreakpadUploadAttempt') 233 actions.add('MobileBreakpadUploadFailure') 234 actions.add('MobileBreakpadUploadSuccess') 235 actions.add('MobileContextMenuCopyImageLinkAddress') 236 actions.add('MobileContextMenuCopyLinkAddress') 237 actions.add('MobileContextMenuCopyLinkText') 238 actions.add('MobileContextMenuDownloadImage') 239 actions.add('MobileContextMenuDownloadLink') 240 actions.add('MobileContextMenuDownloadVideo') 241 actions.add('MobileContextMenuImage') 242 actions.add('MobileContextMenuLink') 243 actions.add('MobileContextMenuOpenImageInNewTab') 244 actions.add('MobileContextMenuOpenLink') 245 actions.add('MobileContextMenuOpenLinkInIncognito') 246 actions.add('MobileContextMenuOpenLinkInNewTab') 247 actions.add('MobileContextMenuSaveImage') 248 actions.add('MobileContextMenuSearchByImage') 249 actions.add('MobileContextMenuShareLink') 250 actions.add('MobileContextMenuText') 251 actions.add('MobileContextMenuVideo') 252 actions.add('MobileContextMenuViewImage') 253 actions.add('MobileFirstEditInOmnibox') 254 actions.add('MobileFocusedFakeboxOnNtp') 255 actions.add('MobileFocusedOmniboxNotOnNtp') 256 actions.add('MobileFocusedOmniboxOnNtp') 257 actions.add('MobileFreAttemptSignIn') 258 actions.add('MobileFreSignInSuccessful') 259 actions.add('MobileFreSkipSignIn') 260 actions.add('MobileMenuAddToBookmarks') 261 actions.add('MobileMenuAddToHomescreen') 262 actions.add('MobileMenuAllBookmarks') 263 actions.add('MobileMenuBack') 264 actions.add('MobileMenuCloseAllTabs') 265 actions.add('MobileMenuCloseTab') 266 actions.add('MobileMenuDirectShare') 267 actions.add('MobileMenuFeedback') 268 actions.add('MobileMenuFindInPage') 269 actions.add('MobileMenuForward') 270 actions.add('MobileMenuFullscreen') 271 actions.add('MobileMenuHistory') 272 actions.add('MobileMenuNewIncognitoTab') 273 actions.add('MobileMenuNewTab') 274 actions.add('MobileMenuOpenTabs') 275 actions.add('MobileMenuPrint') 276 actions.add('MobileMenuQuit') 277 actions.add('MobileMenuReload') 278 actions.add('MobileMenuRequestDesktopSite') 279 actions.add('MobileMenuSettings') 280 actions.add('MobileMenuShare') 281 actions.add('MobileMenuShow') 282 actions.add('MobileNTPBookmark') 283 actions.add('MobileNTPForeignSession') 284 actions.add('MobileNTPMostVisited') 285 actions.add('MobileNTPRecentlyClosed') 286 actions.add('MobileNTPSwitchToBookmarks') 287 actions.add('MobileNTPSwitchToIncognito') 288 actions.add('MobileNTPSwitchToMostVisited') 289 actions.add('MobileNTPSwitchToOpenTabs') 290 actions.add('MobileNewTabOpened') 291 actions.add('MobileOmniboxSearch') 292 actions.add('MobileOmniboxVoiceSearch') 293 actions.add('MobileOmniboxRefineSuggestion') 294 actions.add('MobilePageLoaded') 295 actions.add('MobilePageLoadedDesktopUserAgent') 296 actions.add('MobilePageLoadedWithKeyboard') 297 actions.add('MobileReceivedExternalIntent') 298 actions.add('MobileRendererCrashed') 299 actions.add('MobileShortcutAllBookmarks') 300 actions.add('MobileShortcutFindInPage') 301 actions.add('MobileShortcutNewIncognitoTab') 302 actions.add('MobileShortcutNewTab') 303 actions.add('MobileSideSwipeFinished') 304 actions.add('MobileStackViewCloseTab') 305 actions.add('MobileStackViewSwipeCloseTab') 306 actions.add('MobileTabClobbered') 307 actions.add('MobileTabClosed') 308 actions.add('MobileTabStripCloseTab') 309 actions.add('MobileTabStripNewTab') 310 actions.add('MobileTabSwitched') 311 actions.add('MobileToolbarBack') 312 actions.add('MobileToolbarForward') 313 actions.add('MobileToolbarNewTab') 314 actions.add('MobileToolbarReload') 315 actions.add('MobileToolbarShowMenu') 316 actions.add('MobileToolbarShowStackView') 317 actions.add('MobileToolbarStackViewNewTab') 318 actions.add('MobileToolbarToggleBookmark') 319 actions.add('MobileUsingMenuByHwButtonDragging') 320 actions.add('MobileUsingMenuByHwButtonTap') 321 actions.add('MobileUsingMenuBySwButtonDragging') 322 actions.add('MobileUsingMenuBySwButtonTap') 323 actions.add('SystemBack') 324 actions.add('SystemBackForNavigation') 325 326 def AddAboutFlagsActions(actions): 327 """This parses the experimental feature flags for UMA actions. 328 329 Arguments: 330 actions: set of actions to add to. 331 """ 332 about_flags = os.path.join(REPOSITORY_ROOT, 'chrome', 'browser', 333 'about_flags.cc') 334 flag_name_re = re.compile(r'\s*"([0-9a-zA-Z\-_]+)",\s*// FLAGS:RECORD_UMA') 335 for line in open(about_flags): 336 match = flag_name_re.search(line) 337 if match: 338 actions.add("AboutFlags_" + match.group(1)) 339 # If the line contains the marker but was not matched by the regex, put up 340 # an error if the line is not a comment. 341 elif 'FLAGS:RECORD_UMA' in line and line[0:2] != '//': 342 print >>sys.stderr, 'WARNING: This line is marked for recording ' + \ 343 'about:flags metrics, but is not in the proper format:\n' + line 344 345 def AddBookmarkManagerActions(actions): 346 """Add actions that are used by BookmarkManager. 347 348 Arguments 349 actions: set of actions to add to. 350 """ 351 actions.add('BookmarkManager_Command_AddPage') 352 actions.add('BookmarkManager_Command_Copy') 353 actions.add('BookmarkManager_Command_Cut') 354 actions.add('BookmarkManager_Command_Delete') 355 actions.add('BookmarkManager_Command_Edit') 356 actions.add('BookmarkManager_Command_Export') 357 actions.add('BookmarkManager_Command_Import') 358 actions.add('BookmarkManager_Command_NewFolder') 359 actions.add('BookmarkManager_Command_OpenIncognito') 360 actions.add('BookmarkManager_Command_OpenInNewTab') 361 actions.add('BookmarkManager_Command_OpenInNewWindow') 362 actions.add('BookmarkManager_Command_OpenInSame') 363 actions.add('BookmarkManager_Command_Paste') 364 actions.add('BookmarkManager_Command_ShowInFolder') 365 actions.add('BookmarkManager_Command_Sort') 366 actions.add('BookmarkManager_Command_UndoDelete') 367 actions.add('BookmarkManager_Command_UndoGlobal') 368 actions.add('BookmarkManager_Command_UndoNone') 369 370 actions.add('BookmarkManager_NavigateTo_BookmarkBar') 371 actions.add('BookmarkManager_NavigateTo_Mobile') 372 actions.add('BookmarkManager_NavigateTo_Other') 373 actions.add('BookmarkManager_NavigateTo_Recent') 374 actions.add('BookmarkManager_NavigateTo_Search') 375 actions.add('BookmarkManager_NavigateTo_SubFolder') 376 377 def AddChromeOSActions(actions): 378 """Add actions reported by non-Chrome processes in Chrome OS. 379 380 Arguments: 381 actions: set of actions to add to. 382 """ 383 # Actions sent by Chrome OS update engine. 384 actions.add('Updater.ServerCertificateChanged') 385 actions.add('Updater.ServerCertificateFailed') 386 387 # Actions sent by Chrome OS cryptohome. 388 actions.add('Cryptohome.PKCS11InitFail') 389 390 def AddExtensionActions(actions): 391 """Add actions reported by extensions via chrome.metricsPrivate API. 392 393 Arguments: 394 actions: set of actions to add to. 395 """ 396 # Actions sent by Chrome OS File Browser. 397 actions.add('FileBrowser.CreateNewFolder') 398 actions.add('FileBrowser.PhotoEditor.Edit') 399 actions.add('FileBrowser.PhotoEditor.View') 400 actions.add('FileBrowser.SuggestApps.ShowDialog') 401 402 # Actions sent by Google Now client. 403 actions.add('GoogleNow.MessageClicked') 404 actions.add('GoogleNow.ButtonClicked0') 405 actions.add('GoogleNow.ButtonClicked1') 406 actions.add('GoogleNow.Dismissed') 407 408 # Actions sent by Chrome Connectivity Diagnostics. 409 actions.add('ConnectivityDiagnostics.LaunchSource.OfflineChromeOS') 410 actions.add('ConnectivityDiagnostics.LaunchSource.WebStore') 411 actions.add('ConnectivityDiagnostics.UA.LogsShown') 412 actions.add('ConnectivityDiagnostics.UA.PassingTestsShown') 413 actions.add('ConnectivityDiagnostics.UA.SettingsShown') 414 actions.add('ConnectivityDiagnostics.UA.TestResultExpanded') 415 actions.add('ConnectivityDiagnostics.UA.TestSuiteRun') 416 417 def GrepForActions(path, actions): 418 """Grep a source file for calls to UserMetrics functions. 419 420 Arguments: 421 path: path to the file 422 actions: set of actions to add to 423 """ 424 global number_of_files_total 425 number_of_files_total = number_of_files_total + 1 426 # we look for the UserMetricsAction structure constructor 427 # this should be on one line 428 action_re = re.compile(r'[^a-zA-Z]UserMetricsAction\("([^"]*)') 429 malformed_action_re = re.compile(r'[^a-zA-Z]UserMetricsAction\([^"]') 430 computed_action_re = re.compile(r'RecordComputedAction') 431 line_number = 0 432 for line in open(path): 433 line_number = line_number + 1 434 match = action_re.search(line) 435 if match: # Plain call to RecordAction 436 actions.add(match.group(1)) 437 elif malformed_action_re.search(line): 438 # Warn if this line is using RecordAction incorrectly. 439 print >>sys.stderr, ('WARNING: %s has malformed call to RecordAction' 440 ' at %d' % (path, line_number)) 441 elif computed_action_re.search(line): 442 # Warn if this file shouldn't be calling RecordComputedAction. 443 if os.path.basename(path) not in KNOWN_COMPUTED_USERS: 444 print >>sys.stderr, ('WARNING: %s has RecordComputedAction at %d' % 445 (path, line_number)) 446 447 class WebUIActionsParser(HTMLParser): 448 """Parses an HTML file, looking for all tags with a 'metric' attribute. 449 Adds user actions corresponding to any metrics found. 450 451 Arguments: 452 actions: set of actions to add to 453 """ 454 def __init__(self, actions): 455 HTMLParser.__init__(self) 456 self.actions = actions 457 458 def handle_starttag(self, tag, attrs): 459 # We only care to examine tags that have a 'metric' attribute. 460 attrs = dict(attrs) 461 if not 'metric' in attrs: 462 return 463 464 # Boolean metrics have two corresponding actions. All other metrics have 465 # just one corresponding action. By default, we check the 'dataType' 466 # attribute. 467 is_boolean = ('dataType' in attrs and attrs['dataType'] == 'boolean') 468 if 'type' in attrs and attrs['type'] in ('checkbox', 'radio'): 469 if attrs['type'] == 'checkbox': 470 is_boolean = True 471 else: 472 # Radio buttons are boolean if and only if their values are 'true' or 473 # 'false'. 474 assert(attrs['type'] == 'radio') 475 if 'value' in attrs and attrs['value'] in ['true', 'false']: 476 is_boolean = True 477 478 if is_boolean: 479 self.actions.add(attrs['metric'] + '_Enable') 480 self.actions.add(attrs['metric'] + '_Disable') 481 else: 482 self.actions.add(attrs['metric']) 483 484 def GrepForWebUIActions(path, actions): 485 """Grep a WebUI source file for elements with associated metrics. 486 487 Arguments: 488 path: path to the file 489 actions: set of actions to add to 490 """ 491 close_called = False 492 try: 493 parser = WebUIActionsParser(actions) 494 parser.feed(open(path).read()) 495 # An exception can be thrown by parser.close(), so do it in the try to 496 # ensure the path of the file being parsed gets printed if that happens. 497 close_called = True 498 parser.close() 499 except Exception, e: 500 print "Error encountered for path %s" % path 501 raise e 502 finally: 503 if not close_called: 504 parser.close() 505 506 def WalkDirectory(root_path, actions, extensions, callback): 507 for path, dirs, files in os.walk(root_path): 508 if '.svn' in dirs: 509 dirs.remove('.svn') 510 if '.git' in dirs: 511 dirs.remove('.git') 512 for file in files: 513 ext = os.path.splitext(file)[1] 514 if ext in extensions: 515 callback(os.path.join(path, file), actions) 516 517 def AddLiteralActions(actions): 518 """Add literal actions specified via calls to UserMetrics functions. 519 520 Arguments: 521 actions: set of actions to add to. 522 """ 523 EXTENSIONS = ('.cc', '.mm', '.c', '.m') 524 525 # Walk the source tree to process all .cc files. 526 ash_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'ash')) 527 WalkDirectory(ash_root, actions, EXTENSIONS, GrepForActions) 528 chrome_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'chrome')) 529 WalkDirectory(chrome_root, actions, EXTENSIONS, GrepForActions) 530 content_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'content')) 531 WalkDirectory(content_root, actions, EXTENSIONS, GrepForActions) 532 components_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 533 'components')) 534 WalkDirectory(components_root, actions, EXTENSIONS, GrepForActions) 535 net_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'net')) 536 WalkDirectory(net_root, actions, EXTENSIONS, GrepForActions) 537 webkit_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'webkit')) 538 WalkDirectory(os.path.join(webkit_root, 'glue'), actions, EXTENSIONS, 539 GrepForActions) 540 WalkDirectory(os.path.join(webkit_root, 'port'), actions, EXTENSIONS, 541 GrepForActions) 542 543 def AddWebUIActions(actions): 544 """Add user actions defined in WebUI files. 545 546 Arguments: 547 actions: set of actions to add to. 548 """ 549 resources_root = os.path.join(REPOSITORY_ROOT, 'chrome', 'browser', 550 'resources') 551 WalkDirectory(resources_root, actions, ('.html'), GrepForWebUIActions) 552 553 def AddHistoryPageActions(actions): 554 """Add actions that are used in History page. 555 556 Arguments 557 actions: set of actions to add to. 558 """ 559 actions.add('HistoryPage_BookmarkStarClicked') 560 actions.add('HistoryPage_EntryMenuRemoveFromHistory') 561 actions.add('HistoryPage_EntryLinkClick') 562 actions.add('HistoryPage_EntryLinkRightClick') 563 actions.add('HistoryPage_SearchResultClick') 564 actions.add('HistoryPage_EntryMenuShowMoreFromSite') 565 actions.add('HistoryPage_NewestHistoryClick') 566 actions.add('HistoryPage_NewerHistoryClick') 567 actions.add('HistoryPage_OlderHistoryClick') 568 actions.add('HistoryPage_Search') 569 actions.add('HistoryPage_InitClearBrowsingData') 570 actions.add('HistoryPage_RemoveSelected') 571 actions.add('HistoryPage_SearchResultRemove') 572 actions.add('HistoryPage_ConfirmRemoveSelected') 573 actions.add('HistoryPage_CancelRemoveSelected') 574 575 def AddKeySystemSupportActions(actions): 576 """Add actions that are used for key system support metrics. 577 578 Arguments 579 actions: set of actions to add to. 580 """ 581 actions.add('KeySystemSupport.Widevine.Queried') 582 actions.add('KeySystemSupport.WidevineWithType.Queried') 583 actions.add('KeySystemSupport.Widevine.Supported') 584 actions.add('KeySystemSupport.WidevineWithType.Supported') 585 586 def AddAutomaticResetBannerActions(actions): 587 """Add actions that are used for the automatic profile settings reset banners 588 in chrome://settings. 589 590 Arguments 591 actions: set of actions to add to. 592 """ 593 # These actions relate to the the automatic settings reset banner shown as 594 # a result of the reset prompt. 595 actions.add('AutomaticReset_WebUIBanner_BannerShown') 596 actions.add('AutomaticReset_WebUIBanner_ManuallyClosed') 597 actions.add('AutomaticReset_WebUIBanner_ResetClicked') 598 599 # These actions relate to the the automatic settings reset banner shown as 600 # a result of settings hardening. 601 actions.add('AutomaticSettingsReset_WebUIBanner_BannerShown') 602 actions.add('AutomaticSettingsReset_WebUIBanner_ManuallyClosed') 603 actions.add('AutomaticSettingsReset_WebUIBanner_LearnMoreClicked') 604 605 606 class Error(Exception): 607 pass 608 609 610 def _ExtractText(parent_dom, tag_name): 611 """Extract the text enclosed by |tag_name| under |parent_dom| 612 613 Args: 614 parent_dom: The parent Element under which text node is searched for. 615 tag_name: The name of the tag which contains a text node. 616 617 Returns: 618 A (list of) string enclosed by |tag_name| under |parent_dom|. 619 """ 620 texts = [] 621 for child_dom in parent_dom.getElementsByTagName(tag_name): 622 text_dom = child_dom.childNodes 623 if text_dom.length != 1: 624 raise Error('More than 1 child node exists under %s' % tag_name) 625 if text_dom[0].nodeType != minidom.Node.TEXT_NODE: 626 raise Error('%s\'s child node is not a text node.' % tag_name) 627 texts.append(text_dom[0].data) 628 return texts 629 630 631 class Action(object): 632 def __init__(self, name, description, owners, obsolete=None): 633 self.name = name 634 self.description = description 635 self.owners = owners 636 self.obsolete = obsolete 637 638 639 def ParseActionFile(file_content): 640 """Parse the XML data currently stored in the file. 641 642 Args: 643 file_content: a string containing the action XML file content. 644 645 Returns: 646 (actions, actions_dict) actions is a set with all user actions' names. 647 actions_dict is a dict from user action name to Action object. 648 """ 649 dom = minidom.parseString(file_content) 650 651 comment_nodes = [] 652 # Get top-level comments. It is assumed that all comments are placed before 653 # <acionts> tag. Therefore the loop will stop if it encounters a non-comment 654 # node. 655 for node in dom.childNodes: 656 if node.nodeType == minidom.Node.COMMENT_NODE: 657 comment_nodes.append(node) 658 else: 659 break 660 661 actions = set() 662 actions_dict = {} 663 # Get each user action data. 664 for action_dom in dom.getElementsByTagName('action'): 665 action_name = action_dom.getAttribute('name') 666 actions.add(action_name) 667 668 owners = _ExtractText(action_dom, 'owner') 669 # There is only one description for each user action. Get the first element 670 # of the returned list. 671 description_list = _ExtractText(action_dom, 'description') 672 if len(description_list) > 1: 673 logging.error('user actions "%s" has more than one descriptions. Exactly ' 674 'one description is needed for each user action. Please ' 675 'fix.', action_name) 676 sys.exit(1) 677 description = description_list[0] if description_list else None 678 # There is at most one obsolete tag for each user action. 679 obsolete_list = _ExtractText(action_dom, 'obsolete') 680 if len(obsolete_list) > 1: 681 logging.error('user actions "%s" has more than one obsolete tag. At most ' 682 'one obsolete tag can be added for each user action. Please' 683 ' fix.', action_name) 684 sys.exit(1) 685 obsolete = obsolete_list[0] if obsolete_list else None 686 actions_dict[action_name] = Action(action_name, description, owners, 687 obsolete) 688 return actions, actions_dict, comment_nodes 689 690 691 def _CreateActionTag(doc, action_name, action_object): 692 """Create a new action tag. 693 694 Format of an action tag: 695 <action name="name"> 696 <owner>Owner</owner> 697 <description>Description.</description> 698 <obsolete>Deprecated.</obsolete> 699 </action> 700 701 <obsolete> is an optional tag. It's added to user actions that are no longer 702 used any more. 703 704 If action_name is in actions_dict, the values to be inserted are based on the 705 corresponding Action object. If action_name is not in actions_dict, the 706 default value from TAGS is used. 707 708 Args: 709 doc: The document under which the new action tag is created. 710 action_name: The name of an action. 711 action_object: An action object representing the data to be inserted. 712 713 Returns: 714 An action tag Element with proper children elements. 715 """ 716 action_dom = doc.createElement('action') 717 action_dom.setAttribute('name', action_name) 718 719 # Create owner tag. 720 if action_object and action_object.owners: 721 # If owners for this action is not None, use the stored value. Otherwise, 722 # use the default value. 723 for owner in action_object.owners: 724 owner_dom = doc.createElement('owner') 725 owner_dom.appendChild(doc.createTextNode(owner)) 726 action_dom.appendChild(owner_dom) 727 else: 728 # Use default value. 729 owner_dom = doc.createElement('owner') 730 owner_dom.appendChild(doc.createTextNode(TAGS.get('owner', ''))) 731 action_dom.appendChild(owner_dom) 732 733 # Create description tag. 734 description_dom = doc.createElement('description') 735 action_dom.appendChild(description_dom) 736 if action_object and action_object.description: 737 # If description for this action is not None, use the store value. 738 # Otherwise, use the default value. 739 description_dom.appendChild(doc.createTextNode( 740 action_object.description)) 741 else: 742 description_dom.appendChild(doc.createTextNode( 743 TAGS.get('description', ''))) 744 745 # Create obsolete tag. 746 if action_object and action_object.obsolete: 747 obsolete_dom = doc.createElement('obsolete') 748 action_dom.appendChild(obsolete_dom) 749 obsolete_dom.appendChild(doc.createTextNode( 750 action_object.obsolete)) 751 752 return action_dom 753 754 755 def PrettyPrint(actions, actions_dict, comment_nodes=[]): 756 """Given a list of action data, create a well-printed minidom document. 757 758 Args: 759 actions: A list of action names. 760 actions_dict: A mappting from action name to Action object. 761 762 Returns: 763 A well-printed minidom document that represents the input action data. 764 """ 765 doc = minidom.Document() 766 767 # Attach top-level comments. 768 for node in comment_nodes: 769 doc.appendChild(node) 770 771 actions_element = doc.createElement('actions') 772 doc.appendChild(actions_element) 773 774 # Attach action node based on updated |actions|. 775 for action in sorted(actions): 776 actions_element.appendChild( 777 _CreateActionTag(doc, action, actions_dict.get(action, None))) 778 779 return print_style.GetPrintStyle().PrettyPrintNode(doc) 780 781 782 def main(argv): 783 presubmit = ('--presubmit' in argv) 784 actions_xml_path = os.path.join(path_utils.ScriptDir(), 'actions.xml') 785 786 # Save the original file content. 787 with open(actions_xml_path, 'rb') as f: 788 original_xml = f.read() 789 790 actions, actions_dict, comment_nodes = ParseActionFile(original_xml) 791 792 AddComputedActions(actions) 793 # TODO(fmantek): bring back webkit editor actions. 794 # AddWebKitEditorActions(actions) 795 AddAboutFlagsActions(actions) 796 AddWebUIActions(actions) 797 798 AddLiteralActions(actions) 799 800 # print "Scanned {0} number of files".format(number_of_files_total) 801 # print "Found {0} entries".format(len(actions)) 802 803 AddAndroidActions(actions) 804 AddAutomaticResetBannerActions(actions) 805 AddBookmarkManagerActions(actions) 806 AddChromeOSActions(actions) 807 AddClosedSourceActions(actions) 808 AddExtensionActions(actions) 809 AddHistoryPageActions(actions) 810 AddKeySystemSupportActions(actions) 811 812 pretty = PrettyPrint(actions, actions_dict, comment_nodes) 813 if original_xml == pretty: 814 print 'actions.xml is correctly pretty-printed.' 815 sys.exit(0) 816 if presubmit: 817 logging.info('actions.xml is not formatted correctly; run ' 818 'extract_actions.py to fix.') 819 sys.exit(1) 820 821 # Prompt user to consent on the change. 822 if not diff_util.PromptUserToAcceptDiff( 823 original_xml, pretty, 'Is the new version acceptable?'): 824 logging.error('Aborting') 825 sys.exit(1) 826 827 print 'Creating backup file: actions.old.xml.' 828 shutil.move(actions_xml_path, 'actions.old.xml') 829 830 with open(actions_xml_path, 'wb') as f: 831 f.write(pretty) 832 print ('Updated %s. Don\'t forget to add it to your changelist' % 833 actions_xml_path) 834 return 0 835 836 837 if '__main__' == __name__: 838 sys.exit(main(sys.argv)) 839