Chromium Code Reviews| 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, | |
| 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')} | |
| 133 | |
| 134 # Doc for actions.xml | |
| 135 _DOC = 'User action XML' | |
| 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 432 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 560 """Add actions that are used for the automatic profile settings reset banner | 601 """Add actions that are used for the automatic profile settings reset banner |
| 561 in chrome://settings. | 602 in chrome://settings. |
| 562 | 603 |
| 563 Arguments | 604 Arguments |
| 564 actions: set of actions to add to. | 605 actions: set of actions to add to. |
| 565 """ | 606 """ |
| 566 actions.add('AutomaticReset_WebUIBanner_BannerShown') | 607 actions.add('AutomaticReset_WebUIBanner_BannerShown') |
| 567 actions.add('AutomaticReset_WebUIBanner_ManuallyClosed') | 608 actions.add('AutomaticReset_WebUIBanner_ManuallyClosed') |
| 568 actions.add('AutomaticReset_WebUIBanner_ResetClicked') | 609 actions.add('AutomaticReset_WebUIBanner_ResetClicked') |
| 569 | 610 |
| 570 def main(argv): | 611 |
| 571 if '--hash' in argv: | 612 class Action(object): |
| 572 hash_output = True | 613 def __init__(self, name, description, owners): |
| 614 self.name = name | |
| 615 self.description = description | |
| 616 self.owners = owners | |
| 617 | |
| 618 | |
| 619 class Error(Exception): | |
| 620 pass | |
| 621 | |
| 622 | |
| 623 def _ExtractText(parent_dom, tag_name, as_list=True): | |
| 624 """Extract the text enclosed by |tag_name| under |parent_dom| | |
| 625 | |
| 626 Args: | |
| 627 parent_dom: The parent Element under which text node is searched for. | |
| 628 tag_name: The name of the tag which contains a text node. | |
| 629 as_list: If set to True, returns a list of string. Otherwise, returns a | |
| 630 single string. | |
| 631 | |
| 632 Returns: | |
| 633 A (list of) string enclosed by |tag_name| under |parent_dom|. | |
| 634 """ | |
| 635 texts = [] | |
| 636 for child_dom in parent_dom.getElementsByTagName(tag_name): | |
| 637 text_dom = child_dom.childNodes | |
| 638 if text_dom.length != 1: | |
| 639 raise Error('More than 1 child node exists under %s' % tag_name) | |
| 640 if text_dom[0].nodeType != minidom.Node.TEXT_NODE: | |
| 641 raise Error('%s\'s child node is not a text node.' % tag_name) | |
| 642 texts.append(text_dom[0].data) | |
| 643 if not as_list: | |
| 644 if texts: | |
| 645 return texts[0] | |
| 646 else: | |
| 647 return None | |
| 648 return texts | |
| 649 | |
| 650 | |
| 651 def _CreateActionTag(top_dom, action_name, action_object): | |
| 652 """Create a new action tag. | |
| 653 | |
| 654 Format of an action tag: | |
| 655 <action name="name"> | |
| 656 <owners> | |
| 657 <owner>Owner </owner> | |
| 658 </owners> | |
| 659 <description> | |
| 660 Description. | |
| 661 </description> | |
| 662 </action> | |
| 663 | |
| 664 If action_name is in actions_dict, the values to be inserted is based on the | |
| 665 corresponding Action object. If action_name is not in actions_dict, the | |
| 666 default value from TAGS is used. | |
| 667 | |
| 668 Args: | |
| 669 top_dom: The parent node under which the new action tag is created. | |
| 670 action_name: The name of an action. | |
| 671 actions_dict: A map from action name to Action object. | |
| 672 | |
| 673 Returns: | |
| 674 An action tag Element with proper children elements. | |
| 675 """ | |
| 676 action_dom = top_dom.createElement('action') | |
| 677 action_dom.setAttribute('name', action_name) | |
| 678 owners_dom = top_dom.createElement('owners') | |
| 679 action_dom.appendChild(owners_dom) | |
| 680 description_dom = top_dom.createElement('description') | |
| 681 action_dom.appendChild(description_dom) | |
| 682 | |
| 683 # Create description tag. | |
| 684 if action_object and action_object.description: | |
| 685 # If description for this action is not None, use the store value. | |
| 686 # Otherwise, use the default value. | |
| 687 description_dom.appendChild(top_dom.createTextNode( | |
| 688 action_object.description)) | |
| 573 else: | 689 else: |
| 574 hash_output = False | 690 description_dom.appendChild(top_dom.createTextNode( |
| 575 print >>sys.stderr, "WARNING: If you added new UMA tags, you must" + \ | 691 TAGS.get('description', ''))) |
| 576 " use the --hash option to update chromeactions.txt." | 692 |
| 577 # if we do a hash output, we want to only append NEW actions, and we know | 693 # Create owner tag. |
| 578 # the file we want to work on | 694 if action_object and action_object.owners: |
| 695 # If owners for this action is not None, use the stored value. Otherwise, | |
| 696 # use the default value. | |
| 697 for owner in action_object.owners: | |
| 698 owner_dom = top_dom.createElement('owner') | |
| 699 owner_dom.appendChild(top_dom.createTextNode(owner)) | |
| 700 owners_dom.appendChild(owner_dom) | |
| 701 else: | |
| 702 # Use default value. | |
| 703 owner_dom = top_dom.createElement('owner') | |
| 704 owner_dom.appendChild(top_dom.createTextNode(TAGS.get('owner', ''))) | |
| 705 owners_dom.appendChild(owner_dom) | |
| 706 return action_dom | |
| 707 | |
| 708 | |
| 709 def main(): | |
| 710 # A set to store all actions. | |
| 579 actions = set() | 711 actions = set() |
| 712 # A map from action name to an Action object. | |
| 713 actions_dict = {} | |
| 580 | 714 |
| 581 chromeactions_path = os.path.join(path_utils.ScriptDir(), "chromeactions.txt") | 715 actions_xml_path = os.path.join(path_utils.ScriptDir(), 'actions.xml') |
| 582 | 716 |
| 583 if hash_output: | 717 # Save the original file content. |
| 584 f = open(chromeactions_path) | 718 with open(actions_xml_path, 'rb') as f: |
| 585 for line in f: | 719 original_xml = f.read() |
| 586 part = line.rpartition("\t") | |
| 587 part = part[2].strip() | |
| 588 actions.add(part) | |
| 589 f.close() | |
| 590 | 720 |
| 721 # Parse and store the XML data currently stored in file. | |
| 722 dom = minidom.parse(actions_xml_path) | |
| 723 for action_dom in dom.getElementsByTagName('action'): | |
| 724 action_name = action_dom.getAttribute('name') | |
| 725 actions.add(action_name) | |
| 726 description = _ExtractText(action_dom, 'description', False) | |
| 727 owners = _ExtractText(action_dom, 'owner', True) | |
| 728 actions_dict[action_name] = Action(action_name, description, owners) | |
| 591 | 729 |
| 592 AddComputedActions(actions) | 730 AddComputedActions(actions) |
| 593 # TODO(fmantek): bring back webkit editor actions. | 731 # TODO(fmantek): bring back webkit editor actions. |
| 594 # AddWebKitEditorActions(actions) | 732 # AddWebKitEditorActions(actions) |
| 595 AddAboutFlagsActions(actions) | 733 AddAboutFlagsActions(actions) |
| 596 AddWebUIActions(actions) | 734 AddWebUIActions(actions) |
| 597 | 735 |
| 598 AddLiteralActions(actions) | 736 AddLiteralActions(actions) |
| 599 | 737 |
| 600 # print "Scanned {0} number of files".format(number_of_files_total) | 738 # print "Scanned {0} number of files".format(number_of_files_total) |
| 601 # print "Found {0} entries".format(len(actions)) | 739 # print "Found {0} entries".format(len(actions)) |
| 602 | 740 |
| 603 AddAndroidActions(actions) | 741 AddAndroidActions(actions) |
| 604 AddAutomaticResetBannerActions(actions) | 742 AddAutomaticResetBannerActions(actions) |
| 605 AddBookmarkManagerActions(actions) | 743 AddBookmarkManagerActions(actions) |
| 606 AddChromeOSActions(actions) | 744 AddChromeOSActions(actions) |
| 607 AddClosedSourceActions(actions) | 745 AddClosedSourceActions(actions) |
| 608 AddExtensionActions(actions) | 746 AddExtensionActions(actions) |
| 609 AddHistoryPageActions(actions) | 747 AddHistoryPageActions(actions) |
| 610 AddKeySystemSupportActions(actions) | 748 AddKeySystemSupportActions(actions) |
| 611 | 749 |
| 612 if hash_output: | 750 # Form new minidom nodes based on updated |actions|. |
| 613 f = open(chromeactions_path, "wb") | 751 new_dom = minidom.getDOMImplementation().createDocument(None, 'actions', None) |
| 614 | 752 top_element = new_dom.documentElement |
| 753 top_element.appendChild(new_dom.createComment(_DOC)) | |
| 615 | 754 |
| 616 # Print out the actions as a sorted list. | 755 # Print out the actions as a sorted list. |
| 617 for action in sorted(actions): | 756 for action in sorted(actions): |
| 618 if hash_output: | 757 top_element.appendChild(_CreateActionTag(new_dom, action, |
| 619 hash = hashlib.md5() | 758 actions_dict.get(action, None))) |
| 620 hash.update(action) | |
| 621 print >>f, '0x%s\t%s' % (hash.hexdigest()[:16], action) | |
| 622 else: | |
| 623 print action | |
| 624 | 759 |
| 625 if hash_output: | 760 # Pretty print the generated minidom node |new_dom|. |
| 626 print "Done. Do not forget to add chromeactions.txt to your changelist" | 761 xml_style = pretty_print_xml.XmlStyle(ATTRIBUTE_ORDER, |
| 762 TAGS_THAT_HAVE_EXTRA_NEWLINE, | |
| 763 TAGS_THAT_DONT_INDENT, | |
| 764 TAGS_THAT_ALLOW_SINGLE_LINE) | |
| 765 pretty = xml_style.PrettyPrintNode(new_dom) | |
|
Alexei Svitkine (slow)
2014/02/11 19:05:31
Can you extract the code from line 750 up to this
yao
2014/02/13 15:58:08
Done.
| |
| 766 | |
| 767 if original_xml == pretty: | |
| 768 print 'actions.xml is correctly pretty-printed.' | |
| 769 sys.exit(0) | |
| 770 | |
| 771 # Prompt user to consent on the change. | |
| 772 if not diff_util.PromptUserToAcceptDiff( | |
| 773 original_xml, pretty, 'Is the new version acceptable?'): | |
| 774 logging.error('Aborting') | |
| 775 sys.exit(0) | |
| 776 | |
| 777 print 'Creating backup file: actions.before.xml.' | |
| 778 shutil.move(actions_xml_path, 'actions.before.xml') | |
| 779 | |
| 780 with open(actions_xml_path, 'wb') as f: | |
| 781 f.write(pretty) | |
| 782 print ('Updated %s. Do not forget to add it to your changelist' % | |
| 783 actions_xml_path) | |
| 627 return 0 | 784 return 0 |
| 628 | 785 |
| 629 | 786 |
| 630 if '__main__' == __name__: | 787 if '__main__' == __name__: |
| 631 sys.exit(main(sys.argv)) | 788 sys.exit(main()) |
| OLD | NEW |