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