From bb5fbda873fdb8967bf4e443344c22fe24d0934d Mon Sep 17 00:00:00 2001 From: christopher lee Date: Wed, 25 Feb 2015 10:06:26 -0500 Subject: [PATCH] MA-272 Updated analytics shim for mobile video events Shim has been updated to handle currents builds of the android and iOS apps which are not returning the correct semantics/events. Shim now includes the seek event. --- common/djangoapps/track/shim.py | 50 +++++-- .../track/views/tests/test_segmentio.py | 125 ++++++++++++++++++ 2 files changed, 165 insertions(+), 10 deletions(-) diff --git a/common/djangoapps/track/shim.py b/common/djangoapps/track/shim.py index 7960b75330..ddba730f9f 100644 --- a/common/djangoapps/track/shim.py +++ b/common/djangoapps/track/shim.py @@ -76,6 +76,8 @@ NAME_TO_EVENT_TYPE_MAP = { 'edx.video.paused': 'pause_video', 'edx.video.stopped': 'stop_video', 'edx.video.loaded': 'load_video', + 'edx.video.position.changed': 'seek_video', + 'edx.video.seeked': 'seek_video', 'edx.video.transcript.shown': 'show_transcript', 'edx.video.transcript.hidden': 'hide_transcript', } @@ -101,11 +103,14 @@ class VideoEventProcessor(object): if name not in NAME_TO_EVENT_TYPE_MAP: return + # Convert edx.video.seeked to edx.video.positiion.changed + if name == "edx.video.seeked": + event['name'] = "edx.video.position.changed" + event['event_type'] = NAME_TO_EVENT_TYPE_MAP[name] if 'event' not in event: return - payload = event['event'] if 'module_id' in payload: @@ -122,13 +127,38 @@ class VideoEventProcessor(object): if 'current_time' in payload: payload['currentTime'] = payload.pop('current_time') + if 'context' in event: + context = event['context'] + + # Converts seek_type to seek and skip|slide to onSlideSeek|onSkipSeek + if 'seek_type' in payload: + seek_type = payload['seek_type'] + if seek_type == 'slide': + payload['type'] = "onSlideSeek" + elif seek_type == 'skip': + payload['type'] = "onSkipSeek" + del payload['seek_type'] + + # For the iOS build that is returning a +30 for back skip 30 + if ( + context['application']['version'] == "1.0.02" and + context['application']['name'] == "edx.mobileapp.iOS" + ): + if 'requested_skip_interval' in payload and 'type' in payload: + if ( + payload['requested_skip_interval'] == 30 and + payload['type'] == "onSkipSeek" + ): + payload['requested_skip_interval'] = -30 + + # For the Android build that isn't distinguishing between skip/seek + if 'requested_skip_interval' in payload: + if abs(payload['requested_skip_interval']) != 30: + if 'type' in payload: + payload['type'] = 'onSlideSeek' + + if 'open_in_browser_url' in context: + page, _sep, _tail = context.pop('open_in_browser_url').rpartition('/') + event['page'] = page + event['event'] = json.dumps(payload) - - if 'context' not in event: - return - - context = event['context'] - - if 'open_in_browser_url' in context: - page, _sep, _tail = context.pop('open_in_browser_url').rpartition('/') - event['page'] = page diff --git a/common/djangoapps/track/views/tests/test_segmentio.py b/common/djangoapps/track/views/tests/test_segmentio.py index 83beee46c2..09a1dd1220 100644 --- a/common/djangoapps/track/views/tests/test_segmentio.py +++ b/common/djangoapps/track/views/tests/test_segmentio.py @@ -316,6 +316,7 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase): ('edx.video.paused', 'pause_video'), ('edx.video.stopped', 'stop_video'), ('edx.video.loaded', 'load_video'), + ('edx.video.position.changed', 'seek_video'), ('edx.video.transcript.shown', 'show_transcript'), ('edx.video.transcript.hidden', 'hide_transcript'), ) @@ -404,3 +405,127 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase): self.assertEqualUnicode(actual_event, expected_event_without_payload) self.assertEqualUnicode(payload, expected_payload) + + @data( + # Verify positive slide case. Verify slide to onSlideSeek. Verify edx.video.seeked emitted from iOS v1.0.02 is changed to edx.video.position.changed. + (1, 1, "seek_type", "slide", "onSlideSeek", "edx.video.seeked", "edx.video.position.changed", 'edx.mobileapp.iOS', '1.0.02'), + # Verify negative slide case. Verify slide to onSlideSeek. Verify edx.video.seeked to edx.video.position.changed. + (-2, -2, "seek_type", "slide", "onSlideSeek", "edx.video.seeked", "edx.video.position.changed", 'edx.mobileapp.iOS', '1.0.02'), + # Verify +30 is changed to -30 which is incorrectly emitted in iOS v1.0.02. Verify skip to onSkipSeek + (30, -30, "seek_type", "skip", "onSkipSeek", "edx.video.position.changed", "edx.video.position.changed", 'edx.mobileapp.iOS', '1.0.02'), + # Verify the correct case of -30 is also handled as well. Verify skip to onSkipSeek + (-30, -30, "seek_type", "skip", "onSkipSeek", "edx.video.position.changed", "edx.video.position.changed", 'edx.mobileapp.iOS', '1.0.02'), + # Verify positive slide case where onSkipSeek is changed to onSlideSkip. Verify edx.video.seeked emitted from Android v1.0.02 is changed to edx.video.position.changed. + (1, 1, "type", "onSkipSeek", "onSlideSeek", "edx.video.seeked", "edx.video.position.changed", 'edx.mobileapp.android', '1.0.02'), + # Verify positive slide case where onSkipSeek is changed to onSlideSkip. Verify edx.video.seeked emitted from Android v1.0.02 is changed to edx.video.position.changed. + (-2, -2, "type", "onSkipSeek", "onSlideSeek", "edx.video.seeked", "edx.video.position.changed", 'edx.mobileapp.android', '1.0.02'), + # Verify positive skip case where onSkipSeek is not changed and does not become negative. + (30, 30, "type", "onSkipSeek", "onSkipSeek", "edx.video.position.changed", "edx.video.position.changed", 'edx.mobileapp.android', '1.0.02'), + # Verify positive skip case where onSkipSeek is not changed. + (-30, -30, "type", "onSkipSeek", "onSkipSeek", "edx.video.position.changed", "edx.video.position.changed", 'edx.mobileapp.android', '1.0.02') + ) + @unpack + def test_previous_builds(self, + requested_skip_interval, + expected_skip_interval, + seek_type_key, + seek_type, + expected_seek_type, + name, + expected_name, + platform, + version, + ): + """ + Test backwards compatibility of previous app builds + + iOS version 1.0.02: Incorrectly emits the skip back 30 seconds as +30 + instead of -30. + Android version 1.0.02: Skip and slide were both being returned as a + skip. Skip or slide is determined by checking if the skip time is == -30 + Additionally, for both of the above mentioned versions, edx.video.seeked + was sent instead of edx.video.position.changed + """ + course_id = 'foo/bar/baz' + middleware = TrackMiddleware() + input_payload = { + "code": "mobile", + "new_time": 89.699177437, + "old_time": 119.699177437, + seek_type_key: seek_type, + "requested_skip_interval": requested_skip_interval, + 'module_id': 'i4x://foo/bar/baz/some_module', + } + request = self.create_request( + data=self.create_segmentio_event_json( + name=name, + data=input_payload, + context={ + 'open_in_browser_url': 'https://testserver/courses/foo/bar/baz/courseware/Week_1/Activity/2', + 'course_id': course_id, + 'application': { + 'name': platform, + 'version': version, + 'component': 'videoplayer' + } + }, + ), + content_type='application/json' + ) + User.objects.create(pk=USER_ID, username=str(sentinel.username)) + + middleware.process_request(request) + try: + response = segmentio.segmentio_event(request) + self.assertEquals(response.status_code, 200) + + expected_event_without_payload = { + 'accept_language': '', + 'referer': '', + 'username': str(sentinel.username), + 'ip': '', + 'session': '', + 'event_source': 'mobile', + 'event_type': "seek_video", + 'name': expected_name, + 'agent': str(sentinel.user_agent), + 'page': 'https://testserver/courses/foo/bar/baz/courseware/Week_1/Activity', + 'time': datetime.strptime("2014-08-27T16:33:39.215Z", "%Y-%m-%dT%H:%M:%S.%fZ"), + 'host': 'testserver', + 'context': { + 'user_id': USER_ID, + 'course_id': course_id, + 'org_id': 'foo', + 'path': ENDPOINT, + 'client': { + 'library': { + 'name': 'test-app', + 'version': 'unknown' + }, + 'app': { + 'version': '1.0.1', + }, + }, + 'application': { + 'name': platform, + 'version': version, + 'component': 'videoplayer' + }, + 'received_at': datetime.strptime("2014-08-27T16:33:39.100Z", "%Y-%m-%dT%H:%M:%S.%fZ"), + }, + } + expected_payload = { + "code": "mobile", + "new_time": 89.699177437, + "old_time": 119.699177437, + "type": expected_seek_type, + "requested_skip_interval": expected_skip_interval, + 'id': 'i4x-foo-bar-baz-some_module', + } + finally: + middleware.process_response(request, None) + + actual_event = dict(self.get_event()) + payload = json.loads(actual_event.pop('event')) + self.assertEqualUnicode(actual_event, expected_event_without_payload) + self.assertEqualUnicode(payload, expected_payload)