Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(4)

Side by Side Diff: tools/metrics/actions/extract_actions.py

Issue 149503005: Change actions.txt to actions.xml (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Preserve top-level comments & update general presubmit. Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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 443 matching lines...) Expand 10 before | Expand all | Expand 10 after
571 actions.add('AutomaticReset_WebUIBanner_BannerShown') 588 actions.add('AutomaticReset_WebUIBanner_BannerShown')
572 actions.add('AutomaticReset_WebUIBanner_ManuallyClosed') 589 actions.add('AutomaticReset_WebUIBanner_ManuallyClosed')
573 actions.add('AutomaticReset_WebUIBanner_ResetClicked') 590 actions.add('AutomaticReset_WebUIBanner_ResetClicked')
574 591
575 # These actions relate to the the automatic settings reset banner shown as 592 # These actions relate to the the automatic settings reset banner shown as
576 # a result of settings hardening. 593 # a result of settings hardening.
577 actions.add('AutomaticSettingsReset_WebUIBanner_BannerShown') 594 actions.add('AutomaticSettingsReset_WebUIBanner_BannerShown')
578 actions.add('AutomaticSettingsReset_WebUIBanner_ManuallyClosed') 595 actions.add('AutomaticSettingsReset_WebUIBanner_ManuallyClosed')
579 actions.add('AutomaticSettingsReset_WebUIBanner_LearnMoreClicked') 596 actions.add('AutomaticSettingsReset_WebUIBanner_LearnMoreClicked')
580 597
598
599 class Error(Exception):
600 pass
601
602
603 def _ExtractText(parent_dom, tag_name):
604 """Extract the text enclosed by |tag_name| under |parent_dom|
605
606 Args:
607 parent_dom: The parent Element under which text node is searched for.
608 tag_name: The name of the tag which contains a text node.
609
610 Returns:
611 A (list of) string enclosed by |tag_name| under |parent_dom|.
612 """
613 texts = []
614 for child_dom in parent_dom.getElementsByTagName(tag_name):
615 text_dom = child_dom.childNodes
616 if text_dom.length != 1:
617 raise Error('More than 1 child node exists under %s' % tag_name)
618 if text_dom[0].nodeType != minidom.Node.TEXT_NODE:
619 raise Error('%s\'s child node is not a text node.' % tag_name)
620 texts.append(text_dom[0].data)
621 return texts
622
623
624 class Action(object):
625 def __init__(self, name, description, owners, obsolete=None):
626 self.name = name
627 self.description = description
628 self.owners = owners
629 self.obsolete = obsolete
630
631
632 def ParseActionFile(file_path):
633 """Parse the XML data currently stored in the file.
634
635 Args:
636 file_path: the path to the action XML file.
637
638 Returns:
639 (actions, actions_dict) actions is a set with all user actions' names.
640 actions_dict is a dict from user action name to Action object.
641 """
642 dom = minidom.parse(file_path)
643
644 comment_nodes = []
645 # Get top-level comments. It is assumed that all comments are places before
Ilya Sherman 2014/03/03 23:17:34 nit: "places" -> "placed"
yiyaoliu 2014/03/04 15:31:07 Done.
646 # <acionts> tag. Therefore the loop will stop if it encounters a non-comment
647 # node.
648 for node in dom.childNodes:
649 if node.nodeType == minidom.Node.COMMENT_NODE:
650 comment_nodes.append(node)
651 else:
652 break
653
654 actions = set()
655 actions_dict = {}
656 # Get each user action data.
657 for action_dom in dom.getElementsByTagName('action'):
658 action_name = action_dom.getAttribute('name')
659 actions.add(action_name)
660
661 owners = _ExtractText(action_dom, 'owner')
662 # There is only one description for each user action. Get the first element
663 # of the returned list.
664 description_list = _ExtractText(action_dom, 'description')
665 if len(description_list) > 1:
666 logging.error('user actions "%s" has more than one descriptions. Exactly '
667 'one description is needed for each user action. Please '
668 'fix.', action_name)
669 sys.exit(1)
670 description = description_list[0] if description_list else None
671 # There is at most one obsolete tag for each user action.
672 obsolete_list = _ExtractText(action_dom, 'obsolete')
673 if len(obsolete_list) > 1:
674 logging.error('user actions "%s" has more than one obsolete tag. At most '
675 'one obsolete tag can be added for each user action. Please'
676 ' fix.', action_name)
677 sys.exit(1)
678 obsolete = obsolete_list[0] if obsolete_list else None
679 actions_dict[action_name] = Action(action_name, description, owners,
680 obsolete)
681 return actions, actions_dict, comment_nodes
682
683
684 def _CreateActionTag(doc, action_name, action_object):
685 """Create a new action tag.
686
687 Format of an action tag:
688 <action name="name">
689 <owner>Owner</owner>
690 <description>Description.</description>
691 <obsolete>Deprecated.</obsolete>
692 </action>
693
694 <obsolete> is an optional tag. It's added to user actions that are no longer
695 used any more.
696
697 If action_name is in actions_dict, the values to be inserted are based on the
698 corresponding Action object. If action_name is not in actions_dict, the
699 default value from TAGS is used.
700
701 Args:
702 doc: The document under which the new action tag is created.
703 action_name: The name of an action.
704 action_object: An action object representing the data to be inserted.
705
706 Returns:
707 An action tag Element with proper children elements.
708 """
709 action_dom = doc.createElement('action')
710 action_dom.setAttribute('name', action_name)
711
712 # Create owner tag.
713 if action_object and action_object.owners:
714 # If owners for this action is not None, use the stored value. Otherwise,
715 # use the default value.
716 for owner in action_object.owners:
717 owner_dom = doc.createElement('owner')
718 owner_dom.appendChild(doc.createTextNode(owner))
719 action_dom.appendChild(owner_dom)
720 else:
721 # Use default value.
722 owner_dom = doc.createElement('owner')
723 owner_dom.appendChild(doc.createTextNode(TAGS.get('owner', '')))
724 action_dom.appendChild(owner_dom)
725
726 # Create description tag.
727 description_dom = doc.createElement('description')
728 action_dom.appendChild(description_dom)
729 if action_object and action_object.description:
730 # If description for this action is not None, use the store value.
731 # Otherwise, use the default value.
732 description_dom.appendChild(doc.createTextNode(
733 action_object.description))
734 else:
735 description_dom.appendChild(doc.createTextNode(
736 TAGS.get('description', '')))
737
738 # Create obsolete tag.
739 if action_object and action_object.obsolete:
740 obsolete_dom = doc.createElement('obsolete')
741 action_dom.appendChild(obsolete_dom)
742 obsolete_dom.appendChild(doc.createTextNode(
743 action_object.obsolete))
744
745 return action_dom
746
747
748 def PrettyPrint(actions, actions_dict, comment_nodes=[]):
749 """Given a list of action data, create a well-printed minidom document.
750
751 Args:
752 actions: A list of action names.
753 actions_dict: A mappting from action name to Action object.
754
755 Returns:
756 A well-printed minidom document that represents the input action data.
757 """
758 doc = minidom.Document()
759
760 # Attach top-level comments.
761 for node in comment_nodes:
762 doc.appendChild(node)
763
764 actions_element = doc.createElement('actions')
765 doc.appendChild(actions_element)
766
767 # Attach action node based on updated |actions|.
768 for action in sorted(actions):
769 actions_element.appendChild(
770 _CreateActionTag(doc, action, actions_dict.get(action, None)))
771
772 return print_style.GetPrintStyle().PrettyPrintNode(doc)
773
774
581 def main(argv): 775 def main(argv):
582 if '--hash' in argv: 776 presubmit = ('--presubmit' in argv)
583 hash_output = True 777 actions_xml_path = os.path.join(path_utils.ScriptDir(), 'actions.xml')
584 else:
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()
591 778
592 chromeactions_path = os.path.join(path_utils.ScriptDir(), "chromeactions.txt") 779 # Save the original file content.
780 with open(actions_xml_path, 'rb') as f:
781 original_xml = f.read()
593 782
594 if hash_output: 783 actions, actions_dict, comment_nodes = ParseActionFile(actions_xml_path)
595 f = open(chromeactions_path)
596 for line in f:
597 part = line.rpartition("\t")
598 part = part[2].strip()
599 actions.add(part)
600 f.close()
601
602 784
603 AddComputedActions(actions) 785 AddComputedActions(actions)
604 # TODO(fmantek): bring back webkit editor actions. 786 # TODO(fmantek): bring back webkit editor actions.
605 # AddWebKitEditorActions(actions) 787 # AddWebKitEditorActions(actions)
606 AddAboutFlagsActions(actions) 788 AddAboutFlagsActions(actions)
607 AddWebUIActions(actions) 789 AddWebUIActions(actions)
608 790
609 AddLiteralActions(actions) 791 AddLiteralActions(actions)
610 792
611 # print "Scanned {0} number of files".format(number_of_files_total) 793 # print "Scanned {0} number of files".format(number_of_files_total)
612 # print "Found {0} entries".format(len(actions)) 794 # print "Found {0} entries".format(len(actions))
613 795
614 AddAndroidActions(actions) 796 AddAndroidActions(actions)
615 AddAutomaticResetBannerActions(actions) 797 AddAutomaticResetBannerActions(actions)
616 AddBookmarkManagerActions(actions) 798 AddBookmarkManagerActions(actions)
617 AddChromeOSActions(actions) 799 AddChromeOSActions(actions)
618 AddClosedSourceActions(actions) 800 AddClosedSourceActions(actions)
619 AddExtensionActions(actions) 801 AddExtensionActions(actions)
620 AddHistoryPageActions(actions) 802 AddHistoryPageActions(actions)
621 AddKeySystemSupportActions(actions) 803 AddKeySystemSupportActions(actions)
622 804
623 if hash_output: 805 pretty = PrettyPrint(actions, actions_dict, comment_nodes)
624 f = open(chromeactions_path, "wb") 806 if original_xml == pretty:
807 print 'actions.xml is correctly pretty-printed.'
808 sys.exit(0)
809 if presubmit:
810 logging.info('actions.xml is not formatted correctly; run '
811 'extract_actions.py to fix.')
812 sys.exit(1)
625 813
814 # Prompt user to consent on the change.
815 if not diff_util.PromptUserToAcceptDiff(
816 original_xml, pretty, 'Is the new version acceptable?'):
817 logging.error('Aborting')
818 sys.exit(1)
626 819
627 # Print out the actions as a sorted list. 820 print 'Creating backup file: actions.old.xml.'
628 for action in sorted(actions): 821 shutil.move(actions_xml_path, 'actions.old.xml')
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 822
636 if hash_output: 823 with open(actions_xml_path, 'wb') as f:
637 print "Done. Do not forget to add chromeactions.txt to your changelist" 824 f.write(pretty)
825 print ('Updated %s. Don\'t forget to add it to your changelist' %
826 actions_xml_path)
638 return 0 827 return 0
639 828
640 829
641 if '__main__' == __name__: 830 if '__main__' == __name__:
642 sys.exit(main(sys.argv)) 831 sys.exit(main(sys.argv))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698