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

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: Address code review comments. 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 this user action.',
132 'owner': ('Please specify the owners of this user action. ' +
133 'Add more owner tags as 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 _ParseActionFile(file_path):
604 """Parse the XML data currently stored in the file.
605
606 Args:
607 file_path: the path to the action XML file.
608
609 Returns:
610 (actions, actions_dict) actions is a set with all user actions' names.
611 actions_dict is a dict from user action name to Action object.
612 """
613 actions = set()
614 actions_dict = {}
615
616 dom = minidom.parse(file_path)
617 for action_dom in dom.getElementsByTagName('action'):
618 action_name = action_dom.getAttribute('name')
619 actions.add(action_name)
620 # There is only one description for each user action. Get the first element
621 # of the returned list.
622 description_list = _ExtractText(action_dom, 'description')
623 if len(description_list) != 1:
624 logging.error('user actions "%s" has zero or more than one descriptions. '
625 'Exactly one description is needed for each user action. '
626 'Please fix it.', action_name)
627 sys.exit(1)
628 description = description_list[0]
629 owners = _ExtractText(action_dom, 'owner')
630 actions_dict[action_name] = Action(action_name, description, owners)
631
632 return actions, actions_dict
633
634
635 def _ExtractText(parent_dom, tag_name):
Ilya Sherman 2014/02/24 20:38:41 nit: It's atypical for this to be declared below t
yao 2014/02/25 20:08:11 Done.
636 """Extract the text enclosed by |tag_name| under |parent_dom|
637
638 Args:
639 parent_dom: The parent Element under which text node is searched for.
640 tag_name: The name of the tag which contains a text node.
641
642 Returns:
643 A (list of) string enclosed by |tag_name| under |parent_dom|.
644 """
645 texts = []
646 for child_dom in parent_dom.getElementsByTagName(tag_name):
647 text_dom = child_dom.childNodes
648 if text_dom.length != 1:
649 raise Error('More than 1 child node exists under %s' % tag_name)
650 if text_dom[0].nodeType != minidom.Node.TEXT_NODE:
651 raise Error('%s\'s child node is not a text node.' % tag_name)
652 texts.append(text_dom[0].data)
653 return texts
654
655
656 class Action(object):
657 def __init__(self, name, description, owners):
658 self.name = name
659 self.description = description
660 self.owners = owners
661
662
663 def _CreateActionTag(top_dom, action_name, action_object):
664 """Create a new action tag.
665
666 Format of an action tag:
667 <action name="name">
668 <owners>
669 <owner>Owner</owner>
670 </owners>
671 <description>
672 Description.
673 </description>
674 </action>
675
676 If action_name is in actions_dict, the values to be inserted are based on the
677 corresponding Action object. If action_name is not in actions_dict, the
678 default value from TAGS is used.
679
680 Args:
681 top_dom: The parent node under which the new action tag is created.
682 action_name: The name of an action.
683 action_object: An action object representing the data to be inserted.
684
685 Returns:
686 An action tag Element with proper children elements.
687 """
688 action_dom = top_dom.createElement('action')
689 action_dom.setAttribute('name', action_name)
690 owners_dom = top_dom.createElement('owners')
691 action_dom.appendChild(owners_dom)
692 description_dom = top_dom.createElement('description')
693 action_dom.appendChild(description_dom)
694
695 # Create description tag.
696 if action_object and action_object.description:
697 # If description for this action is not None, use the store value.
698 # Otherwise, use the default value.
699 description_dom.appendChild(top_dom.createTextNode(
700 action_object.description))
701 else:
702 description_dom.appendChild(top_dom.createTextNode(
703 TAGS.get('description', '')))
704
705 # Create owner tag.
706 if action_object and action_object.owners:
707 # If owners for this action is not None, use the stored value. Otherwise,
708 # use the default value.
709 for owner in action_object.owners:
710 owner_dom = top_dom.createElement('owner')
711 owner_dom.appendChild(top_dom.createTextNode(owner))
712 owners_dom.appendChild(owner_dom)
713 else:
714 # Use default value.
715 owner_dom = top_dom.createElement('owner')
716 owner_dom.appendChild(top_dom.createTextNode(TAGS.get('owner', '')))
717 owners_dom.appendChild(owner_dom)
718 return action_dom
719
720
721 def _PrettyPrint(actions, actions_dict):
722 """Given a list of action data, create a well-printed minidom document.
723
724 Args:
725 actions: A list of action names.
726 actions_dict: A mappting from action name to Action object.
727
728 Returns:
729 A well-printed minidom document that represents the input action data.
730 """
731 # Form new minidom nodes based on updated |actions|.
732 new_dom = minidom.getDOMImplementation().createDocument(None, 'actions', None)
733 top_element = new_dom.documentElement
734
735 # Print out the actions as a sorted list.
736 for action in sorted(actions):
737 top_element.appendChild(_CreateActionTag(new_dom, action,
738 actions_dict.get(action, None)))
739
740 return print_style.GetPrintStyle().PrettyPrintNode(new_dom)
741
742
581 def main(argv): 743 def main(argv):
582 if '--hash' in argv: 744 presubmit = ('--presubmit' in argv)
583 hash_output = True 745 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 746
592 chromeactions_path = os.path.join(path_utils.ScriptDir(), "chromeactions.txt") 747 # Save the original file content.
748 with open(actions_xml_path, 'rb') as f:
749 original_xml = f.read()
593 750
594 if hash_output: 751 actions, actions_dict = _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 752
603 AddComputedActions(actions) 753 AddComputedActions(actions)
604 # TODO(fmantek): bring back webkit editor actions. 754 # TODO(fmantek): bring back webkit editor actions.
605 # AddWebKitEditorActions(actions) 755 # AddWebKitEditorActions(actions)
606 AddAboutFlagsActions(actions) 756 AddAboutFlagsActions(actions)
607 AddWebUIActions(actions) 757 AddWebUIActions(actions)
608 758
609 AddLiteralActions(actions) 759 AddLiteralActions(actions)
610 760
611 # print "Scanned {0} number of files".format(number_of_files_total) 761 # print "Scanned {0} number of files".format(number_of_files_total)
612 # print "Found {0} entries".format(len(actions)) 762 # print "Found {0} entries".format(len(actions))
613 763
614 AddAndroidActions(actions) 764 AddAndroidActions(actions)
615 AddAutomaticResetBannerActions(actions) 765 AddAutomaticResetBannerActions(actions)
616 AddBookmarkManagerActions(actions) 766 AddBookmarkManagerActions(actions)
617 AddChromeOSActions(actions) 767 AddChromeOSActions(actions)
618 AddClosedSourceActions(actions) 768 AddClosedSourceActions(actions)
619 AddExtensionActions(actions) 769 AddExtensionActions(actions)
620 AddHistoryPageActions(actions) 770 AddHistoryPageActions(actions)
621 AddKeySystemSupportActions(actions) 771 AddKeySystemSupportActions(actions)
622 772
623 if hash_output: 773 pretty = _PrettyPrint(actions, actions_dict)
624 f = open(chromeactions_path, "wb") 774 if original_xml == pretty:
775 print 'actions.xml is correctly pretty-printed.'
776 sys.exit(0)
777 if presubmit:
778 logging.info('actions.xml is not formatted correctly; run '
779 'extract_actions.py to fix.')
780 sys.exit(1)
625 781
782 # Prompt user to consent on the change.
783 if not diff_util.PromptUserToAcceptDiff(
784 original_xml, pretty, 'Is the new version acceptable?'):
785 logging.error('Aborting')
786 sys.exit(1)
626 787
627 # Print out the actions as a sorted list. 788 print 'Creating backup file: actions.old.xml.'
628 for action in sorted(actions): 789 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 790
636 if hash_output: 791 with open(actions_xml_path, 'wb') as f:
637 print "Done. Do not forget to add chromeactions.txt to your changelist" 792 f.write(pretty)
793 print ('Updated %s. Don\'t forget to add it to your changelist' %
794 actions_xml_path)
638 return 0 795 return 0
639 796
640 797
641 if '__main__' == __name__: 798 if '__main__' == __name__:
642 sys.exit(main(sys.argv)) 799 sys.exit(main(sys.argv))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698