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 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, | |
Ilya Sherman
2014/02/22 07:07:37
nit: "needs be" -> "needs to be"
yao
2014/02/24 19:16:23
Done.
| |
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 diff_util | |
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 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
108 'xkb:sk::slo', 'xkb:tr::tur', 'xkb:ua::ukr', 'xkb:us::eng', | 119 'xkb:sk::slo', 'xkb:tr::tur', 'xkb:ua::ukr', 'xkb:us::eng', |
109 'xkb:us:altgr-intl:eng', 'xkb:us:colemak:eng', 'xkb:us:dvorak:eng', | 120 'xkb:us:altgr-intl:eng', 'xkb:us:colemak:eng', 'xkb:us:dvorak:eng', |
110 'xkb:us:intl:eng', | 121 'xkb:us:intl:eng', |
111 ) | 122 ) |
112 | 123 |
113 # The path to the root of the repository. | 124 # The path to the root of the repository. |
114 REPOSITORY_ROOT = os.path.join(path_utils.ScriptDir(), '..', '..', '..') | 125 REPOSITORY_ROOT = os.path.join(path_utils.ScriptDir(), '..', '..', '..') |
115 | 126 |
116 number_of_files_total = 0 | 127 number_of_files_total = 0 |
117 | 128 |
129 # Tags that need to be inserted to each 'action' tag and their default content. | |
130 TAGS = {'description': 'Please enter the description of this user action.', | |
131 'owner': ('Please specify the owners of this user action. ' + | |
132 'Add more owner tags as needed')} | |
Ilya Sherman
2014/02/22 07:07:37
nit: Please end the sentence with a period.
yao
2014/02/24 19:16:23
Done.
| |
133 | |
134 # Doc for actions.xml | |
135 _DOC = 'User action XML' | |
Ilya Sherman
2014/02/22 07:07:37
nit: Why the leading underscore?
yao
2014/02/24 19:16:23
Done.
| |
136 | |
137 # Desired order for tag and tag attributes. | |
138 # { tag_name: [attribute_name, ...] } | |
139 ATTRIBUTE_ORDER = { | |
140 'action': ['name'], | |
141 'owners': [], | |
142 'owner':[], | |
143 'description': [], | |
144 } | |
145 | |
146 # Tag names for top-level nodes whose children we don't want to indent. | |
147 TAGS_THAT_DONT_INDENT = ['actions'] | |
148 | |
149 # Extra vertical spacing rules for special tag names. | |
150 # {tag_name: (newlines_after_open, newlines_before_close, newlines_after_close)} | |
151 TAGS_THAT_HAVE_EXTRA_NEWLINE = { | |
152 'actions': (2, 1, 1), | |
153 'action': (1, 1, 1), | |
154 } | |
155 | |
156 # Tags that we allow to be squished into a single line for brevity. | |
157 TAGS_THAT_ALLOW_SINGLE_LINE = ['owner'] | |
158 | |
118 | 159 |
119 def AddComputedActions(actions): | 160 def AddComputedActions(actions): |
120 """Add computed actions to the actions list. | 161 """Add computed actions to the actions list. |
121 | 162 |
122 Arguments: | 163 Arguments: |
123 actions: set of actions to add to. | 164 actions: set of actions to add to. |
124 """ | 165 """ |
125 | 166 |
126 # Actions for back_forward_menu_model.cc. | 167 # Actions for back_forward_menu_model.cc. |
127 for dir in ('BackMenu_', 'ForwardMenu_'): | 168 for dir in ('BackMenu_', 'ForwardMenu_'): |
(...skipping 443 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
571 actions.add('AutomaticReset_WebUIBanner_BannerShown') | 612 actions.add('AutomaticReset_WebUIBanner_BannerShown') |
572 actions.add('AutomaticReset_WebUIBanner_ManuallyClosed') | 613 actions.add('AutomaticReset_WebUIBanner_ManuallyClosed') |
573 actions.add('AutomaticReset_WebUIBanner_ResetClicked') | 614 actions.add('AutomaticReset_WebUIBanner_ResetClicked') |
574 | 615 |
575 # These actions relate to the the automatic settings reset banner shown as | 616 # These actions relate to the the automatic settings reset banner shown as |
576 # a result of settings hardening. | 617 # a result of settings hardening. |
577 actions.add('AutomaticSettingsReset_WebUIBanner_BannerShown') | 618 actions.add('AutomaticSettingsReset_WebUIBanner_BannerShown') |
578 actions.add('AutomaticSettingsReset_WebUIBanner_ManuallyClosed') | 619 actions.add('AutomaticSettingsReset_WebUIBanner_ManuallyClosed') |
579 actions.add('AutomaticSettingsReset_WebUIBanner_LearnMoreClicked') | 620 actions.add('AutomaticSettingsReset_WebUIBanner_LearnMoreClicked') |
580 | 621 |
622 | |
623 class Action(object): | |
Alexei Svitkine (slow)
2014/02/20 22:50:55
Can you move this closer to where it's used?
yao
2014/02/24 19:16:23
Done.
| |
624 def __init__(self, name, description, owners): | |
625 self.name = name | |
626 self.description = description | |
627 self.owners = owners | |
628 | |
629 | |
630 class Error(Exception): | |
631 pass | |
632 | |
633 | |
634 def _ExtractText(parent_dom, tag_name, as_list=True): | |
Ilya Sherman
2014/02/22 07:07:37
nit: Why the underscores before the method names?
Ilya Sherman
2014/02/22 07:07:37
Having as_list here seems pretty hacky. Why not j
yao
2014/02/24 19:16:23
In google3, internal/private function names should
yao
2014/02/24 19:16:23
Done.
| |
635 """Extract the text enclosed by |tag_name| under |parent_dom| | |
636 | |
637 Args: | |
638 parent_dom: The parent Element under which text node is searched for. | |
639 tag_name: The name of the tag which contains a text node. | |
640 as_list: If set to True, returns a list of string. Otherwise, returns a | |
Alexei Svitkine (slow)
2014/02/20 22:50:55
nit: "a list of string" -> " a list of strings"
yao
2014/02/24 19:16:23
Done.
| |
641 single string. | |
642 | |
643 Returns: | |
644 A (list of) string enclosed by |tag_name| under |parent_dom|. | |
645 """ | |
646 texts = [] | |
647 for child_dom in parent_dom.getElementsByTagName(tag_name): | |
648 text_dom = child_dom.childNodes | |
649 if text_dom.length != 1: | |
650 raise Error('More than 1 child node exists under %s' % tag_name) | |
651 if text_dom[0].nodeType != minidom.Node.TEXT_NODE: | |
652 raise Error('%s\'s child node is not a text node.' % tag_name) | |
653 texts.append(text_dom[0].data) | |
654 if not as_list: | |
655 if texts: | |
656 return texts[0] | |
Ilya Sherman
2014/02/22 07:07:37
Should it be an error for tests to have a size not
yao
2014/02/24 19:16:23
Done.
| |
657 else: | |
658 return None | |
659 return texts | |
660 | |
661 | |
662 def _CreateActionTag(top_dom, action_name, action_object): | |
663 """Create a new action tag. | |
664 | |
665 Format of an action tag: | |
666 <action name="name"> | |
667 <owners> | |
668 <owner>Owner </owner> | |
Ilya Sherman
2014/02/22 07:07:37
nit: Spurious space after "Owner"?
yao
2014/02/24 19:16:23
Done.
| |
669 </owners> | |
670 <description> | |
671 Description. | |
672 </description> | |
673 </action> | |
674 | |
675 If action_name is in actions_dict, the values to be inserted is based on the | |
Ilya Sherman
2014/02/22 07:07:37
nit: "values ... is" -> "values ... are"
yao
2014/02/24 19:16:23
Done.
| |
676 corresponding Action object. If action_name is not in actions_dict, the | |
677 default value from TAGS is used. | |
678 | |
679 Args: | |
680 top_dom: The parent node under which the new action tag is created. | |
681 action_name: The name of an action. | |
682 actions_dict: A map from action name to Action object. | |
683 | |
684 Returns: | |
685 An action tag Element with proper children elements. | |
686 """ | |
687 action_dom = top_dom.createElement('action') | |
688 action_dom.setAttribute('name', action_name) | |
689 owners_dom = top_dom.createElement('owners') | |
690 action_dom.appendChild(owners_dom) | |
691 description_dom = top_dom.createElement('description') | |
692 action_dom.appendChild(description_dom) | |
693 | |
694 # Create description tag. | |
695 if action_object and action_object.description: | |
696 # If description for this action is not None, use the store value. | |
697 # Otherwise, use the default value. | |
698 description_dom.appendChild(top_dom.createTextNode( | |
699 action_object.description)) | |
700 else: | |
701 description_dom.appendChild(top_dom.createTextNode( | |
702 TAGS.get('description', ''))) | |
703 | |
704 # Create owner tag. | |
705 if action_object and action_object.owners: | |
706 # If owners for this action is not None, use the stored value. Otherwise, | |
707 # use the default value. | |
708 for owner in action_object.owners: | |
709 owner_dom = top_dom.createElement('owner') | |
710 owner_dom.appendChild(top_dom.createTextNode(owner)) | |
711 owners_dom.appendChild(owner_dom) | |
712 else: | |
713 # Use default value. | |
714 owner_dom = top_dom.createElement('owner') | |
715 owner_dom.appendChild(top_dom.createTextNode(TAGS.get('owner', ''))) | |
716 owners_dom.appendChild(owner_dom) | |
717 return action_dom | |
718 | |
719 | |
720 def _PrettyPrint(actions, actions_dict): | |
721 """Given a list of action data, create a well-printed minidom document. | |
722 | |
723 Args: | |
724 actions: A list of action names. | |
725 actions_dict: A mappting from action name to Action object. | |
726 | |
727 Returns: | |
728 A well-printed minidom document that represents the input action data. | |
729 """ | |
730 # Form new minidom nodes based on updated |actions|. | |
731 new_dom = minidom.getDOMImplementation().createDocument(None, 'actions', None) | |
732 top_element = new_dom.documentElement | |
733 top_element.appendChild(new_dom.createComment(_DOC)) | |
734 | |
735 # Print out the actions as a sorted list. | |
736 for action in sorted(actions): | |
737 top_element.appendChild(_CreateActionTag(new_dom, action, | |
738 actions_dict.get(action, None))) | |
739 | |
740 # Pretty print the generated minidom node |new_dom|. | |
741 xml_style = pretty_print_xml.XmlStyle(ATTRIBUTE_ORDER, | |
Alexei Svitkine (slow)
2014/02/20 22:50:55
I noticed you moved this to its own print_style fi
yao
2014/02/24 19:16:23
Done.
| |
742 TAGS_THAT_HAVE_EXTRA_NEWLINE, | |
743 TAGS_THAT_DONT_INDENT, | |
744 TAGS_THAT_ALLOW_SINGLE_LINE) | |
745 return xml_style.PrettyPrintNode(new_dom) | |
746 | |
747 | |
581 def main(argv): | 748 def main(argv): |
582 if '--hash' in argv: | 749 presubmit = ('--presubmit' in argv) |
583 hash_output = True | 750 |
584 else: | 751 # A set to store all actions. |
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() | 752 actions = set() |
753 # A map from action name to an Action object. | |
754 actions_dict = {} | |
755 actions_xml_path = os.path.join(path_utils.ScriptDir(), 'actions.xml') | |
591 | 756 |
592 chromeactions_path = os.path.join(path_utils.ScriptDir(), "chromeactions.txt") | 757 # Save the original file content. |
758 with open(actions_xml_path, 'rb') as f: | |
759 original_xml = f.read() | |
593 | 760 |
594 if hash_output: | 761 # Parse and store the XML data currently stored in file. |
Alexei Svitkine (slow)
2014/02/20 22:50:55
Nit: "in file" -> "in the file"
yao
2014/02/24 19:16:23
Done.
| |
595 f = open(chromeactions_path) | 762 dom = minidom.parse(actions_xml_path) |
596 for line in f: | 763 for action_dom in dom.getElementsByTagName('action'): |
597 part = line.rpartition("\t") | 764 action_name = action_dom.getAttribute('name') |
598 part = part[2].strip() | 765 actions.add(action_name) |
599 actions.add(part) | 766 description = _ExtractText(action_dom, 'description', False) |
600 f.close() | 767 owners = _ExtractText(action_dom, 'owner', True) |
601 | 768 actions_dict[action_name] = Action(action_name, description, owners) |
Alexei Svitkine (slow)
2014/02/20 22:50:55
Nit: Actually, I'd also make a function for this b
yao
2014/02/24 19:16:23
Done.
| |
602 | 769 |
603 AddComputedActions(actions) | 770 AddComputedActions(actions) |
604 # TODO(fmantek): bring back webkit editor actions. | 771 # TODO(fmantek): bring back webkit editor actions. |
605 # AddWebKitEditorActions(actions) | 772 # AddWebKitEditorActions(actions) |
606 AddAboutFlagsActions(actions) | 773 AddAboutFlagsActions(actions) |
607 AddWebUIActions(actions) | 774 AddWebUIActions(actions) |
608 | 775 |
609 AddLiteralActions(actions) | 776 AddLiteralActions(actions) |
610 | 777 |
611 # print "Scanned {0} number of files".format(number_of_files_total) | 778 # print "Scanned {0} number of files".format(number_of_files_total) |
612 # print "Found {0} entries".format(len(actions)) | 779 # print "Found {0} entries".format(len(actions)) |
613 | 780 |
614 AddAndroidActions(actions) | 781 AddAndroidActions(actions) |
615 AddAutomaticResetBannerActions(actions) | 782 AddAutomaticResetBannerActions(actions) |
616 AddBookmarkManagerActions(actions) | 783 AddBookmarkManagerActions(actions) |
617 AddChromeOSActions(actions) | 784 AddChromeOSActions(actions) |
618 AddClosedSourceActions(actions) | 785 AddClosedSourceActions(actions) |
619 AddExtensionActions(actions) | 786 AddExtensionActions(actions) |
620 AddHistoryPageActions(actions) | 787 AddHistoryPageActions(actions) |
621 AddKeySystemSupportActions(actions) | 788 AddKeySystemSupportActions(actions) |
622 | 789 |
623 if hash_output: | 790 pretty = _PrettyPrint(actions, actions_dict) |
624 f = open(chromeactions_path, "wb") | 791 if original_xml == pretty: |
792 print 'actions.xml is correctly pretty-printed.' | |
793 sys.exit(0) | |
794 if presubmit: | |
795 logging.info('actions.xml is not formatted correctly; run ' | |
796 'extract_actions.py to fix.') | |
797 sys.exit(1) | |
625 | 798 |
799 # Prompt user to consent on the change. | |
800 if not diff_util.PromptUserToAcceptDiff( | |
801 original_xml, pretty, 'Is the new version acceptable?'): | |
802 logging.error('Aborting') | |
803 sys.exit(0) | |
Ilya Sherman
2014/02/22 07:07:37
This should probably be sys.exit(1), since the scr
yao
2014/02/24 19:16:23
Done.
| |
626 | 804 |
627 # Print out the actions as a sorted list. | 805 print 'Creating backup file: actions.before.xml.' |
628 for action in sorted(actions): | 806 shutil.move(actions_xml_path, 'actions.before.xml') |
Alexei Svitkine (slow)
2014/02/20 22:50:55
I'd name this actions.old.xml.
yao
2014/02/24 19:16:23
Done.
| |
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 | |
635 | 807 |
636 if hash_output: | 808 with open(actions_xml_path, 'wb') as f: |
637 print "Done. Do not forget to add chromeactions.txt to your changelist" | 809 f.write(pretty) |
810 print ('Updated %s. Do not forget to add it to your changelist' % | |
Ilya Sherman
2014/02/22 07:07:37
Optional nit: "Do not" -> "Don't" (reads more natu
yao
2014/02/24 19:16:23
Done.
| |
811 actions_xml_path) | |
638 return 0 | 812 return 0 |
639 | 813 |
640 | 814 |
641 if '__main__' == __name__: | 815 if '__main__' == __name__: |
642 sys.exit(main(sys.argv)) | 816 sys.exit(main(sys.argv)) |
OLD | NEW |