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