Commit d2b03e28 by Gabe Mulley

add course_id to event context

parent 0138322c
...@@ -384,6 +384,7 @@ INSTALLED_APPS = ( ...@@ -384,6 +384,7 @@ INSTALLED_APPS = (
# Tracking # Tracking
'track', 'track',
'eventtracking.django',
# Monitoring # Monitoring
'datadog', 'datadog',
...@@ -438,3 +439,4 @@ TRACKING_BACKENDS = { ...@@ -438,3 +439,4 @@ TRACKING_BACKENDS = {
# We're already logging events, and we don't want to capture user # We're already logging events, and we don't want to capture user
# names/passwords. Heartbeat events are likely not interesting. # names/passwords. Heartbeat events are likely not interesting.
TRACKING_IGNORE_URL_PATTERNS = [r'^/event', r'^/login', r'^/heartbeat'] TRACKING_IGNORE_URL_PATTERNS = [r'^/event', r'^/login', r'^/heartbeat']
TRACKING_ENABLED = True
"""Generates common contexts"""
import re
COURSE_REGEX = re.compile(r'^.*?/courses/(?P<course_id>(?P<org_id>[^/]+)/[^/]+/[^/]+)')
def course_context_from_url(url):
"""
Extracts the course_id from the given `url.`
Example Returned Context::
{
'course_id': 'org/course/run',
'org_id': 'org'
}
"""
url = url or ''
context = {
'course_id': '',
'org_id': ''
}
match = COURSE_REGEX.match(url)
if match:
context.update(match.groupdict())
return context
...@@ -3,13 +3,20 @@ import re ...@@ -3,13 +3,20 @@ import re
from django.conf import settings from django.conf import settings
import views from track import views
from track import contexts
from eventtracking import tracker
COURSE_CONTEXT_NAME = 'edx.course'
class TrackMiddleware(object): class TrackMiddleware(object):
def process_request(self, request): def process_request(self, request):
try: try:
if not self._should_process_request(request): self.enter_course_context(request)
if not self.should_process_request(request):
return return
# Removes passwords from the tracking logs # Removes passwords from the tracking logs
...@@ -47,7 +54,8 @@ class TrackMiddleware(object): ...@@ -47,7 +54,8 @@ class TrackMiddleware(object):
except: except:
pass pass
def _should_process_request(self, request): def should_process_request(self, request):
"""Don't track requests to the specified URL patterns"""
path = request.META['PATH_INFO'] path = request.META['PATH_INFO']
ignored_url_patterns = getattr(settings, 'TRACKING_IGNORE_URL_PATTERNS', []) ignored_url_patterns = getattr(settings, 'TRACKING_IGNORE_URL_PATTERNS', [])
...@@ -57,3 +65,22 @@ class TrackMiddleware(object): ...@@ -57,3 +65,22 @@ class TrackMiddleware(object):
if re.match(pattern, path): if re.match(pattern, path):
return False return False
return True return True
def enter_course_context(self, request):
"""
Extract course information from the request and add it to the
tracking context.
"""
tracker.get_tracker().enter_context(
COURSE_CONTEXT_NAME,
contexts.course_context_from_url(request.build_absolute_uri())
)
def process_response(self, request, response): # pylint: disable=unused-argument
"""Exit the course context if it exists."""
try:
tracker.get_tracker().exit_context(COURSE_CONTEXT_NAME)
except: # pylint: disable=bare-except
pass
return response
# pylint: disable=missing-docstring,maybe-no-member
from unittest import TestCase
from track import contexts
class TestContexts(TestCase):
COURSE_ID = 'test/course_name/course_run'
ORG_ID = 'test'
def test_course_id_from_url(self):
self.assert_parses_course_id_from_url('http://foo.bar.com/courses/{course_id}/more/stuff')
def assert_parses_course_id_from_url(self, format_string):
self.assertEquals(
contexts.course_context_from_url(format_string.format(course_id=self.COURSE_ID)),
{
'course_id': self.COURSE_ID,
'org_id': self.ORG_ID
}
)
def test_no_course_id_in_url(self):
self.assert_empty_context_for_url('http://foo.bar.com/dashboard')
def assert_empty_context_for_url(self, url):
self.assertEquals(
contexts.course_context_from_url(url),
{
'course_id': '',
'org_id': ''
}
)
def test_malformed_course_id(self):
self.assert_empty_context_for_url('http://foo.bar.com/courses/test')
def test_course_id_later_in_url(self):
self.assert_parses_course_id_from_url('http://foo.bar.com/x/y/z/courses/{course_id}')
def test_no_url(self):
self.assert_empty_context_for_url(None)
...@@ -6,41 +6,61 @@ from django.test import TestCase ...@@ -6,41 +6,61 @@ from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.test.utils import override_settings from django.test.utils import override_settings
from eventtracking import tracker
from track.middleware import TrackMiddleware from track.middleware import TrackMiddleware
@patch('track.views.server_track')
class TrackMiddlewareTestCase(TestCase): class TrackMiddlewareTestCase(TestCase):
def setUp(self): def setUp(self):
self.track_middleware = TrackMiddleware() self.track_middleware = TrackMiddleware()
self.request_factory = RequestFactory() self.request_factory = RequestFactory()
def test_normal_request(self, mock_server_track): patcher = patch('track.views.server_track')
self.mock_server_track = patcher.start()
self.addCleanup(patcher.stop)
def test_normal_request(self):
request = self.request_factory.get('/somewhere') request = self.request_factory.get('/somewhere')
self.track_middleware.process_request(request) self.track_middleware.process_request(request)
self.assertTrue(mock_server_track.called) self.assertTrue(self.mock_server_track.called)
def test_default_filters_do_not_render_view(self, mock_server_track): def test_default_filters_do_not_render_view(self):
for url in ['/event', '/event/1', '/login', '/heartbeat']: for url in ['/event', '/event/1', '/login', '/heartbeat']:
request = self.request_factory.get(url) request = self.request_factory.get(url)
self.track_middleware.process_request(request) self.track_middleware.process_request(request)
self.assertFalse(mock_server_track.called) self.assertFalse(self.mock_server_track.called)
mock_server_track.reset_mock() self.mock_server_track.reset_mock()
@override_settings(TRACKING_IGNORE_URL_PATTERNS=[]) @override_settings(TRACKING_IGNORE_URL_PATTERNS=[])
def test_reading_filtered_urls_from_settings(self, mock_server_track): def test_reading_filtered_urls_from_settings(self):
request = self.request_factory.get('/event') request = self.request_factory.get('/event')
self.track_middleware.process_request(request) self.track_middleware.process_request(request)
self.assertTrue(mock_server_track.called) self.assertTrue(self.mock_server_track.called)
@override_settings(TRACKING_IGNORE_URL_PATTERNS=[r'^/some/excluded.*']) @override_settings(TRACKING_IGNORE_URL_PATTERNS=[r'^/some/excluded.*'])
def test_anchoring_of_patterns_at_beginning(self, mock_server_track): def test_anchoring_of_patterns_at_beginning(self):
request = self.request_factory.get('/excluded') request = self.request_factory.get('/excluded')
self.track_middleware.process_request(request) self.track_middleware.process_request(request)
self.assertTrue(mock_server_track.called) self.assertTrue(self.mock_server_track.called)
mock_server_track.reset_mock() self.mock_server_track.reset_mock()
request = self.request_factory.get('/some/excluded/url') request = self.request_factory.get('/some/excluded/url')
self.track_middleware.process_request(request) self.track_middleware.process_request(request)
self.assertFalse(mock_server_track.called) self.assertFalse(self.mock_server_track.called)
def test_request_in_course_context(self):
request = self.request_factory.get('/courses/test_org/test_course/test_run/foo')
self.track_middleware.process_request(request)
self.assertEquals(
tracker.get_tracker().resolve_context(),
{
'course_id': 'test_org/test_course/test_run',
'org_id': 'test_org'
}
)
self.track_middleware.process_response(request, None)
self.assertEquals(
tracker.get_tracker().resolve_context(),
{}
)
# pylint: disable=missing-docstring,maybe-no-member
from datetime import datetime
from mock import patch
from mock import sentinel
from pytz import UTC
from django.test import TestCase
from django.test.client import RequestFactory
from track import views
class TestTrackViews(TestCase):
def setUp(self):
self.request_factory = RequestFactory()
patcher = patch('track.views.tracker')
self.mock_tracker = patcher.start()
self.addCleanup(patcher.stop)
self._expected_timestamp = datetime.now(UTC)
self._datetime_patcher = patch('track.views.datetime')
self.addCleanup(self._datetime_patcher.stop)
mock_datetime_mod = self._datetime_patcher.start()
mock_datetime_mod.datetime.now.return_value = self._expected_timestamp # pylint: disable=maybe-no-member
self.path_with_course = '/courses/foo/bar/baz/xmod/'
self.url_with_course = 'http://www.edx.org' + self.path_with_course
self.event = {
sentinel.key: sentinel.value
}
def test_user_track(self):
request = self.request_factory.get('/event', {
'page': self.url_with_course,
'event_type': sentinel.event_type,
'event': {}
})
views.user_track(request)
expected_event = {
'username': 'anonymous',
'session': '',
'ip': '127.0.0.1',
'event_source': 'browser',
'event_type': str(sentinel.event_type),
'event': '{}',
'agent': '',
'page': self.url_with_course,
'time': self._expected_timestamp,
'host': 'testserver',
'context': {
'course_id': 'foo/bar/baz',
'org_id': 'foo',
},
}
self.mock_tracker.send.assert_called_once_with(expected_event)
def test_server_track(self):
request = self.request_factory.get(self.path_with_course)
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': self._expected_timestamp,
'host': 'testserver',
'context': {},
}
self.mock_tracker.send.assert_called_once_with(expected_event)
def test_task_track(self):
request_info = {
'username': 'anonymous',
'ip': '127.0.0.1',
'agent': 'agent',
'host': 'testserver',
}
task_info = {
sentinel.task_key: sentinel.task_value
}
expected_event_data = dict(task_info)
expected_event_data.update(self.event)
views.task_track(request_info, task_info, str(sentinel.event_type), self.event)
expected_event = {
'username': 'anonymous',
'ip': '127.0.0.1',
'event_source': 'task',
'event_type': str(sentinel.event_type),
'event': expected_event_data,
'agent': 'agent',
'page': None,
'time': self._expected_timestamp,
'host': 'testserver',
'context': {
'course_id': '',
'org_id': ''
},
}
self.mock_tracker.send.assert_called_once_with(expected_event)
...@@ -12,7 +12,9 @@ from django_future.csrf import ensure_csrf_cookie ...@@ -12,7 +12,9 @@ from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from track import tracker from track import tracker
from track import contexts
from track.models import TrackingLog from track.models import TrackingLog
from eventtracking import tracker as eventtracker
def log_event(event): def log_event(event):
...@@ -43,6 +45,9 @@ def user_track(request): ...@@ -43,6 +45,9 @@ def user_track(request):
except: except:
agent = '' agent = ''
page = request.REQUEST['page']
with eventtracker.get_tracker().context('edx.course.browser', contexts.course_context_from_url(page)):
event = { event = {
"username": username, "username": username,
"session": scookie, "session": scookie,
...@@ -51,9 +56,10 @@ def user_track(request): ...@@ -51,9 +56,10 @@ def user_track(request):
"event_type": request.REQUEST['event_type'], "event_type": request.REQUEST['event_type'],
"event": request.REQUEST['event'], "event": request.REQUEST['event'],
"agent": agent, "agent": agent,
"page": request.REQUEST['page'], "page": page,
"time": datetime.datetime.now(UTC), "time": datetime.datetime.now(UTC),
"host": request.META['SERVER_NAME'], "host": request.META['SERVER_NAME'],
"context": eventtracker.get_tracker().resolve_context(),
} }
log_event(event) log_event(event)
...@@ -83,6 +89,7 @@ def server_track(request, event_type, event, page=None): ...@@ -83,6 +89,7 @@ def server_track(request, event_type, event, page=None):
"page": page, "page": page,
"time": datetime.datetime.now(UTC), "time": datetime.datetime.now(UTC),
"host": request.META['SERVER_NAME'], "host": request.META['SERVER_NAME'],
"context": eventtracker.get_tracker().resolve_context(),
} }
if event_type.startswith("/event_logs") and request.user.is_staff: if event_type.startswith("/event_logs") and request.user.is_staff:
...@@ -118,6 +125,7 @@ def task_track(request_info, task_info, event_type, event, page=None): ...@@ -118,6 +125,7 @@ def task_track(request_info, task_info, event_type, event, page=None):
# All fields must be specified, in case the tracking information is # All fields must be specified, in case the tracking information is
# also saved to the TrackingLog model. Get values from the task-level # also saved to the TrackingLog model. Get values from the task-level
# information, or just add placeholder values. # information, or just add placeholder values.
with eventtracker.get_tracker().context('edx.course.task', contexts.course_context_from_url(page)):
event = { event = {
"username": request_info.get('username', 'unknown'), "username": request_info.get('username', 'unknown'),
"ip": request_info.get('ip', 'unknown'), "ip": request_info.get('ip', 'unknown'),
...@@ -127,7 +135,8 @@ def task_track(request_info, task_info, event_type, event, page=None): ...@@ -127,7 +135,8 @@ def task_track(request_info, task_info, event_type, event, page=None):
"agent": request_info.get('agent', 'unknown'), "agent": request_info.get('agent', 'unknown'),
"page": page, "page": page,
"time": datetime.datetime.now(UTC), "time": datetime.datetime.now(UTC),
"host": request_info.get('host', 'unknown') "host": request_info.get('host', 'unknown'),
"context": eventtracker.get_tracker().resolve_context(),
} }
log_event(event) log_event(event)
......
...@@ -47,16 +47,9 @@ def event_is_emitted(_step, event_type, event_source): ...@@ -47,16 +47,9 @@ def event_is_emitted(_step, event_type, event_source):
event = cursor.next() event = cursor.next()
# These fields should be present in the event, but we won't bother
# validating them since it is difficult to predict their values.
for expected_field in ['host', 'time', 'agent', 'ip', 'event_source', 'event', 'page']:
assert_in(expected_field, event, msg='Expected field {} not found in event'.format(expected_field))
expected_field_values = { expected_field_values = {
"username": world.scenario_dict['USER'].username, "username": world.scenario_dict['USER'].username,
"event_type": event_type, "event_type": event_type,
} }
for key, value in expected_field_values.iteritems(): for key, value in expected_field_values.iteritems():
assert_equals(event[key], value) assert_equals(event[key], value)
# Note that the event may contain other fields, which is fine!
...@@ -357,6 +357,7 @@ if MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'): ...@@ -357,6 +357,7 @@ if MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
# We're already logging events, and we don't want to capture user # We're already logging events, and we don't want to capture user
# names/passwords. Heartbeat events are likely not interesting. # names/passwords. Heartbeat events are likely not interesting.
TRACKING_IGNORE_URL_PATTERNS = [r'^/event', r'^/login', r'^/heartbeat'] TRACKING_IGNORE_URL_PATTERNS = [r'^/event', r'^/login', r'^/heartbeat']
TRACKING_ENABLED = True
######################## subdomain specific settings ########################### ######################## subdomain specific settings ###########################
COURSE_LISTINGS = {} COURSE_LISTINGS = {}
...@@ -882,6 +883,7 @@ INSTALLED_APPS = ( ...@@ -882,6 +883,7 @@ INSTALLED_APPS = (
'static_template_view', 'static_template_view',
'staticbook', 'staticbook',
'track', 'track',
'eventtracking.django',
'util', 'util',
'certificates', 'certificates',
'instructor', 'instructor',
......
...@@ -20,3 +20,4 @@ ...@@ -20,3 +20,4 @@
-e git+https://github.com/edx/diff-cover.git@v0.2.6#egg=diff_cover -e git+https://github.com/edx/diff-cover.git@v0.2.6#egg=diff_cover
-e git+https://github.com/edx/js-test-tool.git@v0.1.3#egg=js_test_tool -e git+https://github.com/edx/js-test-tool.git@v0.1.3#egg=js_test_tool
-e git+https://github.com/edx/django-waffle.git@823a102e48#egg=django-waffle -e git+https://github.com/edx/django-waffle.git@823a102e48#egg=django-waffle
-e git+https://github.com/edx/event-tracking.git@f0211d702d#egg=event-tracking
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