From 79461722dedbf9974c992d2090bac1b49fbc3a45 Mon Sep 17 00:00:00 2001 From: Renzo Lucioni Date: Tue, 19 Aug 2014 12:52:57 -0400 Subject: [PATCH] Re-use GA cookie when sending server-side events to Segment.io --- common/djangoapps/student/models.py | 6 ++++ common/djangoapps/track/middleware.py | 11 ++++++- common/djangoapps/track/shim.py | 6 +++- .../djangoapps/track/tests/test_middleware.py | 1 + common/djangoapps/track/tests/test_shim.py | 1 + common/djangoapps/track/tests/test_views.py | 33 +++++++++++++++++++ 6 files changed, 56 insertions(+), 2 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 764a1b0896..3391c519c2 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -756,6 +756,7 @@ class CourseEnrollment(models.Model): tracker.emit(event_name, data) if settings.FEATURES.get('SEGMENT_IO_LMS') and settings.SEGMENT_IO_LMS_KEY: + tracking_context = tracker.get_tracker().resolve_context() analytics.track(self.user_id, event_name, { 'category': 'conversion', 'label': self.course_id.to_deprecated_string(), @@ -763,7 +764,12 @@ class CourseEnrollment(models.Model): 'course': self.course_id.course, 'run': self.course_id.run, 'mode': self.mode, + }, context={ + 'Google Analytics': { + 'clientId': tracking_context.get('client_id') + } }) + except: # pylint: disable=bare-except if event_name and self.course_id: log.exception('Unable to emit event %s for user %s and course %s', event_name, self.user.username, self.course_id) diff --git a/common/djangoapps/track/middleware.py b/common/djangoapps/track/middleware.py index 01be004d43..9dc2a03692 100644 --- a/common/djangoapps/track/middleware.py +++ b/common/djangoapps/track/middleware.py @@ -87,7 +87,7 @@ class TrackMiddleware(object): Extract information from the request and add it to the tracking context. - The following fields are injected in to the context: + The following fields are injected into the context: * session - The Django session key that identifies the user's session. * user_id - The numeric ID for the logged in user. @@ -96,6 +96,7 @@ class TrackMiddleware(object): * host - The "SERVER_NAME" header, which should be the name of the server running this code. * agent - The client browser identification string. * path - The path part of the requested URL. + * client_id - The unique key used by Google Analytics to identify a user """ context = { 'session': self.get_session_key(request), @@ -105,6 +106,14 @@ class TrackMiddleware(object): for header_name, context_key in META_KEY_TO_CONTEXT_KEY.iteritems(): context[context_key] = request.META.get(header_name, '') + # Google Analytics uses the clientId to keep track of unique visitors. A GA cookie looks like + # this: _ga=GA1.2.1033501218.1368477899. The clientId is this part: 1033501218.1368477899. + google_analytics_cookie = request.COOKIES.get('_ga') + if google_analytics_cookie is None: + context['client_id'] = None + else: + context['client_id'] = '.'.join(google_analytics_cookie.split('.')[2:]) + context.update(contexts.course_context_from_url(request.build_absolute_uri())) tracker.get_tracker().enter_context( diff --git a/common/djangoapps/track/shim.py b/common/djangoapps/track/shim.py index f8c7a61c20..4318f19069 100644 --- a/common/djangoapps/track/shim.py +++ b/common/djangoapps/track/shim.py @@ -45,6 +45,10 @@ class LegacyFieldMappingProcessor(object): def remove_shim_context(event): if 'context' in event: context = event['context'] - for field in CONTEXT_FIELDS_TO_INCLUDE: + # These fields are present elsewhere in the event at this point + context_fields_to_remove = set(CONTEXT_FIELDS_TO_INCLUDE) + # This field is only used for Segment.io web analytics and does not concern researchers + context_fields_to_remove.add('client_id') + for field in context_fields_to_remove: if field in context: del context[field] diff --git a/common/djangoapps/track/tests/test_middleware.py b/common/djangoapps/track/tests/test_middleware.py index a280886311..5779890d3a 100644 --- a/common/djangoapps/track/tests/test_middleware.py +++ b/common/djangoapps/track/tests/test_middleware.py @@ -64,6 +64,7 @@ class TrackMiddlewareTestCase(TestCase): 'path': '/courses/', 'org_id': '', 'course_id': '', + 'client_id': None, }) def get_context_for_path(self, path): diff --git a/common/djangoapps/track/tests/test_shim.py b/common/djangoapps/track/tests/test_shim.py index b20c513d29..12d8f21ae8 100644 --- a/common/djangoapps/track/tests/test_shim.py +++ b/common/djangoapps/track/tests/test_shim.py @@ -50,6 +50,7 @@ class LegacyFieldMappingProcessorTestCase(TestCase): 'course_id': sentinel.course_id, 'org_id': sentinel.org_id, 'event_type': sentinel.event_type, + 'client_id': sentinel.client_id, } with django_tracker.context('test', context): django_tracker.emit(sentinel.name, data) diff --git a/common/djangoapps/track/tests/test_views.py b/common/djangoapps/track/tests/test_views.py index 8a77245c74..f8c0cd2243 100644 --- a/common/djangoapps/track/tests/test_views.py +++ b/common/djangoapps/track/tests/test_views.py @@ -144,6 +144,39 @@ class TestTrackViews(TestCase): self.mock_tracker.send.assert_called_once_with(expected_event) + @freeze_time(expected_time) + def test_server_track_with_middleware_and_google_analytics_cookie(self): + middleware = TrackMiddleware() + request = self.request_factory.get(self.path_with_course) + request.COOKIES['_ga'] = 'GA1.2.1033501218.1368477899' + middleware.process_request(request) + # The middleware emits an event, reset the mock to ignore it since we aren't testing that feature. + self.mock_tracker.reset_mock() + try: + views.server_track(request, str(sentinel.event_type), '{}') + + expected_event = { + 'username': 'anonymous', + 'ip': '127.0.0.1', + 'event_source': 'server', + 'event_type': str(sentinel.event_type), + 'event': '{}', + 'agent': '', + 'page': None, + 'time': expected_time, + 'host': 'testserver', + 'context': { + 'user_id': '', + 'course_id': u'foo/bar/baz', + 'org_id': 'foo', + 'path': u'/courses/foo/bar/baz/xmod/' + }, + } + finally: + middleware.process_response(request, None) + + self.mock_tracker.send.assert_called_once_with(expected_event) + @freeze_time(expected_time) def test_server_track_with_no_request(self): request = None