OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # | 2 # |
3 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 4 # Use of this source code is governed by a BSD-style license that can be |
5 # found in the LICENSE file. | 5 # found in the LICENSE file. |
6 | 6 |
7 """Extract UserMetrics "actions" strings from the Chrome source. | 7 """Extract UserMetrics "actions" strings from the Chrome source. |
8 | 8 |
9 This program generates the list of known actions we expect to see in the | 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 | 10 user behavior logs. It walks the Chrome source, looking for calls to |
11 UserMetrics functions, extracting actions and warning on improper calls, | 11 UserMetrics functions, extracting actions and warning on improper calls, |
12 as well as generating the lists of possible actions in situations where | 12 as well as generating the lists of possible actions in situations where |
13 there are many possible actions. | 13 there are many possible actions. |
14 | 14 |
15 See also: | 15 See also: |
16 content/browser/user_metrics.h | 16 content/browser/user_metrics.h |
17 http://wiki.corp.google.com/twiki/bin/view/Main/ChromeUserExperienceMetrics | 17 http://wiki.corp.google.com/twiki/bin/view/Main/ChromeUserExperienceMetrics |
18 | 18 |
19 If run with a "--hash" argument, chromeactions.txt will be updated. | 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 be changed, | |
21 a window will be prompted asking for user's consent. The old version will also | |
22 be saved in a backup file. | |
20 """ | 23 """ |
21 | 24 |
22 __author__ = 'evanm (Evan Martin)' | 25 __author__ = 'evanm (Evan Martin)' |
23 | 26 |
24 import hashlib | |
25 from HTMLParser import HTMLParser | 27 from HTMLParser import HTMLParser |
28 import logging | |
26 import os | 29 import os |
27 import re | 30 import re |
31 import shutil | |
28 import sys | 32 import sys |
33 from xml.dom import minidom | |
34 | |
29 | 35 |
30 sys.path.insert(1, os.path.join(sys.path[0], '..', '..', 'python')) | 36 sys.path.insert(1, os.path.join(sys.path[0], '..', '..', 'python')) |
31 from google import path_utils | 37 from google import path_utils |
32 | 38 |
39 # Import the metrics/common module for pretty print xml. | |
40 sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'common')) | |
41 import diffutil | |
Alexei Svitkine (slow)
2014/02/03 19:37:34
Hmm, can diffutil and pretty_print_xml use the sam
yao
2014/02/04 19:08:12
Done.
| |
42 import pretty_print_xml | |
43 | |
33 # Files that are known to use content::RecordComputedAction(), which means | 44 # Files that are known to use content::RecordComputedAction(), which means |
34 # they require special handling code in this script. | 45 # they require special handling code in this script. |
35 # To add a new file, add it to this list and add the appropriate logic to | 46 # To add a new file, add it to this list and add the appropriate logic to |
36 # generate the known actions to AddComputedActions() below. | 47 # generate the known actions to AddComputedActions() below. |
37 KNOWN_COMPUTED_USERS = ( | 48 KNOWN_COMPUTED_USERS = ( |
38 'back_forward_menu_model.cc', | 49 'back_forward_menu_model.cc', |
39 'options_page_view.cc', | 50 'options_page_view.cc', |
40 'render_view_host.cc', # called using webkit identifiers | 51 'render_view_host.cc', # called using webkit identifiers |
41 'user_metrics.cc', # method definition | 52 'user_metrics.cc', # method definition |
42 'new_tab_ui.cc', # most visited clicks 1-9 | 53 'new_tab_ui.cc', # most visited clicks 1-9 |
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
114 'xkb:se::swe', 'xkb:si::slv', 'xkb:sk::slo', 'xkb:tr::tur', 'xkb:ua::ukr', | 125 'xkb:se::swe', 'xkb:si::slv', 'xkb:sk::slo', 'xkb:tr::tur', 'xkb:ua::ukr', |
115 'xkb:us::eng', 'xkb:us:altgr-intl:eng', 'xkb:us:colemak:eng', | 126 'xkb:us::eng', 'xkb:us:altgr-intl:eng', 'xkb:us:colemak:eng', |
116 'xkb:us:dvorak:eng', 'xkb:us:intl:eng', | 127 'xkb:us:dvorak:eng', 'xkb:us:intl:eng', |
117 ) | 128 ) |
118 | 129 |
119 # The path to the root of the repository. | 130 # The path to the root of the repository. |
120 REPOSITORY_ROOT = os.path.join(path_utils.ScriptDir(), '..', '..', '..') | 131 REPOSITORY_ROOT = os.path.join(path_utils.ScriptDir(), '..', '..', '..') |
121 | 132 |
122 number_of_files_total = 0 | 133 number_of_files_total = 0 |
123 | 134 |
135 # Tags that need to be inserted to each 'action' tag and their default content. | |
136 TAGS = {'description': 'Please enter the description of this user action.', | |
137 'owner': 'Please specify the owner of this user action.'} | |
138 | |
139 # Doc for actions.xml | |
140 _DOC = 'User action XML' | |
141 | |
142 # Desired order for tag and tag attributes. | |
143 # { tag_name: [attribute_name, ...] } | |
144 ATTRIBUTE_ORDER = { | |
145 'action': ['name'], | |
146 'owner': [], | |
147 'description': [], | |
148 } | |
149 | |
150 # Tag names for top-level nodes whose children we don't want to indent. | |
151 TAGS_THAT_DONT_INDENT = ['actions'] | |
152 | |
153 # Extra vertical spacing rules for special tag names. | |
154 # {tag_name: (newlines_after_open, newlines_before_close, newlines_after_close)} | |
155 TAGS_THAT_HAVE_EXTRA_NEWLINE = { | |
156 'actions': (2, 1, 1), | |
157 'action': (1, 1, 1), | |
158 } | |
159 | |
160 # Tags that we allow to be squished into a single line for brevity. | |
161 TAGS_THAT_ALLOW_SINGLE_LINE = ['owner'] | |
162 | |
124 | 163 |
125 def AddComputedActions(actions): | 164 def AddComputedActions(actions): |
126 """Add computed actions to the actions list. | 165 """Add computed actions to the actions list. |
127 | 166 |
128 Arguments: | 167 Arguments: |
129 actions: set of actions to add to. | 168 actions: set of actions to add to. |
130 """ | 169 """ |
131 | 170 |
132 # Actions for back_forward_menu_model.cc. | 171 # Actions for back_forward_menu_model.cc. |
133 for dir in ('BackMenu_', 'ForwardMenu_'): | 172 for dir in ('BackMenu_', 'ForwardMenu_'): |
(...skipping 432 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
566 """Add actions that are used for the automatic profile settings reset banner | 605 """Add actions that are used for the automatic profile settings reset banner |
567 in chrome://settings. | 606 in chrome://settings. |
568 | 607 |
569 Arguments | 608 Arguments |
570 actions: set of actions to add to. | 609 actions: set of actions to add to. |
571 """ | 610 """ |
572 actions.add('AutomaticReset_WebUIBanner_BannerShown') | 611 actions.add('AutomaticReset_WebUIBanner_BannerShown') |
573 actions.add('AutomaticReset_WebUIBanner_ManuallyClosed') | 612 actions.add('AutomaticReset_WebUIBanner_ManuallyClosed') |
574 actions.add('AutomaticReset_WebUIBanner_ResetClicked') | 613 actions.add('AutomaticReset_WebUIBanner_ResetClicked') |
575 | 614 |
615 | |
616 class Action(object): | |
617 def __init__(self, name, description, owner): | |
618 self.name = name | |
619 self.values = {'description': description, 'owner': owner} | |
620 | |
621 | |
622 class Error(Exception): | |
623 pass | |
624 | |
625 | |
626 def _ExtractText(parent_dom, tag_name): | |
627 """Extract the text value enclosed by |tag_name| under |parent_dom|""" | |
628 child_dom = parent_dom.getElementsByTagName(tag_name) | |
629 if child_dom.length == 1: | |
630 text_dom = child_dom[0].childNodes | |
631 if text_dom.length != 1: | |
632 logging.error('More than 1 child node exist under %s' % tag_name) | |
Alexei Svitkine (slow)
2014/02/03 19:37:34
Nit: exist -> exists
yao
2014/02/04 19:08:12
Done.
| |
633 raise Error() | |
Alexei Svitkine (slow)
2014/02/03 19:37:34
Can't the message above be included in the Error?
yao
2014/02/04 19:08:12
Done.
| |
634 if text_dom[0].nodeType != minidom.Node.TEXT_NODE: | |
635 logging.error('%s\'s child node is not a text node.' % tag_name) | |
636 raise Error() | |
637 return text_dom[0].data | |
638 elif child_dom.length > 1: | |
639 logging.error('There are more than 1 %s tag.' % tag_name) | |
640 raise Error() | |
641 | |
642 | |
643 def _CreateTag(top_dom, tag_name, action_name, actions_dict): | |
644 """Create a new tag | |
645 | |
646 If action_name is in actions_dict, the values to be inserted is based on the | |
647 corresponding Action object. If action_name is not in actions_dict, the | |
648 default value from TAGS is used. | |
649 | |
650 Args: | |
651 top_dom: The parent node under which the new tag is created. | |
652 tag_name: The name of the tag to be created. | |
653 action_name: The name of an action. | |
654 actions_dict: A map from action name to Action object. | |
655 """ | |
656 tag_dom = top_dom.createElement(tag_name) | |
657 if action_name in actions_dict: | |
658 tag_dom.appendChild(top_dom.createTextNode( | |
659 actions_dict[action_name].values[tag_name])) | |
660 else: | |
661 tag_dom.appendChild(top_dom.createTextNode(TAGS.get(tag_name, ''))) | |
662 return tag_dom | |
663 | |
664 | |
576 def main(argv): | 665 def main(argv): |
577 if '--hash' in argv: | 666 # A set to store all actions. |
578 hash_output = True | |
579 else: | |
580 hash_output = False | |
581 print >>sys.stderr, "WARNING: If you added new UMA tags, you must" + \ | |
582 " use the --hash option to update chromeactions.txt." | |
583 # if we do a hash output, we want to only append NEW actions, and we know | |
584 # the file we want to work on | |
585 actions = set() | 667 actions = set() |
668 # A map from action name to an Action object. | |
669 actions_dict = {} | |
586 | 670 |
587 chromeactions_path = os.path.join(path_utils.ScriptDir(), "chromeactions.txt") | 671 actions_xml_path = os.path.join(path_utils.ScriptDir(), 'actions.xml') |
588 | 672 |
589 if hash_output: | 673 # Save the original file content. |
590 f = open(chromeactions_path) | 674 with open(actions_xml_path, 'rb') as f: |
591 for line in f: | 675 original_xml = f.read() |
592 part = line.rpartition("\t") | |
593 part = part[2].strip() | |
594 actions.add(part) | |
595 f.close() | |
596 | 676 |
677 # Parse and store the XML data currently stored in file. | |
678 dom = minidom.parse(actions_xml_path) | |
679 for action_dom in dom.getElementsByTagName('action'): | |
680 action_name = action_dom.getAttribute('name') | |
681 actions.add(action_name) | |
682 description = _ExtractText(action_dom, 'description') | |
683 owner = _ExtractText(action_dom, 'owner') | |
684 actions_dict[action_name] = Action(action_name, description, owner) | |
597 | 685 |
598 AddComputedActions(actions) | 686 AddComputedActions(actions) |
599 # TODO(fmantek): bring back webkit editor actions. | 687 # TODO(fmantek): bring back webkit editor actions. |
600 # AddWebKitEditorActions(actions) | 688 # AddWebKitEditorActions(actions) |
601 AddAboutFlagsActions(actions) | 689 AddAboutFlagsActions(actions) |
602 AddWebUIActions(actions) | 690 AddWebUIActions(actions) |
603 | 691 |
604 AddLiteralActions(actions) | 692 AddLiteralActions(actions) |
605 | 693 |
606 # print "Scanned {0} number of files".format(number_of_files_total) | 694 # print "Scanned {0} number of files".format(number_of_files_total) |
607 # print "Found {0} entries".format(len(actions)) | 695 # print "Found {0} entries".format(len(actions)) |
608 | 696 |
609 AddAndroidActions(actions) | 697 AddAndroidActions(actions) |
610 AddAutomaticResetBannerActions(actions) | 698 AddAutomaticResetBannerActions(actions) |
611 AddBookmarkManagerActions(actions) | 699 AddBookmarkManagerActions(actions) |
612 AddChromeOSActions(actions) | 700 AddChromeOSActions(actions) |
613 AddClosedSourceActions(actions) | 701 AddClosedSourceActions(actions) |
614 AddExtensionActions(actions) | 702 AddExtensionActions(actions) |
615 AddHistoryPageActions(actions) | 703 AddHistoryPageActions(actions) |
616 AddKeySystemSupportActions(actions) | 704 AddKeySystemSupportActions(actions) |
617 | 705 |
618 if hash_output: | 706 # Form new minidom nodes based on updated |actions|. |
619 f = open(chromeactions_path, "wb") | 707 new_dom = minidom.getDOMImplementation().createDocument(None, 'actions', None) |
620 | 708 top_element = new_dom.documentElement |
709 top_element.appendChild(new_dom.createComment(_DOC)) | |
621 | 710 |
622 # Print out the actions as a sorted list. | 711 # Print out the actions as a sorted list. |
623 for action in sorted(actions): | 712 for action in sorted(actions): |
624 if hash_output: | 713 action_dom = new_dom.createElement('action') |
625 hash = hashlib.md5() | 714 action_dom.setAttribute('name', action) |
626 hash.update(action) | 715 action_dom.appendChild(_CreateTag(new_dom, 'owner', action, actions_dict)) |
627 print >>f, '0x%s\t%s' % (hash.hexdigest()[:16], action) | 716 action_dom.appendChild(_CreateTag( |
628 else: | 717 new_dom, 'description', action, actions_dict)) |
629 print action | 718 top_element.appendChild(action_dom) |
630 | 719 |
631 if hash_output: | 720 # Pretty print the generate minidom node |new_dom|. |
632 print "Done. Do not forget to add chromeactions.txt to your changelist" | 721 pretty = pretty_print_xml.PrettyPrintNode( |
722 new_dom, pretty_print_xml.XmlStyle(ATTRIBUTE_ORDER, | |
723 TAGS_THAT_HAVE_EXTRA_NEWLINE, | |
724 TAGS_THAT_DONT_INDENT, | |
725 TAGS_THAT_ALLOW_SINGLE_LINE)) | |
726 if original_xml == pretty: | |
727 print 'actions.xml is correctly pretty-printed.' | |
728 sys.exit(0) | |
729 | |
730 # Prompt user to consent on the change. | |
731 if not diffutil.PromptUserToAcceptDiff( | |
732 original_xml, pretty, 'Is the new version acceptable?'): | |
733 logging.error('Aborting') | |
734 sys.exit(0) | |
735 | |
736 print 'Creating backup file: actions.before.xml.' | |
737 shutil.move(actions_xml_path, 'actions.before.xml') | |
738 | |
739 with open(actions_xml_path, 'wb') as f: | |
740 f.write(pretty) | |
741 print ('Updated %s. Do not forget to add it to your changelist' % | |
742 actions_xml_path) | |
633 return 0 | 743 return 0 |
634 | 744 |
635 | 745 |
636 if '__main__' == __name__: | 746 if '__main__' == __name__: |
637 sys.exit(main(sys.argv)) | 747 sys.exit(main(sys.argv)) |
OLD | NEW |