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 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 |