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