Provide more context to calls to Segment.
Implementation for DE-1089. Centralize the definition of context into a single method. This is in common/djangoapps/track because the context is originally set there by middleware.
This commit is contained in:
57
common/djangoapps/track/segment.py
Normal file
57
common/djangoapps/track/segment.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Wrapper methods for emitting events to Segment directly (rather than through tracking log events).
|
||||
|
||||
These take advantage of properties that are extracted from incoming requests by track middleware,
|
||||
stored in tracking context objects, and extracted here to be passed to Segment as part of context
|
||||
required by server-side events.
|
||||
|
||||
To use, call "from track import segment", then call segment.track() or segment.identify().
|
||||
|
||||
"""
|
||||
|
||||
import analytics
|
||||
|
||||
from django.conf import settings
|
||||
from eventtracking import tracker
|
||||
|
||||
|
||||
def track(user_id, event_name, properties=None, context=None):
|
||||
"""Wrapper for emitting Segment track event, including augmenting context information from middleware."""
|
||||
|
||||
if event_name is not None and hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
|
||||
properties = properties or {}
|
||||
segment_context = dict(context) if context else {}
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
|
||||
if 'ip' not in segment_context and 'ip' in tracking_context:
|
||||
segment_context['ip'] = tracking_context.get('ip')
|
||||
|
||||
if ('Google Analytics' not in segment_context or 'clientId' not in segment_context['Google Analytics']) and 'client_id' in tracking_context:
|
||||
segment_context['Google Analytics'] = {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
|
||||
if 'userAgent' not in segment_context and 'agent' in tracking_context:
|
||||
segment_context['userAgent'] = tracking_context.get('agent')
|
||||
|
||||
path = tracking_context.get('path')
|
||||
referer = tracking_context.get('referer')
|
||||
page = tracking_context.get('page')
|
||||
if path is not None or referer is not None or page is not None:
|
||||
if 'page' not in segment_context:
|
||||
segment_context['page'] = {}
|
||||
if path is not None and 'path' not in segment_context['page']:
|
||||
segment_context['page']['path'] = path
|
||||
if referer is not None and 'referrer' not in segment_context['page']:
|
||||
segment_context['page']['referrer'] = referer
|
||||
if page is not None and 'url' not in segment_context['page']:
|
||||
segment_context['page']['url'] = page
|
||||
|
||||
analytics.track(user_id, event_name, properties, segment_context)
|
||||
|
||||
|
||||
def identify(user_id, properties, context=None):
|
||||
"""Wrapper for emitting Segment identify event."""
|
||||
if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
|
||||
segment_context = dict(context) if context else {}
|
||||
analytics.identify(user_id, properties, segment_context)
|
||||
149
common/djangoapps/track/tests/test_segment.py
Normal file
149
common/djangoapps/track/tests/test_segment.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""Ensure emitted events contain the fields legacy processors expect to find."""
|
||||
|
||||
import ddt
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from eventtracking import tracker
|
||||
from eventtracking.django import DjangoTracker
|
||||
from mock import sentinel, patch
|
||||
|
||||
from track import segment
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class SegmentTrackTestCase(TestCase):
|
||||
"""Ensure emitted events contain the expected context values."""
|
||||
|
||||
def setUp(self):
|
||||
super(SegmentTrackTestCase, self).setUp()
|
||||
self.tracker = DjangoTracker()
|
||||
tracker.register_tracker(self.tracker)
|
||||
self.properties = {sentinel.key: sentinel.value}
|
||||
|
||||
patcher = patch('track.segment.analytics.track')
|
||||
self.mock_segment_track = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def test_missing_key(self):
|
||||
segment.track(sentinel.user_id, sentinel.name, self.properties)
|
||||
self.assertFalse(self.mock_segment_track.called)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY=None)
|
||||
def test_null_key(self):
|
||||
segment.track(sentinel.user_id, sentinel.name, self.properties)
|
||||
self.assertFalse(self.mock_segment_track.called)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY="testkey")
|
||||
def test_missing_name(self):
|
||||
segment.track(sentinel.user_id, None, self.properties)
|
||||
self.assertFalse(self.mock_segment_track.called)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY="testkey")
|
||||
def test_track_without_tracking_context(self):
|
||||
segment.track(sentinel.user_id, sentinel.name, self.properties)
|
||||
self.assertTrue(self.mock_segment_track.called)
|
||||
args, kwargs = self.mock_segment_track.call_args
|
||||
expected_segment_context = {}
|
||||
self.assertEqual((sentinel.user_id, sentinel.name, self.properties, expected_segment_context), args)
|
||||
|
||||
@ddt.unpack
|
||||
@ddt.data(
|
||||
({'ip': sentinel.ip}, {'ip': sentinel.provided_ip}, {'ip': sentinel.ip}),
|
||||
({'agent': sentinel.agent}, {'userAgent': sentinel.provided_agent}, {'userAgent': sentinel.agent}),
|
||||
({'path': sentinel.path}, {'page': {'path': sentinel.provided_path}}, {'page': {'path': sentinel.path}}),
|
||||
({'referer': sentinel.referer}, {'page': {'referrer': sentinel.provided_referer}}, {'page': {'referrer': sentinel.referer}}),
|
||||
({'page': sentinel.page}, {'page': {'url': sentinel.provided_page}}, {'page': {'url': sentinel.page}}),
|
||||
({'client_id': sentinel.client_id}, {'Google Analytics': {'clientId': sentinel.provided_client_id}}, {'Google Analytics': {'clientId': sentinel.client_id}}),
|
||||
)
|
||||
@override_settings(LMS_SEGMENT_KEY="testkey")
|
||||
def test_track_context_with_stuff(self, tracking_context, provided_context, expected_segment_context):
|
||||
# Test first with tracking and no provided context.
|
||||
with self.tracker.context('test', tracking_context):
|
||||
segment.track(sentinel.user_id, sentinel.name, self.properties)
|
||||
args, kwargs = self.mock_segment_track.call_args
|
||||
self.assertEqual((sentinel.user_id, sentinel.name, self.properties, expected_segment_context), args)
|
||||
|
||||
# Test with provided context and no tracking context.
|
||||
segment.track(sentinel.user_id, sentinel.name, self.properties, provided_context)
|
||||
args, kwargs = self.mock_segment_track.call_args
|
||||
self.assertEqual((sentinel.user_id, sentinel.name, self.properties, provided_context), args)
|
||||
|
||||
# Test with provided context and also tracking context.
|
||||
with self.tracker.context('test', tracking_context):
|
||||
segment.track(sentinel.user_id, sentinel.name, self.properties, provided_context)
|
||||
self.assertTrue(self.mock_segment_track.called)
|
||||
args, kwargs = self.mock_segment_track.call_args
|
||||
self.assertEqual((sentinel.user_id, sentinel.name, self.properties, provided_context), args)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY="testkey")
|
||||
def test_track_with_standard_context(self):
|
||||
|
||||
tracking_context = {
|
||||
'accept_language': sentinel.accept_language,
|
||||
'referer': sentinel.referer,
|
||||
'username': sentinel.username,
|
||||
'session': sentinel.session,
|
||||
'ip': sentinel.ip,
|
||||
'host': sentinel.host,
|
||||
'agent': sentinel.agent,
|
||||
'path': sentinel.path,
|
||||
'user_id': sentinel.user_id,
|
||||
'course_id': sentinel.course_id,
|
||||
'org_id': sentinel.org_id,
|
||||
'client_id': sentinel.client_id,
|
||||
}
|
||||
with self.tracker.context('test', tracking_context):
|
||||
segment.track(sentinel.user_id, sentinel.name, self.properties)
|
||||
|
||||
self.assertTrue(self.mock_segment_track.called)
|
||||
args, kwargs = self.mock_segment_track.call_args
|
||||
|
||||
expected_segment_context = {
|
||||
'ip': sentinel.ip,
|
||||
'Google Analytics': {
|
||||
'clientId': sentinel.client_id,
|
||||
},
|
||||
'userAgent': sentinel.agent,
|
||||
'page': {
|
||||
'path': sentinel.path,
|
||||
'referrer': sentinel.referer,
|
||||
# No URL value.
|
||||
}
|
||||
}
|
||||
self.assertEqual((sentinel.user_id, sentinel.name, self.properties, expected_segment_context), args)
|
||||
|
||||
|
||||
class SegmentIdentifyTestCase(TestCase):
|
||||
"""Ensure emitted events contain the fields legacy processors expect to find."""
|
||||
|
||||
def setUp(self):
|
||||
super(SegmentIdentifyTestCase, self).setUp()
|
||||
patcher = patch('track.segment.analytics.identify')
|
||||
self.mock_segment_identify = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
self.properties = {sentinel.key: sentinel.value}
|
||||
|
||||
def test_missing_key(self):
|
||||
segment.identify(sentinel.user_id, self.properties)
|
||||
self.assertFalse(self.mock_segment_identify.called)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY=None)
|
||||
def test_null_key(self):
|
||||
segment.identify(sentinel.user_id, self.properties)
|
||||
self.assertFalse(self.mock_segment_identify.called)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY="testkey")
|
||||
def test_normal_call(self):
|
||||
segment.identify(sentinel.user_id, self.properties)
|
||||
self.assertTrue(self.mock_segment_identify.called)
|
||||
args, kwargs = self.mock_segment_identify.call_args
|
||||
self.assertEqual((sentinel.user_id, self.properties, {}), args)
|
||||
|
||||
@override_settings(LMS_SEGMENT_KEY="testkey")
|
||||
def test_call_with_context(self):
|
||||
provided_context = {sentinel.context_key: sentinel.context_value}
|
||||
segment.identify(sentinel.user_id, self.properties, provided_context)
|
||||
self.assertTrue(self.mock_segment_identify.called)
|
||||
args, kwargs = self.mock_segment_identify.call_args
|
||||
self.assertEqual((sentinel.user_id, self.properties, provided_context), args)
|
||||
Reference in New Issue
Block a user