diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index e74ba6dc03..2c125231bb 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -3,7 +3,7 @@ xModule implementation of a learning sequence """ # pylint: disable=abstract-method - +import collections import json import logging from pkg_resources import resource_string @@ -13,6 +13,7 @@ from lxml import etree from xblock.core import XBlock from xblock.fields import Integer, Scope, Boolean, String from xblock.fragment import Fragment +import newrelic.agent from .exceptions import NotFoundError from .fields import Date @@ -191,6 +192,11 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): context["username"] = self.runtime.service(self, "user").get_current_user().opt_attrs['edx-platform.username'] display_names = [self.get_parent().display_name or '', self.display_name or ''] + + # We do this up here because proctored exam functionality could bypass + # rendering after this section. + self._capture_basic_metrics() + # Is this sequential part of a timed or proctored exam? if self.is_time_limited: view_html = self._time_limited_student_view(context) @@ -201,7 +207,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): fragment.add_content(view_html) return fragment - for child in self.get_display_items(): + display_items = self.get_display_items() + for child in display_items: is_bookmarked = bookmarks_service.is_bookmarked(usage_key=child.scope_ids.usage_id) context["bookmarked"] = is_bookmarked @@ -238,8 +245,78 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): fragment.add_content(self.system.render_template("seq_module.html", params)) + self._capture_full_seq_item_metrics(display_items) + self._capture_current_unit_metrics(display_items) + + # Get all descendant XBlock types and counts return fragment + def _locations_in_subtree(self, node): + """ + The usage keys for all descendants of an XBlock/XModule as a flat list. + + Includes the location of the node passed in. + """ + stack = [node] + locations = [] + + while stack: + curr = stack.pop() + locations.append(curr.location) + if curr.has_children: + stack.extend(curr.get_children()) + + return locations + + def _capture_basic_metrics(self): + """ + Capture basic information about this sequence in New Relic. + """ + newrelic.agent.add_custom_parameter('seq.block_id', unicode(self.location)) + newrelic.agent.add_custom_parameter('seq.display_name', self.display_name or '') + newrelic.agent.add_custom_parameter('seq.position', self.position) + newrelic.agent.add_custom_parameter('seq.is_time_limited', self.is_time_limited) + + def _capture_full_seq_item_metrics(self, display_items): + """ + Capture information about the number and types of XBlock content in + the sequence as a whole. We send this information to New Relic so that + we can do better performance analysis of courseware. + """ + # Basic count of the number of Units (a.k.a. VerticalBlocks) we have in + # this learning sequence + newrelic.agent.add_custom_parameter('seq.num_units', len(display_items)) + + # Count of all modules (leaf nodes) in this sequence (e.g. videos, + # problems, etc.) The units (verticals) themselves are not counted. + all_item_keys = self._locations_in_subtree(self) + newrelic.agent.add_custom_parameter('seq.num_items', len(all_item_keys)) + + # Count of all modules by block_type (e.g. "video": 2, "discussion": 4) + block_counts = collections.Counter(usage_key.block_type for usage_key in all_item_keys) + for block_type, count in block_counts.items(): + newrelic.agent.add_custom_parameter('seq.block_counts.{}'.format(block_type), count) + + def _capture_current_unit_metrics(self, display_items): + """ + Capture information about the current selected Unit within the Sequence. + """ + # Positions are stored with indexing starting at 1. If we get into a + # weird state where the saved position is out of bounds (e.g. the + # content was changed), avoid going into any details about this unit. + if 1 <= self.position <= len(display_items): + # Basic info about the Unit... + current = display_items[self.position - 1] + newrelic.agent.add_custom_parameter('seq.current.block_id', unicode(current.location)) + newrelic.agent.add_custom_parameter('seq.current.display_name', current.display_name or '') + + # Examining all items inside the Unit (or split_test, conditional, etc.) + child_locs = self._locations_in_subtree(current) + newrelic.agent.add_custom_parameter('seq.current.num_items', len(child_locs)) + curr_block_counts = collections.Counter(usage_key.block_type for usage_key in child_locs) + for block_type, count in curr_block_counts.items(): + newrelic.agent.add_custom_parameter('seq.current.block_counts.{}'.format(block_type), count) + def _time_limited_student_view(self, context): """ Delegated rendering of a student view when in a time