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 |
| 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) |
| 633 raise Error() |
| 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 |