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

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: Fix function name. Created 6 years, 10 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 this user action.',
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
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 zero or more than one descriptions. '
657 'Exactly one description is needed for each user action. '
658 'Please fix it.', action_name)
659 sys.exit(1)
660 description = description_list[0]
661 # There are at most one obsolete tag for each user action.
Ilya Sherman 2014/02/26 01:32:18 nit: "There are ... one" -> "There is ... one".
yao 2014/02/27 14:45:21 Done.
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 it.', 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 long
Ilya Sherman 2014/02/26 01:32:18 nit: "no long" -> "no longer"
yao 2014/02/27 14:45:21 Done.
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))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698