Commit 5643101a by Gabe Mulley

Small changes to the segment.io event handler

parent 03b41032
...@@ -93,14 +93,19 @@ def track_segmentio_event(request): ...@@ -93,14 +93,19 @@ def track_segmentio_event(request):
# The POST body will contain the JSON encoded event # The POST body will contain the JSON encoded event
full_segment_event = request.json full_segment_event = request.json
# We mostly care about the properties
segment_event = full_segment_event.get('properties', {})
def logged_failure_response(*args, **kwargs): def logged_failure_response(*args, **kwargs):
"""Indicate a failure and log information about the event that will aide debugging efforts""" """Indicate a failure and log information about the event that will aide debugging efforts"""
failed_response = failure_response(*args, **kwargs) failed_response = failure_response(*args, **kwargs)
log.warning('Unable to process event received from segment.io: %s', json.dumps(full_segment_event)) log.warning('Unable to process event received from segment.io: %s', json.dumps(full_segment_event))
return failed_response return failed_response
# Selectively listen to particular channels # Selectively listen to particular channels, note that the client can set the "event_source" field in the
channel = full_segment_event.get('channel') # "properties" dict to override the channel provided by segment.io. This is necessary because there is a bug in some
# segment.io client libraries that prevented them from sending correct channel fields.
channel = segment_event.get('event_source')
allowed_channels = [c.lower() for c in getattr(settings, 'TRACKING_SEGMENTIO_ALLOWED_CHANNELS', [])] allowed_channels = [c.lower() for c in getattr(settings, 'TRACKING_SEGMENTIO_ALLOWED_CHANNELS', [])]
if not channel or channel.lower() not in allowed_channels: if not channel or channel.lower() not in allowed_channels:
return response(WARNING_IGNORED_CHANNEL, committed=False) return response(WARNING_IGNORED_CHANNEL, committed=False)
...@@ -111,15 +116,15 @@ def track_segmentio_event(request): ...@@ -111,15 +116,15 @@ def track_segmentio_event(request):
if not action or action.lower() not in allowed_actions: if not action or action.lower() not in allowed_actions:
return response(WARNING_IGNORED_ACTION, committed=False) return response(WARNING_IGNORED_ACTION, committed=False)
# We mostly care about the properties
segment_event = full_segment_event.get('properties', {})
context = {} context = {}
# Start with the context provided by segment.io in the "client" field if it exists # Start with the context provided by segment.io in the "client" field if it exists
segment_context = full_segment_event.get('context') segment_context = full_segment_event.get('context')
if segment_context: if segment_context:
context['client'] = segment_context context['client'] = segment_context
user_agent = segment_context.get('userAgent', '')
else:
user_agent = ''
# Overlay any context provided in the properties # Overlay any context provided in the properties
context.update(segment_event.get('context', {})) context.update(segment_event.get('context', {}))
...@@ -136,7 +141,7 @@ def track_segmentio_event(request): ...@@ -136,7 +141,7 @@ def track_segmentio_event(request):
except ValueError: except ValueError:
return logged_failure_response(ERROR_INVALID_USER_ID) return logged_failure_response(ERROR_INVALID_USER_ID)
else: else:
context['user_id'] = user_id context['user_id'] = user.id
# course_id is expected to be provided in the context when applicable # course_id is expected to be provided in the context when applicable
course_id = context.get('course_id') course_id = context.get('course_id')
...@@ -173,6 +178,7 @@ def track_segmentio_event(request): ...@@ -173,6 +178,7 @@ def track_segmentio_event(request):
event = { event = {
"username": user.username, "username": user.username,
"event_type": event_type, "event_type": event_type,
"name": segment_event.get('name', ''),
# Will be either "mobile", "browser" or "server". These names happen to be identical to the names we already # Will be either "mobile", "browser" or "server". These names happen to be identical to the names we already
# use so no mapping is necessary. # use so no mapping is necessary.
"event_source": channel, "event_source": channel,
...@@ -181,7 +187,7 @@ def track_segmentio_event(request): ...@@ -181,7 +187,7 @@ def track_segmentio_event(request):
"context": complete_context, "context": complete_context,
"page": segment_event.get('page'), "page": segment_event.get('page'),
"host": complete_context.get('host', ''), "host": complete_context.get('host', ''),
"agent": '', "agent": user_agent,
"ip": segment_event.get('ip', ''), "ip": segment_event.get('ip', ''),
"event": segment_event.get('event', {}), "event": segment_event.get('event', {}),
} }
......
...@@ -40,7 +40,7 @@ class SegmentIOTrackingTestCase(TestCase): ...@@ -40,7 +40,7 @@ class SegmentIOTrackingTestCase(TestCase):
self.mock_tracker = patcher.start() self.mock_tracker = patcher.start()
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
def test_segmentio_tracking_get_request(self): def test_get_request(self):
request = self.request_factory.get(ENDPOINT) request = self.request_factory.get(ENDPOINT)
response = segmentio.track_segmentio_event(request) response = segmentio.track_segmentio_event(request)
self.assertEquals(response.status_code, 405) self.assertEquals(response.status_code, 405)
...@@ -49,7 +49,7 @@ class SegmentIOTrackingTestCase(TestCase): ...@@ -49,7 +49,7 @@ class SegmentIOTrackingTestCase(TestCase):
@override_settings( @override_settings(
TRACKING_SEGMENTIO_WEBHOOK_SECRET=None TRACKING_SEGMENTIO_WEBHOOK_SECRET=None
) )
def test_segmentio_tracking_no_secret_config(self): def test_no_secret_config(self):
request = self.request_factory.post(ENDPOINT) request = self.request_factory.post(ENDPOINT)
response = segmentio.track_segmentio_event(request) response = segmentio.track_segmentio_event(request)
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_UNAUTHORIZED, 401) self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_UNAUTHORIZED, 401)
...@@ -61,12 +61,12 @@ class SegmentIOTrackingTestCase(TestCase): ...@@ -61,12 +61,12 @@ class SegmentIOTrackingTestCase(TestCase):
self.assertEquals(parsed_content, {'committed': False, 'message': expected_message}) self.assertEquals(parsed_content, {'committed': False, 'message': expected_message})
self.assertFalse(self.mock_tracker.send.called) # pylint: disable=maybe-no-member self.assertFalse(self.mock_tracker.send.called) # pylint: disable=maybe-no-member
def test_segmentio_tracking_no_secret_provided(self): def test_no_secret_provided(self):
request = self.request_factory.post(ENDPOINT) request = self.request_factory.post(ENDPOINT)
response = segmentio.track_segmentio_event(request) response = segmentio.track_segmentio_event(request)
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_UNAUTHORIZED, 401) self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_UNAUTHORIZED, 401)
def test_segmentio_tracking_secret_mismatch(self): def test_secret_mismatch(self):
request = self.create_request(key='y') request = self.create_request(key='y')
response = segmentio.track_segmentio_event(request) response = segmentio.track_segmentio_event(request)
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_UNAUTHORIZED, 401) self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_UNAUTHORIZED, 401)
...@@ -93,7 +93,7 @@ class SegmentIOTrackingTestCase(TestCase): ...@@ -93,7 +93,7 @@ class SegmentIOTrackingTestCase(TestCase):
@data('server', 'browser', 'Browser') @data('server', 'browser', 'Browser')
def test_segmentio_ignore_channels(self, channel): def test_segmentio_ignore_channels(self, channel):
response = self.post_segmentio_event(channel=channel) response = self.post_segmentio_event(event_source=channel)
self.assert_segmentio_uncommitted_response(response, segmentio.WARNING_IGNORED_CHANNEL, 200) self.assert_segmentio_uncommitted_response(response, segmentio.WARNING_IGNORED_CHANNEL, 200)
def create_segmentio_event(self, **kwargs): def create_segmentio_event(self, **kwargs):
...@@ -104,17 +104,20 @@ class SegmentIOTrackingTestCase(TestCase): ...@@ -104,17 +104,20 @@ class SegmentIOTrackingTestCase(TestCase):
"event": "Did something", "event": "Did something",
"properties": { "properties": {
'event_type': kwargs.get('event_type', ''), 'event_type': kwargs.get('event_type', ''),
'event_source': kwargs.get('event_source', 'mobile'),
'event': kwargs.get('event', {}), 'event': kwargs.get('event', {}),
'context': { 'context': {
'course_id': kwargs.get('course_id') or '', 'course_id': kwargs.get('course_id') or '',
} },
'name': str(sentinel.name),
}, },
"channel": kwargs.get('channel', 'mobile'), "channel": kwargs.get('channel', 'mobile'),
"context": { "context": {
"library": { "library": {
"name": "unknown", "name": "unknown",
"version": "unknown" "version": "unknown"
} },
'userAgent': str(sentinel.user_agent),
}, },
"receivedAt": "2014-08-27T16:33:39.100Z", "receivedAt": "2014-08-27T16:33:39.100Z",
"timestamp": "2014-08-27T16:33:39.215Z", "timestamp": "2014-08-27T16:33:39.215Z",
...@@ -129,22 +132,23 @@ class SegmentIOTrackingTestCase(TestCase): ...@@ -129,22 +132,23 @@ class SegmentIOTrackingTestCase(TestCase):
}, },
"action": action "action": action
} }
return sample_event return sample_event
def create_segmentio_event_json(self, **kwargs): def create_segmentio_event_json(self, **kwargs):
"""Return a json string containing a fake segment.io event""" """Return a json string containing a fake segment.io event"""
return json.dumps(self.create_segmentio_event(**kwargs)) return json.dumps(self.create_segmentio_event(**kwargs))
def test_segmentio_tracking_no_user_for_user_id(self): def test_no_user_for_user_id(self):
response = self.post_segmentio_event(user_id=40) response = self.post_segmentio_event(user_id=40)
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_USER_NOT_EXIST, 400) self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_USER_NOT_EXIST, 400)
def test_segmentio_tracking_invalid_user_id(self): def test_invalid_user_id(self):
response = self.post_segmentio_event(user_id='foobar') response = self.post_segmentio_event(user_id='foobar')
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_INVALID_USER_ID, 400) self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_INVALID_USER_ID, 400)
@data('foo/bar/baz', 'course-v1:foo+bar+baz') @data('foo/bar/baz', 'course-v1:foo+bar+baz')
def test_segmentio_tracking(self, course_id): def test_success(self, course_id):
middleware = TrackMiddleware() middleware = TrackMiddleware()
request = self.create_request( request = self.create_request(
...@@ -165,8 +169,9 @@ class SegmentIOTrackingTestCase(TestCase): ...@@ -165,8 +169,9 @@ class SegmentIOTrackingTestCase(TestCase):
'ip': '', 'ip': '',
'event_source': 'mobile', 'event_source': 'mobile',
'event_type': str(sentinel.event_type), 'event_type': str(sentinel.event_type),
'name': str(sentinel.name),
'event': {'foo': 'bar'}, 'event': {'foo': 'bar'},
'agent': '', 'agent': str(sentinel.user_agent),
'page': None, 'page': None,
'time': datetime.strptime("2014-08-27T16:33:39.215Z", "%Y-%m-%dT%H:%M:%S.%fZ"), 'time': datetime.strptime("2014-08-27T16:33:39.215Z", "%Y-%m-%dT%H:%M:%S.%fZ"),
'host': 'testserver', 'host': 'testserver',
...@@ -179,7 +184,8 @@ class SegmentIOTrackingTestCase(TestCase): ...@@ -179,7 +184,8 @@ class SegmentIOTrackingTestCase(TestCase):
'library': { 'library': {
'name': 'unknown', 'name': 'unknown',
'version': 'unknown' 'version': 'unknown'
} },
'userAgent': str(sentinel.user_agent)
}, },
'received_at': datetime.strptime("2014-08-27T16:33:39.100Z", "%Y-%m-%dT%H:%M:%S.%fZ"), 'received_at': datetime.strptime("2014-08-27T16:33:39.100Z", "%Y-%m-%dT%H:%M:%S.%fZ"),
}, },
...@@ -189,7 +195,7 @@ class SegmentIOTrackingTestCase(TestCase): ...@@ -189,7 +195,7 @@ class SegmentIOTrackingTestCase(TestCase):
self.mock_tracker.send.assert_called_once_with(expected_event) # pylint: disable=maybe-no-member self.mock_tracker.send.assert_called_once_with(expected_event) # pylint: disable=maybe-no-member
def test_segmentio_tracking_invalid_course_id(self): def test_invalid_course_id(self):
request = self.create_request( request = self.create_request(
data=self.create_segmentio_event_json(course_id='invalid'), data=self.create_segmentio_event_json(course_id='invalid'),
content_type='application/json' content_type='application/json'
...@@ -199,9 +205,9 @@ class SegmentIOTrackingTestCase(TestCase): ...@@ -199,9 +205,9 @@ class SegmentIOTrackingTestCase(TestCase):
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertTrue(self.mock_tracker.send.called) # pylint: disable=maybe-no-member self.assertTrue(self.mock_tracker.send.called) # pylint: disable=maybe-no-member
def test_segmentio_tracking_missing_event_type(self): def test_missing_event_type(self):
sample_event_raw = self.create_segmentio_event() sample_event_raw = self.create_segmentio_event()
sample_event_raw['properties'] = {} del sample_event_raw['properties']['event_type']
request = self.create_request( request = self.create_request(
data=json.dumps(sample_event_raw), data=json.dumps(sample_event_raw),
content_type='application/json' content_type='application/json'
...@@ -211,7 +217,7 @@ class SegmentIOTrackingTestCase(TestCase): ...@@ -211,7 +217,7 @@ class SegmentIOTrackingTestCase(TestCase):
response = segmentio.track_segmentio_event(request) response = segmentio.track_segmentio_event(request)
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_MISSING_EVENT_TYPE, 400) self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_MISSING_EVENT_TYPE, 400)
def test_segmentio_tracking_missing_timestamp(self): def test_missing_timestamp(self):
sample_event_raw = self.create_event_without_fields('timestamp') sample_event_raw = self.create_event_without_fields('timestamp')
request = self.create_request( request = self.create_request(
data=json.dumps(sample_event_raw), data=json.dumps(sample_event_raw),
...@@ -232,7 +238,7 @@ class SegmentIOTrackingTestCase(TestCase): ...@@ -232,7 +238,7 @@ class SegmentIOTrackingTestCase(TestCase):
return event return event
def test_segmentio_tracking_missing_received_at(self): def test_missing_received_at(self):
sample_event_raw = self.create_event_without_fields('receivedAt') sample_event_raw = self.create_event_without_fields('receivedAt')
request = self.create_request( request = self.create_request(
data=json.dumps(sample_event_raw), data=json.dumps(sample_event_raw),
...@@ -242,3 +248,18 @@ class SegmentIOTrackingTestCase(TestCase): ...@@ -242,3 +248,18 @@ class SegmentIOTrackingTestCase(TestCase):
response = segmentio.track_segmentio_event(request) response = segmentio.track_segmentio_event(request)
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_MISSING_RECEIVED_AT, 400) self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_MISSING_RECEIVED_AT, 400)
def test_string_user_id(self):
User.objects.create(pk=USER_ID, username=str(sentinel.username))
response = self.post_segmentio_event(user_id=str(USER_ID))
result = self.assert_segmentio_committed_response(response)
self.assertEquals(result['context']['user_id'], USER_ID)
def assert_segmentio_committed_response(self, response):
"""Assert that an event was emitted"""
self.assertEquals(response.status_code, 200)
parsed_content = json.loads(response.content)
self.assertEquals(parsed_content, {'committed': True})
self.assertTrue(self.mock_tracker.send.called) # pylint: disable=maybe-no-member
return self.mock_tracker.send.mock_calls[0][1][0]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment