Home | History | Annotate | Download | only in actions
      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