• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  '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.
83LANGUAGE_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.
108INPUT_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.
126REPOSITORY_ROOT = os.path.join(path_utils.ScriptDir(), '..', '..', '..')
127
128number_of_files_total = 0
129
130# Tags that need to be inserted to each 'action' tag and their default content.
131TAGS = {'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
136def 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
168def 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
183def 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
213def 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
326def 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
345def 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
377def 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
390def 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
417def 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
447class 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
484def 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
506def 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
517def 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
543def 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
553def 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
575def 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
586def 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
606class Error(Exception):
607  pass
608
609
610def _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
631class 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
639def 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
691def _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
755def 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
782def 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
837if '__main__' == __name__:
838  sys.exit(main(sys.argv))
839