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.
7 """Extract UserMetrics "actions" strings from the Chrome source.
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.
16 base/metrics/user_metrics.h
17 http://wiki.corp.google.com/twiki/bin/view/Main/ChromeUserExperienceMetrics
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.
25 __author__ = 'evanm (Evan Martin)'
27 from HTMLParser import HTMLParser
33 from xml.dom import minidom
37 sys.path.insert(1, os.path.join(sys.path[0], '..', '..', 'python'))
38 from google import path_utils
40 # Import the metrics/common module for pretty print xml.
41 sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'common'))
43 import pretty_print_xml
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()
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:
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
81 # The script extracts language codes from kAcceptLanguageList, but es-419
82 # (Spanish in Latin America) is an exception.
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',
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
103 # % sort chromeos/ime/input_methods.txt | \
104 # perl -ne "print \"'\$1', \" if /^([^#]+?)\s/" | \
105 # fold -w75 -s | perl -pe 's/^/ /;s/ $//'; echo
107 # The script extracts input method IDs from input_methods.txt.
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',
125 # The path to the root of the repository.
126 REPOSITORY_ROOT = os.path.join(path_utils.ScriptDir(), '..', '..', '..')
128 number_of_files_total = 0
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 '
136 def AddComputedActions(actions):
137 """Add computed actions to the actions list.
140 actions: set of actions to add to.
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))
151 # Actions for new_tab_ui.cc.
152 for i in range(1, 10):
153 actions.add('MostVisited%d' % i)
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))
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)
168 def AddWebKitEditorActions(actions):
169 """Add editor actions from editor_client_impl.cc.
172 actions: set of actions to add to.
174 action_re = re.compile(r'''\{ [\w']+, +\w+, +"(.*)" +\},''')
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))
183 def AddClosedSourceActions(actions):
184 """Add actions that are in code which is not checked out by default
187 actions: set of actions to add to.
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')
213 def AddAndroidActions(actions):
214 """Add actions that are used by Chrome on Android.
217 actions: set of actions to add to.
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')
326 def AddAboutFlagsActions(actions):
327 """This parses the experimental feature flags for UMA actions.
330 actions: set of actions to add to.
332 about_flags = os.path.join(REPOSITORY_ROOT, 'chrome', 'browser',
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)
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
345 def AddBookmarkManagerActions(actions):
346 """Add actions that are used by BookmarkManager.
349 actions: set of actions to add to.
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')
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')
377 def AddChromeOSActions(actions):
378 """Add actions reported by non-Chrome processes in Chrome OS.
381 actions: set of actions to add to.
383 # Actions sent by Chrome OS update engine.
384 actions.add('Updater.ServerCertificateChanged')
385 actions.add('Updater.ServerCertificateFailed')
387 # Actions sent by Chrome OS cryptohome.
388 actions.add('Cryptohome.PKCS11InitFail')
390 def AddExtensionActions(actions):
391 """Add actions reported by extensions via chrome.metricsPrivate API.
394 actions: set of actions to add to.
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')
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')
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')
417 def GrepForActions(path, actions):
418 """Grep a source file for calls to UserMetrics functions.
421 path: path to the file
422 actions: set of actions to add to
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')
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' %
447 class WebUIActionsParser(HTMLParser):
448 """Parses an HTML file, looking for all tags with a 'metric' attribute.
449 Adds user actions corresponding to any metrics found.
452 actions: set of actions to add to
454 def __init__(self, actions):
455 HTMLParser.__init__(self)
456 self.actions = actions
458 def handle_starttag(self, tag, attrs):
459 # We only care to examine tags that have a 'metric' attribute.
461 if not 'metric' in attrs:
464 # Boolean metrics have two corresponding actions. All other metrics have
465 # just one corresponding action. By default, we check the 'dataType'
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':
472 # Radio buttons are boolean if and only if their values are 'true' or
474 assert(attrs['type'] == 'radio')
475 if 'value' in attrs and attrs['value'] in ['true', 'false']:
479 self.actions.add(attrs['metric'] + '_Enable')
480 self.actions.add(attrs['metric'] + '_Disable')
482 self.actions.add(attrs['metric'])
484 def GrepForWebUIActions(path, actions):
485 """Grep a WebUI source file for elements with associated metrics.
488 path: path to the file
489 actions: set of actions to add to
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.
500 print "Error encountered for path %s" % path
506 def WalkDirectory(root_path, actions, extensions, callback):
507 for path, dirs, files in os.walk(root_path):
513 ext = os.path.splitext(file)[1]
514 if ext in extensions:
515 callback(os.path.join(path, file), actions)
517 def AddLiteralActions(actions):
518 """Add literal actions specified via calls to UserMetrics functions.
521 actions: set of actions to add to.
523 EXTENSIONS = ('.cc', '.mm', '.c', '.m')
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 net_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'net'))
533 WalkDirectory(net_root, actions, EXTENSIONS, GrepForActions)
534 webkit_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'webkit'))
535 WalkDirectory(os.path.join(webkit_root, 'glue'), actions, EXTENSIONS,
537 WalkDirectory(os.path.join(webkit_root, 'port'), actions, EXTENSIONS,
540 def AddWebUIActions(actions):
541 """Add user actions defined in WebUI files.
544 actions: set of actions to add to.
546 resources_root = os.path.join(REPOSITORY_ROOT, 'chrome', 'browser',
548 WalkDirectory(resources_root, actions, ('.html'), GrepForWebUIActions)
550 def AddHistoryPageActions(actions):
551 """Add actions that are used in History page.
554 actions: set of actions to add to.
556 actions.add('HistoryPage_BookmarkStarClicked')
557 actions.add('HistoryPage_EntryMenuRemoveFromHistory')
558 actions.add('HistoryPage_EntryLinkClick')
559 actions.add('HistoryPage_EntryLinkRightClick')
560 actions.add('HistoryPage_SearchResultClick')
561 actions.add('HistoryPage_EntryMenuShowMoreFromSite')
562 actions.add('HistoryPage_NewestHistoryClick')
563 actions.add('HistoryPage_NewerHistoryClick')
564 actions.add('HistoryPage_OlderHistoryClick')
565 actions.add('HistoryPage_Search')
566 actions.add('HistoryPage_InitClearBrowsingData')
567 actions.add('HistoryPage_RemoveSelected')
568 actions.add('HistoryPage_SearchResultRemove')
569 actions.add('HistoryPage_ConfirmRemoveSelected')
570 actions.add('HistoryPage_CancelRemoveSelected')
572 def AddKeySystemSupportActions(actions):
573 """Add actions that are used for key system support metrics.
576 actions: set of actions to add to.
578 actions.add('KeySystemSupport.Widevine.Queried')
579 actions.add('KeySystemSupport.WidevineWithType.Queried')
580 actions.add('KeySystemSupport.Widevine.Supported')
581 actions.add('KeySystemSupport.WidevineWithType.Supported')
583 def AddAutomaticResetBannerActions(actions):
584 """Add actions that are used for the automatic profile settings reset banners
585 in chrome://settings.
588 actions: set of actions to add to.
590 # These actions relate to the the automatic settings reset banner shown as
591 # a result of the reset prompt.
592 actions.add('AutomaticReset_WebUIBanner_BannerShown')
593 actions.add('AutomaticReset_WebUIBanner_ManuallyClosed')
594 actions.add('AutomaticReset_WebUIBanner_ResetClicked')
596 # These actions relate to the the automatic settings reset banner shown as
597 # a result of settings hardening.
598 actions.add('AutomaticSettingsReset_WebUIBanner_BannerShown')
599 actions.add('AutomaticSettingsReset_WebUIBanner_ManuallyClosed')
600 actions.add('AutomaticSettingsReset_WebUIBanner_LearnMoreClicked')
603 class Error(Exception):
607 def _ExtractText(parent_dom, tag_name):
608 """Extract the text enclosed by |tag_name| under |parent_dom|
611 parent_dom: The parent Element under which text node is searched for.
612 tag_name: The name of the tag which contains a text node.
615 A (list of) string enclosed by |tag_name| under |parent_dom|.
618 for child_dom in parent_dom.getElementsByTagName(tag_name):
619 text_dom = child_dom.childNodes
620 if text_dom.length != 1:
621 raise Error('More than 1 child node exists under %s' % tag_name)
622 if text_dom[0].nodeType != minidom.Node.TEXT_NODE:
623 raise Error('%s\'s child node is not a text node.' % tag_name)
624 texts.append(text_dom[0].data)
628 class Action(object):
629 def __init__(self, name, description, owners, obsolete=None):
631 self.description = description
633 self.obsolete = obsolete
636 def ParseActionFile(file_content):
637 """Parse the XML data currently stored in the file.
640 file_content: a string containing the action XML file content.
643 (actions, actions_dict) actions is a set with all user actions' names.
644 actions_dict is a dict from user action name to Action object.
646 dom = minidom.parseString(file_content)
649 # Get top-level comments. It is assumed that all comments are placed before
650 # <acionts> tag. Therefore the loop will stop if it encounters a non-comment
652 for node in dom.childNodes:
653 if node.nodeType == minidom.Node.COMMENT_NODE:
654 comment_nodes.append(node)
660 # Get each user action data.
661 for action_dom in dom.getElementsByTagName('action'):
662 action_name = action_dom.getAttribute('name')
663 actions.add(action_name)
665 owners = _ExtractText(action_dom, 'owner')
666 # There is only one description for each user action. Get the first element
667 # of the returned list.
668 description_list = _ExtractText(action_dom, 'description')
669 if len(description_list) > 1:
670 logging.error('user actions "%s" has more than one descriptions. Exactly '
671 'one description is needed for each user action. Please '
674 description = description_list[0] if description_list else None
675 # There is at most one obsolete tag for each user action.
676 obsolete_list = _ExtractText(action_dom, 'obsolete')
677 if len(obsolete_list) > 1:
678 logging.error('user actions "%s" has more than one obsolete tag. At most '
679 'one obsolete tag can be added for each user action. Please'
680 ' fix.', action_name)
682 obsolete = obsolete_list[0] if obsolete_list else None
683 actions_dict[action_name] = Action(action_name, description, owners,
685 return actions, actions_dict, comment_nodes
688 def _CreateActionTag(doc, action_name, action_object):
689 """Create a new action tag.
691 Format of an action tag:
694 <description>Description.</description>
695 <obsolete>Deprecated.</obsolete>
698 <obsolete> is an optional tag. It's added to user actions that are no longer
701 If action_name is in actions_dict, the values to be inserted are based on the
702 corresponding Action object. If action_name is not in actions_dict, the
703 default value from TAGS is used.
706 doc: The document under which the new action tag is created.
707 action_name: The name of an action.
708 action_object: An action object representing the data to be inserted.
711 An action tag Element with proper children elements.
713 action_dom = doc.createElement('action')
714 action_dom.setAttribute('name', action_name)
717 if action_object and action_object.owners:
718 # If owners for this action is not None, use the stored value. Otherwise,
719 # use the default value.
720 for owner in action_object.owners:
721 owner_dom = doc.createElement('owner')
722 owner_dom.appendChild(doc.createTextNode(owner))
723 action_dom.appendChild(owner_dom)
726 owner_dom = doc.createElement('owner')
727 owner_dom.appendChild(doc.createTextNode(TAGS.get('owner', '')))
728 action_dom.appendChild(owner_dom)
730 # Create description tag.
731 description_dom = doc.createElement('description')
732 action_dom.appendChild(description_dom)
733 if action_object and action_object.description:
734 # If description for this action is not None, use the store value.
735 # Otherwise, use the default value.
736 description_dom.appendChild(doc.createTextNode(
737 action_object.description))
739 description_dom.appendChild(doc.createTextNode(
740 TAGS.get('description', '')))
742 # Create obsolete tag.
743 if action_object and action_object.obsolete:
744 obsolete_dom = doc.createElement('obsolete')
745 action_dom.appendChild(obsolete_dom)
746 obsolete_dom.appendChild(doc.createTextNode(
747 action_object.obsolete))
752 def PrettyPrint(actions, actions_dict, comment_nodes=[]):
753 """Given a list of action data, create a well-printed minidom document.
756 actions: A list of action names.
757 actions_dict: A mappting from action name to Action object.
760 A well-printed minidom document that represents the input action data.
762 doc = minidom.Document()
764 # Attach top-level comments.
765 for node in comment_nodes:
766 doc.appendChild(node)
768 actions_element = doc.createElement('actions')
769 doc.appendChild(actions_element)
771 # Attach action node based on updated |actions|.
772 for action in sorted(actions):
773 actions_element.appendChild(
774 _CreateActionTag(doc, action, actions_dict.get(action, None)))
776 return print_style.GetPrintStyle().PrettyPrintNode(doc)
780 presubmit = ('--presubmit' in argv)
781 actions_xml_path = os.path.join(path_utils.ScriptDir(), 'actions.xml')
783 # Save the original file content.
784 with open(actions_xml_path, 'rb') as f:
785 original_xml = f.read()
787 actions, actions_dict, comment_nodes = ParseActionFile(original_xml)
789 AddComputedActions(actions)
790 # TODO(fmantek): bring back webkit editor actions.
791 # AddWebKitEditorActions(actions)
792 AddAboutFlagsActions(actions)
793 AddWebUIActions(actions)
795 AddLiteralActions(actions)
797 # print "Scanned {0} number of files".format(number_of_files_total)
798 # print "Found {0} entries".format(len(actions))
800 AddAndroidActions(actions)
801 AddAutomaticResetBannerActions(actions)
802 AddBookmarkManagerActions(actions)
803 AddChromeOSActions(actions)
804 AddClosedSourceActions(actions)
805 AddExtensionActions(actions)
806 AddHistoryPageActions(actions)
807 AddKeySystemSupportActions(actions)
809 pretty = PrettyPrint(actions, actions_dict, comment_nodes)
810 if original_xml == pretty:
811 print 'actions.xml is correctly pretty-printed.'
814 logging.info('actions.xml is not formatted correctly; run '
815 'extract_actions.py to fix.')
818 # Prompt user to consent on the change.
819 if not diff_util.PromptUserToAcceptDiff(
820 original_xml, pretty, 'Is the new version acceptable?'):
821 logging.error('Aborting')
824 print 'Creating backup file: actions.old.xml.'
825 shutil.move(actions_xml_path, 'actions.old.xml')
827 with open(actions_xml_path, 'wb') as f:
829 print ('Updated %s. Don\'t forget to add it to your changelist' %
834 if '__main__' == __name__:
835 sys.exit(main(sys.argv))