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 base/metrics/user_metrics.h | 16 base/metrics/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 After extracting all actions, the content will go through a pretty print | 19 If run with a "--hash" argument, chromeactions.txt will be updated. |
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 """ | 20 """ |
24 | 21 |
25 __author__ = 'evanm (Evan Martin)' | 22 __author__ = 'evanm (Evan Martin)' |
26 | 23 |
| 24 import hashlib |
27 from HTMLParser import HTMLParser | 25 from HTMLParser import HTMLParser |
28 import logging | |
29 import os | 26 import os |
30 import re | 27 import re |
31 import shutil | |
32 import sys | 28 import sys |
33 from xml.dom import minidom | |
34 | |
35 import print_style | |
36 | 29 |
37 sys.path.insert(1, os.path.join(sys.path[0], '..', '..', 'python')) | 30 sys.path.insert(1, os.path.join(sys.path[0], '..', '..', 'python')) |
38 from google import path_utils | 31 from google import path_utils |
39 | 32 |
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 | 33 # Files that are known to use content::RecordComputedAction(), which means |
46 # they require special handling code in this script. | 34 # 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 | 35 # To add a new file, add it to this list and add the appropriate logic to |
48 # generate the known actions to AddComputedActions() below. | 36 # generate the known actions to AddComputedActions() below. |
49 KNOWN_COMPUTED_USERS = ( | 37 KNOWN_COMPUTED_USERS = ( |
50 'back_forward_menu_model.cc', | 38 'back_forward_menu_model.cc', |
51 'options_page_view.cc', | 39 'options_page_view.cc', |
52 'render_view_host.cc', # called using webkit identifiers | 40 'render_view_host.cc', # called using webkit identifiers |
53 'user_metrics.cc', # method definition | 41 'user_metrics.cc', # method definition |
54 'new_tab_ui.cc', # most visited clicks 1-9 | 42 'new_tab_ui.cc', # most visited clicks 1-9 |
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
120 'xkb:sk::slo', 'xkb:tr::tur', 'xkb:ua::ukr', 'xkb:us::eng', | 108 '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', | 109 'xkb:us:altgr-intl:eng', 'xkb:us:colemak:eng', 'xkb:us:dvorak:eng', |
122 'xkb:us:intl:eng', | 110 'xkb:us:intl:eng', |
123 ) | 111 ) |
124 | 112 |
125 # The path to the root of the repository. | 113 # The path to the root of the repository. |
126 REPOSITORY_ROOT = os.path.join(path_utils.ScriptDir(), '..', '..', '..') | 114 REPOSITORY_ROOT = os.path.join(path_utils.ScriptDir(), '..', '..', '..') |
127 | 115 |
128 number_of_files_total = 0 | 116 number_of_files_total = 0 |
129 | 117 |
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 | 118 |
136 def AddComputedActions(actions): | 119 def AddComputedActions(actions): |
137 """Add computed actions to the actions list. | 120 """Add computed actions to the actions list. |
138 | 121 |
139 Arguments: | 122 Arguments: |
140 actions: set of actions to add to. | 123 actions: set of actions to add to. |
141 """ | 124 """ |
142 | 125 |
143 # Actions for back_forward_menu_model.cc. | 126 # Actions for back_forward_menu_model.cc. |
144 for dir in ('BackMenu_', 'ForwardMenu_'): | 127 for dir in ('BackMenu_', 'ForwardMenu_'): |
(...skipping 443 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
588 actions.add('AutomaticReset_WebUIBanner_BannerShown') | 571 actions.add('AutomaticReset_WebUIBanner_BannerShown') |
589 actions.add('AutomaticReset_WebUIBanner_ManuallyClosed') | 572 actions.add('AutomaticReset_WebUIBanner_ManuallyClosed') |
590 actions.add('AutomaticReset_WebUIBanner_ResetClicked') | 573 actions.add('AutomaticReset_WebUIBanner_ResetClicked') |
591 | 574 |
592 # These actions relate to the the automatic settings reset banner shown as | 575 # These actions relate to the the automatic settings reset banner shown as |
593 # a result of settings hardening. | 576 # a result of settings hardening. |
594 actions.add('AutomaticSettingsReset_WebUIBanner_BannerShown') | 577 actions.add('AutomaticSettingsReset_WebUIBanner_BannerShown') |
595 actions.add('AutomaticSettingsReset_WebUIBanner_ManuallyClosed') | 578 actions.add('AutomaticSettingsReset_WebUIBanner_ManuallyClosed') |
596 actions.add('AutomaticSettingsReset_WebUIBanner_LearnMoreClicked') | 579 actions.add('AutomaticSettingsReset_WebUIBanner_LearnMoreClicked') |
597 | 580 |
| 581 def main(argv): |
| 582 if '--hash' in argv: |
| 583 hash_output = True |
| 584 else: |
| 585 hash_output = False |
| 586 print >>sys.stderr, "WARNING: If you added new UMA tags, you must" + \ |
| 587 " use the --hash option to update chromeactions.txt." |
| 588 # if we do a hash output, we want to only append NEW actions, and we know |
| 589 # the file we want to work on |
| 590 actions = set() |
598 | 591 |
599 class Error(Exception): | 592 chromeactions_path = os.path.join(path_utils.ScriptDir(), "chromeactions.txt") |
600 pass | |
601 | 593 |
| 594 if hash_output: |
| 595 f = open(chromeactions_path) |
| 596 for line in f: |
| 597 part = line.rpartition("\t") |
| 598 part = part[2].strip() |
| 599 actions.add(part) |
| 600 f.close() |
602 | 601 |
603 def _ExtractText(parent_dom, tag_name): | |
604 """Extract the text enclosed by |tag_name| under |parent_dom| | |
605 | |
606 Args: | |
607 parent_dom: The parent Element under which text node is searched for. | |
608 tag_name: The name of the tag which contains a text node. | |
609 | |
610 Returns: | |
611 A (list of) string enclosed by |tag_name| under |parent_dom|. | |
612 """ | |
613 texts = [] | |
614 for child_dom in parent_dom.getElementsByTagName(tag_name): | |
615 text_dom = child_dom.childNodes | |
616 if text_dom.length != 1: | |
617 raise Error('More than 1 child node exists under %s' % tag_name) | |
618 if text_dom[0].nodeType != minidom.Node.TEXT_NODE: | |
619 raise Error('%s\'s child node is not a text node.' % tag_name) | |
620 texts.append(text_dom[0].data) | |
621 return texts | |
622 | |
623 | |
624 class Action(object): | |
625 def __init__(self, name, description, owners, obsolete=None): | |
626 self.name = name | |
627 self.description = description | |
628 self.owners = owners | |
629 self.obsolete = obsolete | |
630 | |
631 | |
632 def ParseActionFile(file_content): | |
633 """Parse the XML data currently stored in the file. | |
634 | |
635 Args: | |
636 file_content: a string containing the action XML file content. | |
637 | |
638 Returns: | |
639 (actions, actions_dict) actions is a set with all user actions' names. | |
640 actions_dict is a dict from user action name to Action object. | |
641 """ | |
642 dom = minidom.parseString(file_content) | |
643 | |
644 comment_nodes = [] | |
645 # Get top-level comments. It is assumed that all comments are placed before | |
646 # <acionts> tag. Therefore the loop will stop if it encounters a non-comment | |
647 # node. | |
648 for node in dom.childNodes: | |
649 if node.nodeType == minidom.Node.COMMENT_NODE: | |
650 comment_nodes.append(node) | |
651 else: | |
652 break | |
653 | |
654 actions = set() | |
655 actions_dict = {} | |
656 # Get each user action data. | |
657 for action_dom in dom.getElementsByTagName('action'): | |
658 action_name = action_dom.getAttribute('name') | |
659 actions.add(action_name) | |
660 | |
661 owners = _ExtractText(action_dom, 'owner') | |
662 # There is only one description for each user action. Get the first element | |
663 # of the returned list. | |
664 description_list = _ExtractText(action_dom, 'description') | |
665 if len(description_list) > 1: | |
666 logging.error('user actions "%s" has more than one descriptions. Exactly ' | |
667 'one description is needed for each user action. Please ' | |
668 'fix.', action_name) | |
669 sys.exit(1) | |
670 description = description_list[0] if description_list else None | |
671 # There is at most one obsolete tag for each user action. | |
672 obsolete_list = _ExtractText(action_dom, 'obsolete') | |
673 if len(obsolete_list) > 1: | |
674 logging.error('user actions "%s" has more than one obsolete tag. At most ' | |
675 'one obsolete tag can be added for each user action. Please' | |
676 ' fix.', action_name) | |
677 sys.exit(1) | |
678 obsolete = obsolete_list[0] if obsolete_list else None | |
679 actions_dict[action_name] = Action(action_name, description, owners, | |
680 obsolete) | |
681 return actions, actions_dict, comment_nodes | |
682 | |
683 | |
684 def _CreateActionTag(doc, action_name, action_object): | |
685 """Create a new action tag. | |
686 | |
687 Format of an action tag: | |
688 <action name="name"> | |
689 <owner>Owner</owner> | |
690 <description>Description.</description> | |
691 <obsolete>Deprecated.</obsolete> | |
692 </action> | |
693 | |
694 <obsolete> is an optional tag. It's added to user actions that are no longer | |
695 used any more. | |
696 | |
697 If action_name is in actions_dict, the values to be inserted are based on the | |
698 corresponding Action object. If action_name is not in actions_dict, the | |
699 default value from TAGS is used. | |
700 | |
701 Args: | |
702 doc: The document under which the new action tag is created. | |
703 action_name: The name of an action. | |
704 action_object: An action object representing the data to be inserted. | |
705 | |
706 Returns: | |
707 An action tag Element with proper children elements. | |
708 """ | |
709 action_dom = doc.createElement('action') | |
710 action_dom.setAttribute('name', action_name) | |
711 | |
712 # Create owner tag. | |
713 if action_object and action_object.owners: | |
714 # If owners for this action is not None, use the stored value. Otherwise, | |
715 # use the default value. | |
716 for owner in action_object.owners: | |
717 owner_dom = doc.createElement('owner') | |
718 owner_dom.appendChild(doc.createTextNode(owner)) | |
719 action_dom.appendChild(owner_dom) | |
720 else: | |
721 # Use default value. | |
722 owner_dom = doc.createElement('owner') | |
723 owner_dom.appendChild(doc.createTextNode(TAGS.get('owner', ''))) | |
724 action_dom.appendChild(owner_dom) | |
725 | |
726 # Create description tag. | |
727 description_dom = doc.createElement('description') | |
728 action_dom.appendChild(description_dom) | |
729 if action_object and action_object.description: | |
730 # If description for this action is not None, use the store value. | |
731 # Otherwise, use the default value. | |
732 description_dom.appendChild(doc.createTextNode( | |
733 action_object.description)) | |
734 else: | |
735 description_dom.appendChild(doc.createTextNode( | |
736 TAGS.get('description', ''))) | |
737 | |
738 # Create obsolete tag. | |
739 if action_object and action_object.obsolete: | |
740 obsolete_dom = doc.createElement('obsolete') | |
741 action_dom.appendChild(obsolete_dom) | |
742 obsolete_dom.appendChild(doc.createTextNode( | |
743 action_object.obsolete)) | |
744 | |
745 return action_dom | |
746 | |
747 | |
748 def PrettyPrint(actions, actions_dict, comment_nodes=[]): | |
749 """Given a list of action data, create a well-printed minidom document. | |
750 | |
751 Args: | |
752 actions: A list of action names. | |
753 actions_dict: A mappting from action name to Action object. | |
754 | |
755 Returns: | |
756 A well-printed minidom document that represents the input action data. | |
757 """ | |
758 doc = minidom.Document() | |
759 | |
760 # Attach top-level comments. | |
761 for node in comment_nodes: | |
762 doc.appendChild(node) | |
763 | |
764 actions_element = doc.createElement('actions') | |
765 doc.appendChild(actions_element) | |
766 | |
767 # Attach action node based on updated |actions|. | |
768 for action in sorted(actions): | |
769 actions_element.appendChild( | |
770 _CreateActionTag(doc, action, actions_dict.get(action, None))) | |
771 | |
772 return print_style.GetPrintStyle().PrettyPrintNode(doc) | |
773 | |
774 | |
775 def main(argv): | |
776 presubmit = ('--presubmit' in argv) | |
777 actions_xml_path = os.path.join(path_utils.ScriptDir(), 'actions.xml') | |
778 | |
779 # Save the original file content. | |
780 with open(actions_xml_path, 'rb') as f: | |
781 original_xml = f.read() | |
782 | |
783 actions, actions_dict, comment_nodes = ParseActionFile(original_xml) | |
784 | 602 |
785 AddComputedActions(actions) | 603 AddComputedActions(actions) |
786 # TODO(fmantek): bring back webkit editor actions. | 604 # TODO(fmantek): bring back webkit editor actions. |
787 # AddWebKitEditorActions(actions) | 605 # AddWebKitEditorActions(actions) |
788 AddAboutFlagsActions(actions) | 606 AddAboutFlagsActions(actions) |
789 AddWebUIActions(actions) | 607 AddWebUIActions(actions) |
790 | 608 |
791 AddLiteralActions(actions) | 609 AddLiteralActions(actions) |
792 | 610 |
793 # print "Scanned {0} number of files".format(number_of_files_total) | 611 # print "Scanned {0} number of files".format(number_of_files_total) |
794 # print "Found {0} entries".format(len(actions)) | 612 # print "Found {0} entries".format(len(actions)) |
795 | 613 |
796 AddAndroidActions(actions) | 614 AddAndroidActions(actions) |
797 AddAutomaticResetBannerActions(actions) | 615 AddAutomaticResetBannerActions(actions) |
798 AddBookmarkManagerActions(actions) | 616 AddBookmarkManagerActions(actions) |
799 AddChromeOSActions(actions) | 617 AddChromeOSActions(actions) |
800 AddClosedSourceActions(actions) | 618 AddClosedSourceActions(actions) |
801 AddExtensionActions(actions) | 619 AddExtensionActions(actions) |
802 AddHistoryPageActions(actions) | 620 AddHistoryPageActions(actions) |
803 AddKeySystemSupportActions(actions) | 621 AddKeySystemSupportActions(actions) |
804 | 622 |
805 pretty = PrettyPrint(actions, actions_dict, comment_nodes) | 623 if hash_output: |
806 if original_xml == pretty: | 624 f = open(chromeactions_path, "wb") |
807 print 'actions.xml is correctly pretty-printed.' | |
808 sys.exit(0) | |
809 if presubmit: | |
810 logging.info('actions.xml is not formatted correctly; run ' | |
811 'extract_actions.py to fix.') | |
812 sys.exit(1) | |
813 | 625 |
814 # Prompt user to consent on the change. | |
815 if not diff_util.PromptUserToAcceptDiff( | |
816 original_xml, pretty, 'Is the new version acceptable?'): | |
817 logging.error('Aborting') | |
818 sys.exit(1) | |
819 | 626 |
820 print 'Creating backup file: actions.old.xml.' | 627 # Print out the actions as a sorted list. |
821 shutil.move(actions_xml_path, 'actions.old.xml') | 628 for action in sorted(actions): |
| 629 if hash_output: |
| 630 hash = hashlib.md5() |
| 631 hash.update(action) |
| 632 print >>f, '0x%s\t%s' % (hash.hexdigest()[:16], action) |
| 633 else: |
| 634 print action |
822 | 635 |
823 with open(actions_xml_path, 'wb') as f: | 636 if hash_output: |
824 f.write(pretty) | 637 print "Done. Do not forget to add chromeactions.txt to your changelist" |
825 print ('Updated %s. Don\'t forget to add it to your changelist' % | |
826 actions_xml_path) | |
827 return 0 | 638 return 0 |
828 | 639 |
829 | 640 |
830 if '__main__' == __name__: | 641 if '__main__' == __name__: |
831 sys.exit(main(sys.argv)) | 642 sys.exit(main(sys.argv)) |
OLD | NEW |