Make hitting "Enter" submit the add/change profile dialog.
[chromium-blink-merge.git] / chrome / tools / extract_actions.py
blobfc2e40dd5b40c8351099654dcbd8836c3aa5f4ea
1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Extract UserMetrics "actions" strings from the Chrome source.
8 This program generates the list of known actions we expect to see in the
9 user behavior logs. It walks the Chrome source, looking for calls to
10 UserMetrics functions, extracting actions and warning on improper calls,
11 as well as generating the lists of possible actions in situations where
12 there are many possible actions.
14 See also:
15 content/browser/user_metrics.h
16 http://wiki.corp.google.com/twiki/bin/view/Main/ChromeUserExperienceMetrics
18 If run with a "--hash" argument, chromeactions.txt will be updated.
19 """
21 __author__ = 'evanm (Evan Martin)'
23 import hashlib
24 from HTMLParser import HTMLParser
25 import os
26 import re
27 import sys
29 sys.path.insert(1, os.path.join(sys.path[0], '..', '..', 'tools', 'python'))
30 from google import path_utils
32 # Files that are known to use content::RecordComputedAction(), which means
33 # they require special handling code in this script.
34 # To add a new file, add it to this list and add the appropriate logic to
35 # generate the known actions to AddComputedActions() below.
36 KNOWN_COMPUTED_USERS = (
37 'back_forward_menu_model.cc',
38 'options_page_view.cc',
39 'render_view_host.cc', # called using webkit identifiers
40 'user_metrics.cc', # method definition
41 'new_tab_ui.cc', # most visited clicks 1-9
42 'extension_metrics_module.cc', # extensions hook for user metrics
43 'safe_browsing_blocking_page.cc', # various interstitial types and actions
44 'language_options_handler_common.cc', # languages and input methods in CrOS
45 'cros_language_options_handler.cc', # languages and input methods in CrOS
46 'about_flags.cc', # do not generate a warning; see AddAboutFlagsActions()
47 'external_metrics.cc', # see AddChromeOSActions()
48 'core_options_handler.cc', # see AddWebUIActions()
49 'browser_render_process_host.cc' # see AddRendererActions()
52 # Language codes used in Chrome. The list should be updated when a new
53 # language is added to app/l10n_util.cc, as follows:
55 # % (cat app/l10n_util.cc | \
56 # perl -n0e 'print $1 if /kAcceptLanguageList.*?\{(.*?)\}/s' | \
57 # perl -nle 'print $1, if /"(.*)"/'; echo 'es-419') | \
58 # sort | perl -pe "s/(.*)\n/'\$1', /" | \
59 # fold -w75 -s | perl -pe 's/^/ /;s/ $//'; echo
61 # The script extracts language codes from kAcceptLanguageList, but es-419
62 # (Spanish in Latin America) is an exception.
63 LANGUAGE_CODES = (
64 'af', 'am', 'ar', 'az', 'be', 'bg', 'bh', 'bn', 'br', 'bs', 'ca', 'co',
65 'cs', 'cy', 'da', 'de', 'de-AT', 'de-CH', 'de-DE', 'el', 'en', 'en-AU',
66 'en-CA', 'en-GB', 'en-NZ', 'en-US', 'en-ZA', 'eo', 'es', 'es-419', 'et',
67 'eu', 'fa', 'fi', 'fil', 'fo', 'fr', 'fr-CA', 'fr-CH', 'fr-FR', 'fy',
68 'ga', 'gd', 'gl', 'gn', 'gu', 'ha', 'haw', 'he', 'hi', 'hr', 'hu', 'hy',
69 'ia', 'id', 'is', 'it', 'it-CH', 'it-IT', 'ja', 'jw', 'ka', 'kk', 'km',
70 'kn', 'ko', 'ku', 'ky', 'la', 'ln', 'lo', 'lt', 'lv', 'mk', 'ml', 'mn',
71 'mo', 'mr', 'ms', 'mt', 'nb', 'ne', 'nl', 'nn', 'no', 'oc', 'om', 'or',
72 'pa', 'pl', 'ps', 'pt', 'pt-BR', 'pt-PT', 'qu', 'rm', 'ro', 'ru', 'sd',
73 'sh', 'si', 'sk', 'sl', 'sn', 'so', 'sq', 'sr', 'st', 'su', 'sv', 'sw',
74 'ta', 'te', 'tg', 'th', 'ti', 'tk', 'to', 'tr', 'tt', 'tw', 'ug', 'uk',
75 'ur', 'uz', 'vi', 'xh', 'yi', 'yo', 'zh', 'zh-CN', 'zh-TW', 'zu',
78 # Input method IDs used in Chrome OS. The list should be updated when a
79 # new input method is added to
80 # chrome/browser/chromeos/input_method/input_methods.txt in the Chrome tree, as
81 # follows:
83 # % sort chrome/browser/chromeos/input_method/input_methods.txt | \
84 # perl -ne "print \"'\$1', \" if /^([^#]+?)\s/" | \
85 # fold -w75 -s | perl -pe 's/^/ /;s/ $//'; echo
87 # The script extracts input method IDs from input_methods.txt.
88 INPUT_METHOD_IDS = (
89 'english-m', 'm17n:am:sera', 'm17n:ar:kbd', 'm17n:bn:itrans',
90 'm17n:fa:isiri', 'm17n:gu:itrans', 'm17n:hi:itrans', 'm17n:kn:itrans',
91 'm17n:ml:itrans', 'm17n:mr:itrans', 'm17n:ta:inscript', 'm17n:ta:itrans',
92 'm17n:ta:phonetic', 'm17n:ta:tamil99', 'm17n:ta:typewriter',
93 'm17n:te:itrans', 'm17n:th:kesmanee', 'm17n:th:pattachote',
94 'm17n:th:tis820', 'm17n:vi:tcvn', 'm17n:vi:telex', 'm17n:vi:viqr',
95 'm17n:vi:vni', 'm17n:zh:cangjie', 'm17n:zh:quick', 'mozc', 'mozc-chewing',
96 'mozc-dv', 'mozc-hangul', 'mozc-jp', 'pinyin', 'pinyin-dv', 'xkb:be::fra',
97 'xkb:be::ger', 'xkb:be::nld', 'xkb:bg::bul', 'xkb:bg:phonetic:bul',
98 'xkb:br::por', 'xkb:ca::fra', 'xkb:ca:eng:eng', 'xkb:ch::ger',
99 'xkb:ch:fr:fra', 'xkb:cz::cze', 'xkb:de::ger', 'xkb:de:neo:ger',
100 'xkb:dk::dan', 'xkb:ee::est', 'xkb:es::spa', 'xkb:es:cat:cat',
101 'xkb:fi::fin', 'xkb:fr::fra', 'xkb:gb:dvorak:eng', 'xkb:gb:extd:eng',
102 'xkb:gr::gre', 'xkb:hr::scr', 'xkb:hu::hun', 'xkb:il::heb', 'xkb:it::ita',
103 'xkb:jp::jpn', 'xkb:kr:kr104:kor', 'xkb:latam::spa', 'xkb:lt::lit',
104 'xkb:lv:apostrophe:lav', 'xkb:no::nob', 'xkb:pl::pol', 'xkb:pt::por',
105 'xkb:ro::rum', 'xkb:rs::srp', 'xkb:ru::rus', 'xkb:ru:phonetic:rus',
106 'xkb:se::swe', 'xkb:si::slv', 'xkb:sk::slo', 'xkb:tr::tur', 'xkb:ua::ukr',
107 'xkb:us::eng', 'xkb:us:altgr-intl:eng', 'xkb:us:colemak:eng',
108 'xkb:us:dvorak:eng', 'xkb:us:intl:eng',
111 number_of_files_total = 0
114 def AddComputedActions(actions):
115 """Add computed actions to the actions list.
117 Arguments:
118 actions: set of actions to add to.
121 # Actions for back_forward_menu_model.cc.
122 for dir in ('BackMenu_', 'ForwardMenu_'):
123 actions.add(dir + 'ShowFullHistory')
124 actions.add(dir + 'Popup')
125 for i in range(1, 20):
126 actions.add(dir + 'HistoryClick' + str(i))
127 actions.add(dir + 'ChapterClick' + str(i))
129 # Actions for new_tab_ui.cc.
130 for i in range(1, 10):
131 actions.add('MostVisited%d' % i)
133 # Actions for safe_browsing_blocking_page.cc.
134 for interstitial in ('Phishing', 'Malware', 'Multiple'):
135 for action in ('Show', 'Proceed', 'DontProceed', 'ForcedDontProceed'):
136 actions.add('SBInterstitial%s%s' % (interstitial, action))
138 # Actions for language_options_handler.cc (Chrome OS specific).
139 for input_method_id in INPUT_METHOD_IDS:
140 actions.add('LanguageOptions_DisableInputMethod_%s' % input_method_id)
141 actions.add('LanguageOptions_EnableInputMethod_%s' % input_method_id)
142 actions.add('InputMethodOptions_Open_%s' % input_method_id)
143 for language_code in LANGUAGE_CODES:
144 actions.add('LanguageOptions_UiLanguageChange_%s' % language_code)
145 actions.add('LanguageOptions_SpellCheckLanguageChange_%s' % language_code)
147 def AddWebKitEditorActions(actions):
148 """Add editor actions from editor_client_impl.cc.
150 Arguments:
151 actions: set of actions to add to.
153 action_re = re.compile(r'''\{ [\w']+, +\w+, +"(.*)" +\},''')
155 editor_file = os.path.join(path_utils.ScriptDir(), '..', '..', 'webkit',
156 'api', 'src','EditorClientImpl.cc')
157 for line in open(editor_file):
158 match = action_re.search(line)
159 if match: # Plain call to RecordAction
160 actions.add(match.group(1))
162 def AddClosedSourceActions(actions):
163 """Add actions that are in code which is not checked out by default
165 Arguments
166 actions: set of actions to add to.
168 actions.add('PDF.PrintPage')
169 actions.add('PDF.FitToHeightButton')
170 actions.add('PDF.FitToWidthButton')
171 actions.add('PDF.LoadFailure')
172 actions.add('PDF.LoadSuccess')
173 actions.add('PDF.PreviewDocumentLoadFailure')
174 actions.add('PDF.ZoomFromBrowser')
175 actions.add('PDF.ZoomOutButton')
176 actions.add('PDF.ZoomInButton')
177 actions.add('PDF_Unsupported_Rights_Management')
178 actions.add('PDF_Unsupported_XFA')
179 actions.add('PDF_Unsupported_3D')
180 actions.add('PDF_Unsupported_Movie')
181 actions.add('PDF_Unsupported_Sound')
182 actions.add('PDF_Unsupported_Screen')
183 actions.add('PDF_Unsupported_Portfolios_Packages')
184 actions.add('PDF_Unsupported_Attachment')
185 actions.add('PDF_Unsupported_Digital_Signature')
186 actions.add('PDF_Unsupported_Shared_Review')
187 actions.add('PDF_Unsupported_Shared_Form')
188 actions.add('PDF_Unsupported_Bookmarks')
190 def AddAndroidActions(actions):
191 """Add actions that are used by Chrome on Android.
193 Arguments
194 actions: set of actions to add to.
196 actions.add('MobileBeamCallbackSuccess')
197 actions.add('MobileBeamInvalidAppState')
198 actions.add('MobileBreakpadUploadAttempt')
199 actions.add('MobileBreakpadUploadFailure')
200 actions.add('MobileBreakpadUploadSuccess')
201 actions.add('MobileContextMenuCopyImageLinkAddress')
202 actions.add('MobileContextMenuCopyLinkAddress')
203 actions.add('MobileContextMenuCopyLinkText')
204 actions.add('MobileContextMenuImage')
205 actions.add('MobileContextMenuLink')
206 actions.add('MobileContextMenuOpenImageInNewTab')
207 actions.add('MobileContextMenuOpenLink')
208 actions.add('MobileContextMenuOpenLinkInIncognito')
209 actions.add('MobileContextMenuOpenLinkInNewTab')
210 actions.add('MobileContextMenuSaveImage')
211 actions.add('MobileContextMenuShareLink')
212 actions.add('MobileContextMenuText')
213 actions.add('MobileContextMenuViewImage')
214 actions.add('MobileFreAttemptSignIn')
215 actions.add('MobileFreSignInSuccessful')
216 actions.add('MobileFreSkipSignIn')
217 actions.add('MobileMenuAddToBookmarks')
218 actions.add('MobileMenuAllBookmarks')
219 actions.add('MobileMenuBack')
220 actions.add('MobileMenuCloseAllTabs')
221 actions.add('MobileMenuCloseTab')
222 actions.add('MobileMenuFeedback')
223 actions.add('MobileMenuFindInPage')
224 actions.add('MobileMenuForward')
225 actions.add('MobileMenuFullscreen')
226 actions.add('MobileMenuNewIncognitoTab')
227 actions.add('MobileMenuNewTab')
228 actions.add('MobileMenuOpenTabs')
229 actions.add('MobileMenuQuit')
230 actions.add('MobileMenuReload')
231 actions.add('MobileMenuSettings')
232 actions.add('MobileMenuShare')
233 actions.add('MobileMenuShow')
234 actions.add('MobileNTPBookmark')
235 actions.add('MobileNTPForeignSession')
236 actions.add('MobileNTPMostVisited')
237 actions.add('MobileNTPSwitchToBookmarks')
238 actions.add('MobileNTPSwitchToIncognito')
239 actions.add('MobileNTPSwitchToMostVisited')
240 actions.add('MobileNTPSwitchToOpenTabs')
241 actions.add('MobileNewTabOpened')
242 actions.add('MobileOmniboxSearch')
243 actions.add('MobileOmniboxVoiceSearch')
244 actions.add('MobilePageLoaded')
245 actions.add('MobilePageLoadedDesktopUserAgent')
246 actions.add('MobilePageLoadedWithKeyboard')
247 actions.add('MobileReceivedExternalIntent')
248 actions.add('MobileRendererCrashed')
249 actions.add('MobileShortcutAllBookmarks')
250 actions.add('MobileShortcutFindInPage')
251 actions.add('MobileShortcutNewIncognitoTab')
252 actions.add('MobileShortcutNewTab')
253 actions.add('MobileSideSwipeFinished')
254 actions.add('MobileStackViewCloseTab')
255 actions.add('MobileStackViewSwipeCloseTab')
256 actions.add('MobileTabClobbered')
257 actions.add('MobileTabClosed')
258 actions.add('MobileTabStripCloseTab')
259 actions.add('MobileTabStripNewTab')
260 actions.add('MobileTabSwitched')
261 actions.add('MobileToolbarBack')
262 actions.add('MobileToolbarForward')
263 actions.add('MobileToolbarReload')
264 actions.add('MobileToolbarShowMenu')
265 actions.add('MobileToolbarShowStackView')
266 actions.add('MobileToolbarStackViewNewTab')
267 actions.add('MobileToolbarToggleBookmark')
268 actions.add('SystemBack')
269 actions.add('SystemBackForNavigation')
271 def AddAboutFlagsActions(actions):
272 """This parses the experimental feature flags for UMA actions.
274 Arguments:
275 actions: set of actions to add to.
277 about_flags = os.path.join(path_utils.ScriptDir(), '..', 'browser',
278 'about_flags.cc')
279 flag_name_re = re.compile(r'\s*"([0-9a-zA-Z\-_]+)",\s*// FLAGS:RECORD_UMA')
280 for line in open(about_flags):
281 match = flag_name_re.search(line)
282 if match:
283 actions.add("AboutFlags_" + match.group(1))
284 # If the line contains the marker but was not matched by the regex, put up
285 # an error if the line is not a comment.
286 elif 'FLAGS:RECORD_UMA' in line and line[0:2] != '//':
287 print >>sys.stderr, 'WARNING: This line is marked for recording ' + \
288 'about:flags metrics, but is not in the proper format:\n' + line
290 def AddChromeOSActions(actions):
291 """Add actions reported by non-Chrome processes in Chrome OS.
293 Arguments:
294 actions: set of actions to add to.
296 # Actions sent by Chrome OS update engine.
297 actions.add('Updater.ServerCertificateChanged')
298 actions.add('Updater.ServerCertificateFailed')
300 # Actions sent by Chrome OS cryptohome.
301 actions.add('Cryptohome.PKCS11InitFail')
303 def AddExtensionActions(actions):
304 """Add actions reported by extensions via chrome.metricsPrivate API.
306 Arguments:
307 actions: set of actions to add to.
309 # Actions sent by Chrome OS File Browser.
310 actions.add('FileBrowser.CreateNewFolder')
311 actions.add('FileBrowser.PhotoEditor.Edit')
312 actions.add('FileBrowser.PhotoEditor.View')
314 def GrepForActions(path, actions):
315 """Grep a source file for calls to UserMetrics functions.
317 Arguments:
318 path: path to the file
319 actions: set of actions to add to
321 global number_of_files_total
322 number_of_files_total = number_of_files_total + 1
323 # we look for the UserMetricsAction structure constructor
324 # this should be on one line
325 action_re = re.compile(r'[^a-zA-Z]UserMetricsAction\("([^"]*)')
326 malformed_action_re = re.compile(r'[^a-zA-Z]UserMetricsAction\([^"]')
327 computed_action_re = re.compile(r'RecordComputedAction')
328 line_number = 0
329 for line in open(path):
330 line_number = line_number + 1
331 match = action_re.search(line)
332 if match: # Plain call to RecordAction
333 actions.add(match.group(1))
334 elif malformed_action_re.search(line):
335 # Warn if this line is using RecordAction incorrectly.
336 print >>sys.stderr, ('WARNING: %s has malformed call to RecordAction'
337 ' at %d' % (path, line_number))
338 elif computed_action_re.search(line):
339 # Warn if this file shouldn't be calling RecordComputedAction.
340 if os.path.basename(path) not in KNOWN_COMPUTED_USERS:
341 print >>sys.stderr, ('WARNING: %s has RecordComputedAction at %d' %
342 (path, line_number))
344 class WebUIActionsParser(HTMLParser):
345 """Parses an HTML file, looking for all tags with a 'metric' attribute.
346 Adds user actions corresponding to any metrics found.
348 Arguments:
349 actions: set of actions to add to
351 def __init__(self, actions):
352 HTMLParser.__init__(self)
353 self.actions = actions
355 def handle_starttag(self, tag, attrs):
356 # We only care to examine tags that have a 'metric' attribute.
357 attrs = dict(attrs)
358 if not 'metric' in attrs:
359 return
361 # Boolean metrics have two corresponding actions. All other metrics have
362 # just one corresponding action. By default, we check the 'dataType'
363 # attribute.
364 is_boolean = ('dataType' in attrs and attrs['dataType'] == 'boolean')
365 if 'type' in attrs and attrs['type'] in ('checkbox', 'radio'):
366 if attrs['type'] == 'checkbox':
367 is_boolean = True
368 else:
369 # Radio buttons are boolean if and only if their values are 'true' or
370 # 'false'.
371 assert(attrs['type'] == 'radio')
372 if 'value' in attrs and attrs['value'] in ['true', 'false']:
373 is_boolean = True
375 if is_boolean:
376 self.actions.add(attrs['metric'] + '_Enable')
377 self.actions.add(attrs['metric'] + '_Disable')
378 else:
379 self.actions.add(attrs['metric'])
381 def GrepForWebUIActions(path, actions):
382 """Grep a WebUI source file for elements with associated metrics.
384 Arguments:
385 path: path to the file
386 actions: set of actions to add to
388 try:
389 parser = WebUIActionsParser(actions)
390 parser.feed(open(path).read())
391 except Exception, e:
392 print "Error encountered for path %s" % path
393 raise e
394 finally:
395 parser.close()
397 def WalkDirectory(root_path, actions, extensions, callback):
398 for path, dirs, files in os.walk(root_path):
399 if '.svn' in dirs:
400 dirs.remove('.svn')
401 if '.git' in dirs:
402 dirs.remove('.git')
403 for file in files:
404 ext = os.path.splitext(file)[1]
405 if ext in extensions:
406 callback(os.path.join(path, file), actions)
408 def GrepForRendererActions(path, actions):
409 """Grep a source file for calls to RenderThread::RecordUserMetrics.
411 Arguments:
412 path: path to the file
413 actions: set of actions to add to
415 # We look for the ViewHostMsg_UserMetricsRecordAction constructor.
416 # This should be on one line.
417 action_re = re.compile(
418 r'[^a-zA-Z]RenderThread::RecordUserMetrics\("([^"]*)')
419 line_number = 0
420 for line in open(path):
421 match = action_re.search(line)
422 if match: # Plain call to RecordAction
423 actions.add(match.group(1))
425 def AddLiteralActions(actions):
426 """Add literal actions specified via calls to UserMetrics functions.
428 Arguments:
429 actions: set of actions to add to.
431 EXTENSIONS = ('.cc', '.mm', '.c', '.m')
433 # Walk the source tree to process all .cc files.
434 chrome_root = os.path.normpath(os.path.join(path_utils.ScriptDir(), '..'))
435 WalkDirectory(chrome_root, actions, EXTENSIONS, GrepForActions)
436 content_root = os.path.normpath(os.path.join(path_utils.ScriptDir(),
437 '..', '..', 'content'))
438 WalkDirectory(content_root, actions, EXTENSIONS, GrepForActions)
439 webkit_root = os.path.normpath(os.path.join(path_utils.ScriptDir(),
440 '..', '..', 'webkit'))
441 WalkDirectory(os.path.join(webkit_root, 'glue'), actions, EXTENSIONS,
442 GrepForActions)
443 WalkDirectory(os.path.join(webkit_root, 'port'), actions, EXTENSIONS,
444 GrepForActions)
446 def AddWebUIActions(actions):
447 """Add user actions defined in WebUI files.
449 Arguments:
450 actions: set of actions to add to.
452 resources_root = os.path.join(path_utils.ScriptDir(), '..', 'browser',
453 'resources')
454 WalkDirectory(resources_root, actions, ('.html'), GrepForWebUIActions)
456 def AddRendererActions(actions):
457 """Add user actions sent via calls to RenderThread::RecordUserMetrics.
459 Arguments:
460 actions: set of actions to add to.
462 EXTENSIONS = ('.cc', '.mm', '.c', '.m')
464 chrome_renderer_root = os.path.join(path_utils.ScriptDir(), '..', 'renderer')
465 content_renderer_root = os.path.join(path_utils.ScriptDir(), '..', '..',
466 'content', 'renderer')
467 WalkDirectory(chrome_renderer_root, actions, EXTENSIONS,
468 GrepForRendererActions)
469 WalkDirectory(content_renderer_root, actions, EXTENSIONS,
470 GrepForRendererActions)
472 def main(argv):
473 if '--hash' in argv:
474 hash_output = True
475 else:
476 hash_output = False
477 print >>sys.stderr, "WARNING: If you added new UMA tags, you must" + \
478 " use the --hash option to update chromeactions.txt."
479 # if we do a hash output, we want to only append NEW actions, and we know
480 # the file we want to work on
481 actions = set()
483 chromeactions_path = os.path.join(path_utils.ScriptDir(), "chromeactions.txt")
485 if hash_output:
486 f = open(chromeactions_path)
487 for line in f:
488 part = line.rpartition("\t")
489 part = part[2].strip()
490 actions.add(part)
491 f.close()
494 AddComputedActions(actions)
495 # TODO(fmantek): bring back webkit editor actions.
496 # AddWebKitEditorActions(actions)
497 AddAboutFlagsActions(actions)
498 AddWebUIActions(actions)
499 AddRendererActions(actions)
501 AddLiteralActions(actions)
503 # print "Scanned {0} number of files".format(number_of_files_total)
504 # print "Found {0} entries".format(len(actions))
506 AddClosedSourceActions(actions)
507 AddChromeOSActions(actions)
508 AddExtensionActions(actions)
509 AddAndroidActions(actions)
511 if hash_output:
512 f = open(chromeactions_path, "w")
515 # Print out the actions as a sorted list.
516 for action in sorted(actions):
517 if hash_output:
518 hash = hashlib.md5()
519 hash.update(action)
520 print >>f, '0x%s\t%s' % (hash.hexdigest()[:16], action)
521 else:
522 print action
524 if hash_output:
525 print "Done. Do not forget to add chromeactions.txt to your changelist"
526 return 0
529 if '__main__' == __name__:
530 sys.exit(main(sys.argv))