964de36d1fa3a01e0b4ae5a4a85d97657b87e122
[platform/framework/web/crosswalk.git] / src / tools / metrics / actions / extract_actions.py
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('MobileMenuNewIncognitoTab')
272   actions.add('MobileMenuNewTab')
273   actions.add('MobileMenuOpenTabs')
274   actions.add('MobileMenuPrint')
275   actions.add('MobileMenuQuit')
276   actions.add('MobileMenuReload')
277   actions.add('MobileMenuRequestDesktopSite')
278   actions.add('MobileMenuSettings')
279   actions.add('MobileMenuShare')
280   actions.add('MobileMenuShow')
281   actions.add('MobileNTPBookmark')
282   actions.add('MobileNTPForeignSession')
283   actions.add('MobileNTPMostVisited')
284   actions.add('MobileNTPRecentlyClosed')
285   actions.add('MobileNTPSwitchToBookmarks')
286   actions.add('MobileNTPSwitchToIncognito')
287   actions.add('MobileNTPSwitchToMostVisited')
288   actions.add('MobileNTPSwitchToOpenTabs')
289   actions.add('MobileNewTabOpened')
290   actions.add('MobileOmniboxSearch')
291   actions.add('MobileOmniboxVoiceSearch')
292   actions.add('MobileOmniboxRefineSuggestion')
293   actions.add('MobilePageLoaded')
294   actions.add('MobilePageLoadedDesktopUserAgent')
295   actions.add('MobilePageLoadedWithKeyboard')
296   actions.add('MobileReceivedExternalIntent')
297   actions.add('MobileRendererCrashed')
298   actions.add('MobileShortcutAllBookmarks')
299   actions.add('MobileShortcutFindInPage')
300   actions.add('MobileShortcutNewIncognitoTab')
301   actions.add('MobileShortcutNewTab')
302   actions.add('MobileSideSwipeFinished')
303   actions.add('MobileStackViewCloseTab')
304   actions.add('MobileStackViewSwipeCloseTab')
305   actions.add('MobileTabClobbered')
306   actions.add('MobileTabClosed')
307   actions.add('MobileTabStripCloseTab')
308   actions.add('MobileTabStripNewTab')
309   actions.add('MobileTabSwitched')
310   actions.add('MobileToolbarBack')
311   actions.add('MobileToolbarForward')
312   actions.add('MobileToolbarNewTab')
313   actions.add('MobileToolbarReload')
314   actions.add('MobileToolbarShowMenu')
315   actions.add('MobileToolbarShowStackView')
316   actions.add('MobileToolbarStackViewNewTab')
317   actions.add('MobileToolbarToggleBookmark')
318   actions.add('MobileUsingMenuByHwButtonDragging')
319   actions.add('MobileUsingMenuByHwButtonTap')
320   actions.add('MobileUsingMenuBySwButtonDragging')
321   actions.add('MobileUsingMenuBySwButtonTap')
322   actions.add('SystemBack')
323   actions.add('SystemBackForNavigation')
324
325 def AddAboutFlagsActions(actions):
326   """This parses the experimental feature flags for UMA actions.
327
328   Arguments:
329     actions: set of actions to add to.
330   """
331   about_flags = os.path.join(REPOSITORY_ROOT, 'chrome', 'browser',
332                              'about_flags.cc')
333   flag_name_re = re.compile(r'\s*"([0-9a-zA-Z\-_]+)",\s*// FLAGS:RECORD_UMA')
334   for line in open(about_flags):
335     match = flag_name_re.search(line)
336     if match:
337       actions.add("AboutFlags_" + match.group(1))
338     # If the line contains the marker but was not matched by the regex, put up
339     # an error if the line is not a comment.
340     elif 'FLAGS:RECORD_UMA' in line and line[0:2] != '//':
341       print >>sys.stderr, 'WARNING: This line is marked for recording ' + \
342           'about:flags metrics, but is not in the proper format:\n' + line
343
344 def AddBookmarkManagerActions(actions):
345   """Add actions that are used by BookmarkManager.
346
347   Arguments
348     actions: set of actions to add to.
349   """
350   actions.add('BookmarkManager_Command_AddPage')
351   actions.add('BookmarkManager_Command_Copy')
352   actions.add('BookmarkManager_Command_Cut')
353   actions.add('BookmarkManager_Command_Delete')
354   actions.add('BookmarkManager_Command_Edit')
355   actions.add('BookmarkManager_Command_Export')
356   actions.add('BookmarkManager_Command_Import')
357   actions.add('BookmarkManager_Command_NewFolder')
358   actions.add('BookmarkManager_Command_OpenIncognito')
359   actions.add('BookmarkManager_Command_OpenInNewTab')
360   actions.add('BookmarkManager_Command_OpenInNewWindow')
361   actions.add('BookmarkManager_Command_OpenInSame')
362   actions.add('BookmarkManager_Command_Paste')
363   actions.add('BookmarkManager_Command_ShowInFolder')
364   actions.add('BookmarkManager_Command_Sort')
365   actions.add('BookmarkManager_Command_UndoDelete')
366   actions.add('BookmarkManager_Command_UndoGlobal')
367   actions.add('BookmarkManager_Command_UndoNone')
368
369   actions.add('BookmarkManager_NavigateTo_BookmarkBar')
370   actions.add('BookmarkManager_NavigateTo_Mobile')
371   actions.add('BookmarkManager_NavigateTo_Other')
372   actions.add('BookmarkManager_NavigateTo_Recent')
373   actions.add('BookmarkManager_NavigateTo_Search')
374   actions.add('BookmarkManager_NavigateTo_SubFolder')
375
376 def AddChromeOSActions(actions):
377   """Add actions reported by non-Chrome processes in Chrome OS.
378
379   Arguments:
380     actions: set of actions to add to.
381   """
382   # Actions sent by Chrome OS update engine.
383   actions.add('Updater.ServerCertificateChanged')
384   actions.add('Updater.ServerCertificateFailed')
385
386   # Actions sent by Chrome OS cryptohome.
387   actions.add('Cryptohome.PKCS11InitFail')
388
389 def AddExtensionActions(actions):
390   """Add actions reported by extensions via chrome.metricsPrivate API.
391
392   Arguments:
393     actions: set of actions to add to.
394   """
395   # Actions sent by Chrome OS File Browser.
396   actions.add('FileBrowser.CreateNewFolder')
397   actions.add('FileBrowser.PhotoEditor.Edit')
398   actions.add('FileBrowser.PhotoEditor.View')
399   actions.add('FileBrowser.SuggestApps.ShowDialog')
400
401   # Actions sent by Google Now client.
402   actions.add('GoogleNow.MessageClicked')
403   actions.add('GoogleNow.ButtonClicked0')
404   actions.add('GoogleNow.ButtonClicked1')
405   actions.add('GoogleNow.Dismissed')
406
407   # Actions sent by Chrome Connectivity Diagnostics.
408   actions.add('ConnectivityDiagnostics.LaunchSource.OfflineChromeOS')
409   actions.add('ConnectivityDiagnostics.LaunchSource.WebStore')
410   actions.add('ConnectivityDiagnostics.UA.LogsShown')
411   actions.add('ConnectivityDiagnostics.UA.PassingTestsShown')
412   actions.add('ConnectivityDiagnostics.UA.SettingsShown')
413   actions.add('ConnectivityDiagnostics.UA.TestResultExpanded')
414   actions.add('ConnectivityDiagnostics.UA.TestSuiteRun')
415
416 def GrepForActions(path, actions):
417   """Grep a source file for calls to UserMetrics functions.
418
419   Arguments:
420     path: path to the file
421     actions: set of actions to add to
422   """
423   global number_of_files_total
424   number_of_files_total = number_of_files_total + 1
425   # we look for the UserMetricsAction structure constructor
426   # this should be on one line
427   action_re = re.compile(r'[^a-zA-Z]UserMetricsAction\("([^"]*)')
428   malformed_action_re = re.compile(r'[^a-zA-Z]UserMetricsAction\([^"]')
429   computed_action_re = re.compile(r'RecordComputedAction')
430   line_number = 0
431   for line in open(path):
432     line_number = line_number + 1
433     match = action_re.search(line)
434     if match:  # Plain call to RecordAction
435       actions.add(match.group(1))
436     elif malformed_action_re.search(line):
437       # Warn if this line is using RecordAction incorrectly.
438       print >>sys.stderr, ('WARNING: %s has malformed call to RecordAction'
439                            ' at %d' % (path, line_number))
440     elif computed_action_re.search(line):
441       # Warn if this file shouldn't be calling RecordComputedAction.
442       if os.path.basename(path) not in KNOWN_COMPUTED_USERS:
443         print >>sys.stderr, ('WARNING: %s has RecordComputedAction at %d' %
444                              (path, line_number))
445
446 class WebUIActionsParser(HTMLParser):
447   """Parses an HTML file, looking for all tags with a 'metric' attribute.
448   Adds user actions corresponding to any metrics found.
449
450   Arguments:
451     actions: set of actions to add to
452   """
453   def __init__(self, actions):
454     HTMLParser.__init__(self)
455     self.actions = actions
456
457   def handle_starttag(self, tag, attrs):
458     # We only care to examine tags that have a 'metric' attribute.
459     attrs = dict(attrs)
460     if not 'metric' in attrs:
461       return
462
463     # Boolean metrics have two corresponding actions.  All other metrics have
464     # just one corresponding action.  By default, we check the 'dataType'
465     # attribute.
466     is_boolean = ('dataType' in attrs and attrs['dataType'] == 'boolean')
467     if 'type' in attrs and attrs['type'] in ('checkbox', 'radio'):
468       if attrs['type'] == 'checkbox':
469         is_boolean = True
470       else:
471         # Radio buttons are boolean if and only if their values are 'true' or
472         # 'false'.
473         assert(attrs['type'] == 'radio')
474         if 'value' in attrs and attrs['value'] in ['true', 'false']:
475           is_boolean = True
476
477     if is_boolean:
478       self.actions.add(attrs['metric'] + '_Enable')
479       self.actions.add(attrs['metric'] + '_Disable')
480     else:
481       self.actions.add(attrs['metric'])
482
483 def GrepForWebUIActions(path, actions):
484   """Grep a WebUI source file for elements with associated metrics.
485
486   Arguments:
487     path: path to the file
488     actions: set of actions to add to
489   """
490   close_called = False
491   try:
492     parser = WebUIActionsParser(actions)
493     parser.feed(open(path).read())
494     # An exception can be thrown by parser.close(), so do it in the try to
495     # ensure the path of the file being parsed gets printed if that happens.
496     close_called = True
497     parser.close()
498   except Exception, e:
499     print "Error encountered for path %s" % path
500     raise e
501   finally:
502     if not close_called:
503       parser.close()
504
505 def WalkDirectory(root_path, actions, extensions, callback):
506   for path, dirs, files in os.walk(root_path):
507     if '.svn' in dirs:
508       dirs.remove('.svn')
509     if '.git' in dirs:
510       dirs.remove('.git')
511     for file in files:
512       ext = os.path.splitext(file)[1]
513       if ext in extensions:
514         callback(os.path.join(path, file), actions)
515
516 def AddLiteralActions(actions):
517   """Add literal actions specified via calls to UserMetrics functions.
518
519   Arguments:
520     actions: set of actions to add to.
521   """
522   EXTENSIONS = ('.cc', '.mm', '.c', '.m')
523
524   # Walk the source tree to process all .cc files.
525   ash_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'ash'))
526   WalkDirectory(ash_root, actions, EXTENSIONS, GrepForActions)
527   chrome_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'chrome'))
528   WalkDirectory(chrome_root, actions, EXTENSIONS, GrepForActions)
529   content_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'content'))
530   WalkDirectory(content_root, actions, EXTENSIONS, GrepForActions)
531   net_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'net'))
532   WalkDirectory(net_root, actions, EXTENSIONS, GrepForActions)
533   webkit_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'webkit'))
534   WalkDirectory(os.path.join(webkit_root, 'glue'), actions, EXTENSIONS,
535                 GrepForActions)
536   WalkDirectory(os.path.join(webkit_root, 'port'), actions, EXTENSIONS,
537                 GrepForActions)
538
539 def AddWebUIActions(actions):
540   """Add user actions defined in WebUI files.
541
542   Arguments:
543     actions: set of actions to add to.
544   """
545   resources_root = os.path.join(REPOSITORY_ROOT, 'chrome', 'browser',
546                                 'resources')
547   WalkDirectory(resources_root, actions, ('.html'), GrepForWebUIActions)
548
549 def AddHistoryPageActions(actions):
550   """Add actions that are used in History page.
551
552   Arguments
553     actions: set of actions to add to.
554   """
555   actions.add('HistoryPage_BookmarkStarClicked')
556   actions.add('HistoryPage_EntryMenuRemoveFromHistory')
557   actions.add('HistoryPage_EntryLinkClick')
558   actions.add('HistoryPage_EntryLinkRightClick')
559   actions.add('HistoryPage_SearchResultClick')
560   actions.add('HistoryPage_EntryMenuShowMoreFromSite')
561   actions.add('HistoryPage_NewestHistoryClick')
562   actions.add('HistoryPage_NewerHistoryClick')
563   actions.add('HistoryPage_OlderHistoryClick')
564   actions.add('HistoryPage_Search')
565   actions.add('HistoryPage_InitClearBrowsingData')
566   actions.add('HistoryPage_RemoveSelected')
567   actions.add('HistoryPage_SearchResultRemove')
568   actions.add('HistoryPage_ConfirmRemoveSelected')
569   actions.add('HistoryPage_CancelRemoveSelected')
570
571 def AddKeySystemSupportActions(actions):
572   """Add actions that are used for key system support metrics.
573
574   Arguments
575     actions: set of actions to add to.
576   """
577   actions.add('KeySystemSupport.Widevine.Queried')
578   actions.add('KeySystemSupport.WidevineWithType.Queried')
579   actions.add('KeySystemSupport.Widevine.Supported')
580   actions.add('KeySystemSupport.WidevineWithType.Supported')
581
582 def AddAutomaticResetBannerActions(actions):
583   """Add actions that are used for the automatic profile settings reset banners
584   in chrome://settings.
585
586   Arguments
587     actions: set of actions to add to.
588   """
589   # These actions relate to the the automatic settings reset banner shown as
590   # a result of the reset prompt.
591   actions.add('AutomaticReset_WebUIBanner_BannerShown')
592   actions.add('AutomaticReset_WebUIBanner_ManuallyClosed')
593   actions.add('AutomaticReset_WebUIBanner_ResetClicked')
594
595   # These actions relate to the the automatic settings reset banner shown as
596   # a result of settings hardening.
597   actions.add('AutomaticSettingsReset_WebUIBanner_BannerShown')
598   actions.add('AutomaticSettingsReset_WebUIBanner_ManuallyClosed')
599   actions.add('AutomaticSettingsReset_WebUIBanner_LearnMoreClicked')
600
601
602 class Error(Exception):
603   pass
604
605
606 def _ExtractText(parent_dom, tag_name):
607   """Extract the text enclosed by |tag_name| under |parent_dom|
608
609   Args:
610     parent_dom: The parent Element under which text node is searched for.
611     tag_name: The name of the tag which contains a text node.
612
613   Returns:
614     A (list of) string enclosed by |tag_name| under |parent_dom|.
615   """
616   texts = []
617   for child_dom in parent_dom.getElementsByTagName(tag_name):
618     text_dom = child_dom.childNodes
619     if text_dom.length != 1:
620       raise Error('More than 1 child node exists under %s' % tag_name)
621     if text_dom[0].nodeType != minidom.Node.TEXT_NODE:
622       raise Error('%s\'s child node is not a text node.' % tag_name)
623     texts.append(text_dom[0].data)
624   return texts
625
626
627 class Action(object):
628   def __init__(self, name, description, owners, obsolete=None):
629     self.name = name
630     self.description = description
631     self.owners = owners
632     self.obsolete = obsolete
633
634
635 def ParseActionFile(file_content):
636   """Parse the XML data currently stored in the file.
637
638   Args:
639     file_content: a string containing the action XML file content.
640
641   Returns:
642     (actions, actions_dict) actions is a set with all user actions' names.
643     actions_dict is a dict from user action name to Action object.
644   """
645   dom = minidom.parseString(file_content)
646
647   comment_nodes = []
648   # Get top-level comments. It is assumed that all comments are placed before
649   # <acionts> tag. Therefore the loop will stop if it encounters a non-comment
650   # node.
651   for node in dom.childNodes:
652     if node.nodeType == minidom.Node.COMMENT_NODE:
653       comment_nodes.append(node)
654     else:
655       break
656
657   actions = set()
658   actions_dict = {}
659   # Get each user action data.
660   for action_dom in dom.getElementsByTagName('action'):
661     action_name = action_dom.getAttribute('name')
662     actions.add(action_name)
663
664     owners = _ExtractText(action_dom, 'owner')
665     # There is only one description for each user action. Get the first element
666     # of the returned list.
667     description_list = _ExtractText(action_dom, 'description')
668     if len(description_list) > 1:
669       logging.error('user actions "%s" has more than one descriptions. Exactly '
670                     'one description is needed for each user action. Please '
671                     'fix.', action_name)
672       sys.exit(1)
673     description = description_list[0] if description_list else None
674     # There is at most one obsolete tag for each user action.
675     obsolete_list = _ExtractText(action_dom, 'obsolete')
676     if len(obsolete_list) > 1:
677       logging.error('user actions "%s" has more than one obsolete tag. At most '
678                     'one obsolete tag can be added for each user action. Please'
679                     ' fix.', action_name)
680       sys.exit(1)
681     obsolete = obsolete_list[0] if obsolete_list else None
682     actions_dict[action_name] = Action(action_name, description, owners,
683                                        obsolete)
684   return actions, actions_dict, comment_nodes
685
686
687 def _CreateActionTag(doc, action_name, action_object):
688   """Create a new action tag.
689
690   Format of an action tag:
691   <action name="name">
692     <owner>Owner</owner>
693     <description>Description.</description>
694     <obsolete>Deprecated.</obsolete>
695   </action>
696
697   <obsolete> is an optional tag. It's added to user actions that are no longer
698   used any more.
699
700   If action_name is in actions_dict, the values to be inserted are based on the
701   corresponding Action object. If action_name is not in actions_dict, the
702   default value from TAGS is used.
703
704   Args:
705     doc: The document under which the new action tag is created.
706     action_name: The name of an action.
707     action_object: An action object representing the data to be inserted.
708
709   Returns:
710     An action tag Element with proper children elements.
711   """
712   action_dom = doc.createElement('action')
713   action_dom.setAttribute('name', action_name)
714
715   # Create owner tag.
716   if action_object and action_object.owners:
717     # If owners for this action is not None, use the stored value. Otherwise,
718     # use the default value.
719     for owner in action_object.owners:
720       owner_dom = doc.createElement('owner')
721       owner_dom.appendChild(doc.createTextNode(owner))
722       action_dom.appendChild(owner_dom)
723   else:
724     # Use default value.
725     owner_dom = doc.createElement('owner')
726     owner_dom.appendChild(doc.createTextNode(TAGS.get('owner', '')))
727     action_dom.appendChild(owner_dom)
728
729   # Create description tag.
730   description_dom = doc.createElement('description')
731   action_dom.appendChild(description_dom)
732   if action_object and action_object.description:
733     # If description for this action is not None, use the store value.
734     # Otherwise, use the default value.
735     description_dom.appendChild(doc.createTextNode(
736         action_object.description))
737   else:
738     description_dom.appendChild(doc.createTextNode(
739         TAGS.get('description', '')))
740
741   # Create obsolete tag.
742   if action_object and action_object.obsolete:
743     obsolete_dom = doc.createElement('obsolete')
744     action_dom.appendChild(obsolete_dom)
745     obsolete_dom.appendChild(doc.createTextNode(
746         action_object.obsolete))
747
748   return action_dom
749
750
751 def PrettyPrint(actions, actions_dict, comment_nodes=[]):
752   """Given a list of action data, create a well-printed minidom document.
753
754   Args:
755     actions: A list of action names.
756     actions_dict: A mappting from action name to Action object.
757
758   Returns:
759     A well-printed minidom document that represents the input action data.
760   """
761   doc = minidom.Document()
762
763   # Attach top-level comments.
764   for node in comment_nodes:
765     doc.appendChild(node)
766
767   actions_element = doc.createElement('actions')
768   doc.appendChild(actions_element)
769
770   # Attach action node based on updated |actions|.
771   for action in sorted(actions):
772     actions_element.appendChild(
773         _CreateActionTag(doc, action, actions_dict.get(action, None)))
774
775   return print_style.GetPrintStyle().PrettyPrintNode(doc)
776
777
778 def main(argv):
779   presubmit = ('--presubmit' in argv)
780   actions_xml_path = os.path.join(path_utils.ScriptDir(), 'actions.xml')
781
782   # Save the original file content.
783   with open(actions_xml_path, 'rb') as f:
784     original_xml = f.read()
785
786   actions, actions_dict, comment_nodes = ParseActionFile(original_xml)
787
788   AddComputedActions(actions)
789   # TODO(fmantek): bring back webkit editor actions.
790   # AddWebKitEditorActions(actions)
791   AddAboutFlagsActions(actions)
792   AddWebUIActions(actions)
793
794   AddLiteralActions(actions)
795
796   # print "Scanned {0} number of files".format(number_of_files_total)
797   # print "Found {0} entries".format(len(actions))
798
799   AddAndroidActions(actions)
800   AddAutomaticResetBannerActions(actions)
801   AddBookmarkManagerActions(actions)
802   AddChromeOSActions(actions)
803   AddClosedSourceActions(actions)
804   AddExtensionActions(actions)
805   AddHistoryPageActions(actions)
806   AddKeySystemSupportActions(actions)
807
808   pretty = PrettyPrint(actions, actions_dict, comment_nodes)
809   if original_xml == pretty:
810     print 'actions.xml is correctly pretty-printed.'
811     sys.exit(0)
812   if presubmit:
813     logging.info('actions.xml is not formatted correctly; run '
814                  'extract_actions.py to fix.')
815     sys.exit(1)
816
817   # Prompt user to consent on the change.
818   if not diff_util.PromptUserToAcceptDiff(
819       original_xml, pretty, 'Is the new version acceptable?'):
820     logging.error('Aborting')
821     sys.exit(1)
822
823   print 'Creating backup file: actions.old.xml.'
824   shutil.move(actions_xml_path, 'actions.old.xml')
825
826   with open(actions_xml_path, 'wb') as f:
827     f.write(pretty)
828   print ('Updated %s. Don\'t forget to add it to your changelist' %
829          actions_xml_path)
830   return 0
831
832
833 if '__main__' == __name__:
834   sys.exit(main(sys.argv))