Commit 36269bd5 by Gabe Mulley

Integrate event-tracking in to the LMS

Support incremental conversion of events from the old API to the new, in order to ensure the new system is working, enrollment events have been modified to make use of the new API.
parent 6ea681f4
...@@ -251,6 +251,7 @@ BROKER_URL = "{0}://{1}:{2}@{3}/{4}".format(CELERY_BROKER_TRANSPORT, ...@@ -251,6 +251,7 @@ BROKER_URL = "{0}://{1}:{2}@{3}/{4}".format(CELERY_BROKER_TRANSPORT,
# Event tracking # Event tracking
TRACKING_BACKENDS.update(AUTH_TOKENS.get("TRACKING_BACKENDS", {})) TRACKING_BACKENDS.update(AUTH_TOKENS.get("TRACKING_BACKENDS", {}))
EVENT_TRACKING_BACKENDS.update(AUTH_TOKENS.get("EVENT_TRACKING_BACKENDS", {}))
SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {}) SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {})
VIRTUAL_UNIVERSITIES = ENV_TOKENS.get('VIRTUAL_UNIVERSITIES', []) VIRTUAL_UNIVERSITIES = ENV_TOKENS.get('VIRTUAL_UNIVERSITIES', [])
......
...@@ -546,7 +546,7 @@ COURSES_WITH_UNSAFE_CODE = [] ...@@ -546,7 +546,7 @@ COURSES_WITH_UNSAFE_CODE = []
############################## EVENT TRACKING ################################# ############################## EVENT TRACKING #################################
TRACK_MAX_EVENT = 10000 TRACK_MAX_EVENT = 50000
TRACKING_BACKENDS = { TRACKING_BACKENDS = {
'logger': { 'logger': {
...@@ -557,6 +557,26 @@ TRACKING_BACKENDS = { ...@@ -557,6 +557,26 @@ TRACKING_BACKENDS = {
} }
} }
# We're already logging events, and we don't want to capture user
# names/passwords. Heartbeat events are likely not interesting.
TRACKING_IGNORE_URL_PATTERNS = [r'^/event', r'^/login', r'^/heartbeat']
EVENT_TRACKING_ENABLED = True
EVENT_TRACKING_BACKENDS = {
'logger': {
'ENGINE': 'eventtracking.backends.logger.LoggerBackend',
'OPTIONS': {
'name': 'tracking',
'max_event_size': TRACK_MAX_EVENT,
}
}
}
EVENT_TRACKING_PROCESSORS = [
{
'ENGINE': 'track.shim.LegacyFieldMappingProcessor'
}
]
#### PASSWORD POLICY SETTINGS ##### #### PASSWORD POLICY SETTINGS #####
PASSWORD_MIN_LENGTH = None PASSWORD_MIN_LENGTH = None
...@@ -565,11 +585,6 @@ PASSWORD_COMPLEXITY = {} ...@@ -565,11 +585,6 @@ PASSWORD_COMPLEXITY = {}
PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD = None PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD = None
PASSWORD_DICTIONARY = [] PASSWORD_DICTIONARY = []
# We're already logging events, and we don't want to capture user
# names/passwords. Heartbeat events are likely not interesting.
TRACKING_IGNORE_URL_PATTERNS = [r'^/event', r'^/login', r'^/heartbeat']
TRACKING_ENABLED = True
##### ACCOUNT LOCKOUT DEFAULT PARAMETERS ##### ##### ACCOUNT LOCKOUT DEFAULT PARAMETERS #####
MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED = 5 MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED = 5
MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60 MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60
......
...@@ -10,7 +10,6 @@ file and check it in at the same time as your model changes. To do that, ...@@ -10,7 +10,6 @@ file and check it in at the same time as your model changes. To do that,
2. ./manage.py lms schemamigration student --auto description_of_your_change 2. ./manage.py lms schemamigration student --auto description_of_your_change
3. Add the migration file created in edx-platform/common/djangoapps/student/migrations/ 3. Add the migration file created in edx-platform/common/djangoapps/student/migrations/
""" """
import crum
from datetime import datetime, timedelta from datetime import datetime, timedelta
import hashlib import hashlib
import json import json
...@@ -34,7 +33,6 @@ from django.core.exceptions import ObjectDoesNotExist ...@@ -34,7 +33,6 @@ from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext_noop from django.utils.translation import ugettext_noop
from django_countries import CountryField from django_countries import CountryField
from track import contexts from track import contexts
from track.views import server_track
from eventtracking import tracker from eventtracking import tracker
from importlib import import_module from importlib import import_module
...@@ -718,7 +716,7 @@ class CourseEnrollment(models.Model): ...@@ -718,7 +716,7 @@ class CourseEnrollment(models.Model):
} }
with tracker.get_tracker().context(event_name, context): with tracker.get_tracker().context(event_name, context):
server_track(crum.get_current_request(), event_name, data) tracker.emit(event_name, data)
except: # pylint: disable=bare-except except: # pylint: disable=bare-except
if event_name and self.course_id: 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) log.exception('Unable to emit event %s for user %s and course %s', event_name, self.user.username, self.course_id)
......
...@@ -21,7 +21,7 @@ from xmodule.modulestore.tests.factories import CourseFactory ...@@ -21,7 +21,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE
from mock import Mock, patch, sentinel from mock import Mock, patch
from student.models import anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment, unique_id_for_user from student.models import anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment, unique_id_for_user
from student.views import (process_survey_link, _cert_info, from student.views import (process_survey_link, _cert_info,
...@@ -192,15 +192,10 @@ class EnrollInCourseTest(TestCase): ...@@ -192,15 +192,10 @@ class EnrollInCourseTest(TestCase):
"""Tests enrolling and unenrolling in courses.""" """Tests enrolling and unenrolling in courses."""
def setUp(self): def setUp(self):
patcher = patch('student.models.server_track') patcher = patch('student.models.tracker')
self.mock_server_track = patcher.start() self.mock_tracker = patcher.start()
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
crum_patcher = patch('student.models.crum.get_current_request')
self.mock_get_current_request = crum_patcher.start()
self.addCleanup(crum_patcher.stop)
self.mock_get_current_request.return_value = sentinel.request
def test_enrollment(self): def test_enrollment(self):
user = User.objects.create_user("joe", "joe@joe.com", "password") user = User.objects.create_user("joe", "joe@joe.com", "password")
course_id = "edX/Test101/2013" course_id = "edX/Test101/2013"
...@@ -254,13 +249,12 @@ class EnrollInCourseTest(TestCase): ...@@ -254,13 +249,12 @@ class EnrollInCourseTest(TestCase):
def assert_no_events_were_emitted(self): def assert_no_events_were_emitted(self):
"""Ensures no events were emitted since the last event related assertion""" """Ensures no events were emitted since the last event related assertion"""
self.assertFalse(self.mock_server_track.called) self.assertFalse(self.mock_tracker.emit.called) # pylint: disable=maybe-no-member
self.mock_server_track.reset_mock() self.mock_tracker.reset_mock()
def assert_enrollment_event_was_emitted(self, user, course_id): def assert_enrollment_event_was_emitted(self, user, course_id):
"""Ensures an enrollment event was emitted since the last event related assertion""" """Ensures an enrollment event was emitted since the last event related assertion"""
self.mock_server_track.assert_called_once_with( self.mock_tracker.emit.assert_called_once_with( # pylint: disable=maybe-no-member
sentinel.request,
'edx.course.enrollment.activated', 'edx.course.enrollment.activated',
{ {
'course_id': course_id, 'course_id': course_id,
...@@ -268,12 +262,11 @@ class EnrollInCourseTest(TestCase): ...@@ -268,12 +262,11 @@ class EnrollInCourseTest(TestCase):
'mode': 'honor' 'mode': 'honor'
} }
) )
self.mock_server_track.reset_mock() self.mock_tracker.reset_mock()
def assert_unenrollment_event_was_emitted(self, user, course_id): def assert_unenrollment_event_was_emitted(self, user, course_id):
"""Ensures an unenrollment event was emitted since the last event related assertion""" """Ensures an unenrollment event was emitted since the last event related assertion"""
self.mock_server_track.assert_called_once_with( self.mock_tracker.emit.assert_called_once_with( # pylint: disable=maybe-no-member
sentinel.request,
'edx.course.enrollment.deactivated', 'edx.course.enrollment.deactivated',
{ {
'course_id': course_id, 'course_id': course_id,
...@@ -281,7 +274,7 @@ class EnrollInCourseTest(TestCase): ...@@ -281,7 +274,7 @@ class EnrollInCourseTest(TestCase):
'mode': 'honor' 'mode': 'honor'
} }
) )
self.mock_server_track.reset_mock() self.mock_tracker.reset_mock()
def test_enrollment_non_existent_user(self): def test_enrollment_non_existent_user(self):
# Testing enrollment of newly unsaved user (i.e. no database entry) # Testing enrollment of newly unsaved user (i.e. no database entry)
...@@ -445,8 +438,8 @@ class AnonymousLookupTable(TestCase): ...@@ -445,8 +438,8 @@ class AnonymousLookupTable(TestCase):
mode_slug='honor', mode_slug='honor',
mode_display_name='Honor Code', mode_display_name='Honor Code',
) )
patcher = patch('student.models.server_track') patcher = patch('student.models.tracker')
self.mock_server_track = patcher.start() patcher.start()
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
def test_for_unregistered_user(self): # same path as for logged out user def test_for_unregistered_user(self): # same path as for logged out user
......
...@@ -12,6 +12,12 @@ from eventtracking import tracker ...@@ -12,6 +12,12 @@ from eventtracking import tracker
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
CONTEXT_NAME = 'edx.request' CONTEXT_NAME = 'edx.request'
META_KEY_TO_CONTEXT_KEY = {
'REMOTE_ADDR': 'ip',
'SERVER_NAME': 'host',
'HTTP_USER_AGENT': 'agent',
'PATH_INFO': 'path'
}
class TrackMiddleware(object): class TrackMiddleware(object):
...@@ -78,26 +84,58 @@ class TrackMiddleware(object): ...@@ -78,26 +84,58 @@ class TrackMiddleware(object):
""" """
Extract information from the request and add it to the tracking Extract information from the request and add it to the tracking
context. context.
The following fields are injected in to the context:
* session - The Django session key that identifies the user's session.
* user_id - The numeric ID for the logged in user.
* username - The username of the logged in user.
* ip - The IP address of the client.
* 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.
""" """
context = {} context = {
'session': self.get_session_key(request),
'user_id': self.get_user_primary_key(request),
'username': self.get_username(request),
}
for header_name, context_key in META_KEY_TO_CONTEXT_KEY.iteritems():
context[context_key] = request.META.get(header_name, '')
context.update(contexts.course_context_from_url(request.build_absolute_uri())) context.update(contexts.course_context_from_url(request.build_absolute_uri()))
try:
context['user_id'] = request.user.pk
except AttributeError:
context['user_id'] = ''
if settings.DEBUG:
log.error('Cannot determine primary key of logged in user.')
tracker.get_tracker().enter_context( tracker.get_tracker().enter_context(
CONTEXT_NAME, CONTEXT_NAME,
context context
) )
def process_response(self, request, response): # pylint: disable=unused-argument def get_session_key(self, request):
"""Gets the Django session key from the request or an empty string if it isn't found"""
try:
return request.session.session_key
except AttributeError:
return ''
def get_user_primary_key(self, request):
"""Gets the primary key of the logged in Django user"""
try:
return request.user.pk
except AttributeError:
return ''
def get_username(self, request):
"""Gets the username of the logged in Django user"""
try:
return request.user.username
except AttributeError:
return ''
def process_response(self, _request, response):
"""Exit the context if it exists.""" """Exit the context if it exists."""
try: try:
tracker.get_tracker().exit_context(CONTEXT_NAME) tracker.get_tracker().exit_context(CONTEXT_NAME)
except: # pylint: disable=bare-except except Exception: # pylint: disable=broad-except
pass pass
return response return response
"""Map new event context values to old top-level field values. Ensures events can be parsed by legacy parsers."""
CONTEXT_FIELDS_TO_INCLUDE = [
'username',
'session',
'ip',
'agent',
'host'
]
class LegacyFieldMappingProcessor(object):
"""Ensures all required fields are included in emitted events"""
def __call__(self, event):
if 'context' in event:
context = event['context']
for field in CONTEXT_FIELDS_TO_INCLUDE:
if field in context:
event[field] = context[field]
del context[field]
else:
event[field] = ''
if 'event_type' in event.get('context', {}):
event['event_type'] = event['context']['event_type']
del event['context']['event_type']
else:
event['event_type'] = event.get('name', '')
if 'data' in event:
event['event'] = event['data']
del event['data']
else:
event['event'] = {}
if 'timestamp' in event:
event['time'] = event['timestamp']
del event['timestamp']
event['event_source'] = 'server'
event['page'] = None
import re import re
from mock import patch from mock import patch
from mock import sentinel
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase 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
...@@ -50,35 +52,86 @@ class TrackMiddlewareTestCase(TestCase): ...@@ -50,35 +52,86 @@ class TrackMiddlewareTestCase(TestCase):
self.track_middleware.process_request(request) self.track_middleware.process_request(request)
self.assertFalse(self.mock_server_track.called) self.assertFalse(self.mock_server_track.called)
def test_request_in_course_context(self): def test_default_request_context(self):
request = self.request_factory.get('/courses/test_org/test_course/test_run/foo') context = self.get_context_for_path('/courses/')
self.assertEquals(context, {
'user_id': '',
'session': '',
'username': '',
'ip': '127.0.0.1',
'host': 'testserver',
'agent': '',
'path': '/courses/',
'org_id': '',
'course_id': '',
})
def get_context_for_path(self, path):
"""Extract the generated event tracking context for a given request for the given path."""
request = self.request_factory.get(path)
return self.get_context_for_request(request)
def get_context_for_request(self, request):
"""Extract the generated event tracking context for the given request."""
self.track_middleware.process_request(request) self.track_middleware.process_request(request)
captured_context = tracker.get_tracker().resolve_context() try:
self.track_middleware.process_response(request, None) captured_context = tracker.get_tracker().resolve_context()
finally:
self.track_middleware.process_response(request, None)
self.assertEquals( self.assertEquals(
captured_context,
{
'course_id': 'test_org/test_course/test_run',
'org_id': 'test_org',
'user_id': ''
}
)
self.assertEquals(
tracker.get_tracker().resolve_context(), tracker.get_tracker().resolve_context(),
{} {}
) )
return captured_context
def test_request_in_course_context(self):
captured_context = self.get_context_for_path('/courses/test_org/test_course/test_run/foo')
expected_context_subset = {
'course_id': 'test_org/test_course/test_run',
'org_id': 'test_org',
}
self.assert_dict_subset(captured_context, expected_context_subset)
def assert_dict_subset(self, superset, subset):
"""Assert that the superset dict contains all of the key-value pairs found in the subset dict."""
for key, expected_value in subset.iteritems():
self.assertEquals(superset[key], expected_value)
def test_request_with_user(self): def test_request_with_user(self):
user_id = 1
username = sentinel.username
request = self.request_factory.get('/courses/') request = self.request_factory.get('/courses/')
request.user = User(pk=1) request.user = User(pk=user_id, username=username)
self.track_middleware.process_request(request)
self.addCleanup(self.track_middleware.process_response, request, None) context = self.get_context_for_request(request)
self.assertEquals( self.assert_dict_subset(context, {
tracker.get_tracker().resolve_context(), 'user_id': user_id,
{ 'username': username,
'course_id': '', })
'org_id': '',
'user_id': 1 def test_request_with_session(self):
} request = self.request_factory.get('/courses/')
) SessionMiddleware().process_request(request)
request.session.save()
session_key = request.session.session_key
context = self.get_context_for_request(request)
self.assert_dict_subset(context, {
'session': session_key,
})
def test_request_headers(self):
ip_address = '10.0.0.0'
user_agent = 'UnitTest/1.0'
factory = RequestFactory(REMOTE_ADDR=ip_address, HTTP_USER_AGENT=user_agent)
request = factory.get('/some-path')
context = self.get_context_for_request(request)
self.assert_dict_subset(context, {
'ip': ip_address,
'agent': user_agent,
})
"""Ensure emitted events contain the fields legacy processors expect to find."""
from datetime import datetime
from freezegun import freeze_time
from mock import sentinel
from django.test import TestCase
from django.test.utils import override_settings
from pytz import UTC
from eventtracking.django import DjangoTracker
IN_MEMORY_BACKEND = {
'mem': {
'ENGINE': 'track.tests.test_shim.InMemoryBackend'
}
}
LEGACY_SHIM_PROCESSOR = [
{
'ENGINE': 'track.shim.LegacyFieldMappingProcessor'
}
]
FROZEN_TIME = datetime(2013, 10, 3, 8, 24, 55, tzinfo=UTC)
@freeze_time(FROZEN_TIME)
class LegacyFieldMappingProcessorTestCase(TestCase):
"""Ensure emitted events contain the fields legacy processors expect to find."""
@override_settings(
EVENT_TRACKING_BACKENDS=IN_MEMORY_BACKEND,
EVENT_TRACKING_PROCESSORS=LEGACY_SHIM_PROCESSOR,
)
def test_event_field_mapping(self):
django_tracker = DjangoTracker()
data = {sentinel.key: sentinel.value}
context = {
'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,
'event_type': sentinel.event_type,
}
with django_tracker.context('test', context):
django_tracker.emit(sentinel.name, data)
emitted_event = django_tracker.backends['mem'].get_event()
expected_event = {
'event_type': sentinel.event_type,
'name': sentinel.name,
'context': {
'user_id': sentinel.user_id,
'course_id': sentinel.course_id,
'org_id': sentinel.org_id,
'path': sentinel.path,
},
'event': data,
'username': sentinel.username,
'event_source': 'server',
'time': FROZEN_TIME,
'agent': sentinel.agent,
'host': sentinel.host,
'ip': sentinel.ip,
'page': None,
'session': sentinel.session,
}
self.assertEqual(expected_event, emitted_event)
@override_settings(
EVENT_TRACKING_BACKENDS=IN_MEMORY_BACKEND,
EVENT_TRACKING_PROCESSORS=LEGACY_SHIM_PROCESSOR,
)
def test_missing_fields(self):
django_tracker = DjangoTracker()
django_tracker.emit(sentinel.name)
emitted_event = django_tracker.backends['mem'].get_event()
expected_event = {
'event_type': sentinel.name,
'name': sentinel.name,
'context': {},
'event': {},
'username': '',
'event_source': 'server',
'time': FROZEN_TIME,
'agent': '',
'host': '',
'ip': '',
'page': None,
'session': '',
}
self.assertEqual(expected_event, emitted_event)
class InMemoryBackend(object):
"""A backend that simply stores all events in memory"""
def __init__(self):
super(InMemoryBackend, self).__init__()
self.events = []
def send(self, event):
"""Store the event in a list"""
self.events.append(event)
def get_event(self):
"""Return the first event that was emitted."""
return self.events[0]
...@@ -7,6 +7,18 @@ from pymongo import MongoClient ...@@ -7,6 +7,18 @@ from pymongo import MongoClient
from nose.tools import assert_equals from nose.tools import assert_equals
from nose.tools import assert_in from nose.tools import assert_in
REQUIRED_EVENT_FIELDS = [
'agent',
'event',
'event_source',
'event_type',
'host',
'ip',
'page',
'time',
'username'
]
@before.all @before.all
def connect_to_mongodb(): def connect_to_mongodb():
...@@ -53,3 +65,6 @@ def event_is_emitted(_step, event_type, event_source): ...@@ -53,3 +65,6 @@ def event_is_emitted(_step, event_type, event_source):
} }
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)
for field in REQUIRED_EVENT_FIELDS:
assert_in(field, event)
...@@ -342,13 +342,9 @@ class CertificateItemTest(ModuleStoreTestCase): ...@@ -342,13 +342,9 @@ class CertificateItemTest(ModuleStoreTestCase):
min_price=self.cost) min_price=self.cost)
course_mode.save() course_mode.save()
patcher = patch('student.models.server_track') patcher = patch('student.models.tracker')
self.mock_server_track = patcher.start() self.mock_tracker = patcher.start()
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
crum_patcher = patch('student.models.crum.get_current_request')
self.mock_get_current_request = crum_patcher.start()
self.addCleanup(crum_patcher.stop)
self.mock_get_current_request.return_value = sentinel.request
def test_existing_enrollment(self): def test_existing_enrollment(self):
CourseEnrollment.enroll(self.user, self.course_id) CourseEnrollment.enroll(self.user, self.course_id)
...@@ -356,7 +352,7 @@ class CertificateItemTest(ModuleStoreTestCase): ...@@ -356,7 +352,7 @@ class CertificateItemTest(ModuleStoreTestCase):
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified') CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified')
# verify that we are still enrolled # verify that we are still enrolled
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_id)) self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_id))
self.mock_server_track.reset_mock() self.mock_tracker.reset_mock()
cart.purchase() cart.purchase()
enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_id) enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_id)
self.assertEquals(enrollment.mode, u'verified') self.assertEquals(enrollment.mode, u'verified')
......
...@@ -40,8 +40,8 @@ postpay_mock = Mock() ...@@ -40,8 +40,8 @@ postpay_mock = Mock()
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class ShoppingCartViewsTests(ModuleStoreTestCase): class ShoppingCartViewsTests(ModuleStoreTestCase):
def setUp(self): def setUp(self):
patcher = patch('student.models.server_track') patcher = patch('student.models.tracker')
self.mock_server_track = patcher.start() self.mock_tracker = patcher.start()
self.user = UserFactory.create() self.user = UserFactory.create()
self.user.set_password('password') self.user.set_password('password')
self.user.save() self.user.save()
...@@ -221,7 +221,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -221,7 +221,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
s['attempting_upgrade'] = True s['attempting_upgrade'] = True
s.save() s.save()
self.mock_server_track.reset_mock() self.mock_tracker.emit.reset_mock() # pylint: disable=maybe-no-member
resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id]))
# Once they've upgraded, they're no longer *attempting* to upgrade # Once they've upgraded, they're no longer *attempting* to upgrade
...@@ -246,8 +246,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -246,8 +246,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
course_enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_id) course_enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_id)
course_enrollment.emit_event('edx.course.enrollment.upgrade.succeeded') course_enrollment.emit_event('edx.course.enrollment.upgrade.succeeded')
self.mock_server_track.assert_any_call( self.mock_tracker.emit.assert_any_call( # pylint: disable=maybe-no-member
None,
'edx.course.enrollment.upgrade.succeeded', 'edx.course.enrollment.upgrade.succeeded',
{ {
'user_id': course_enrollment.user.id, 'user_id': course_enrollment.user.id,
......
...@@ -21,8 +21,6 @@ from django.conf import settings ...@@ -21,8 +21,6 @@ from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from mock import sentinel
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
...@@ -133,15 +131,10 @@ class TestMidCourseReverifyView(TestCase): ...@@ -133,15 +131,10 @@ class TestMidCourseReverifyView(TestCase):
self.course_id = 'Robot/999/Test_Course' self.course_id = 'Robot/999/Test_Course'
CourseFactory.create(org='Robot', number='999', display_name='Test Course') CourseFactory.create(org='Robot', number='999', display_name='Test Course')
patcher = patch('student.models.server_track') patcher = patch('student.models.tracker')
self.mock_server_track = patcher.start() self.mock_tracker = patcher.start()
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
crum_patcher = patch('student.models.crum.get_current_request')
self.mock_get_current_request = crum_patcher.start()
self.addCleanup(crum_patcher.stop)
self.mock_get_current_request.return_value = sentinel.request
@patch('verify_student.views.render_to_response', render_mock) @patch('verify_student.views.render_to_response', render_mock)
def test_midcourse_reverify_get(self): def test_midcourse_reverify_get(self):
url = reverse('verify_student_midcourse_reverify', url = reverse('verify_student_midcourse_reverify',
...@@ -149,8 +142,7 @@ class TestMidCourseReverifyView(TestCase): ...@@ -149,8 +142,7 @@ class TestMidCourseReverifyView(TestCase):
response = self.client.get(url) response = self.client.get(url)
# Check that user entering the reverify flow was logged # Check that user entering the reverify flow was logged
self.mock_server_track.assert_called_once_with( self.mock_tracker.emit.assert_called_once_with( # pylint: disable=maybe-no-member
sentinel.request,
'edx.course.enrollment.reverify.started', 'edx.course.enrollment.reverify.started',
{ {
'user_id': self.user.id, 'user_id': self.user.id,
...@@ -158,7 +150,7 @@ class TestMidCourseReverifyView(TestCase): ...@@ -158,7 +150,7 @@ class TestMidCourseReverifyView(TestCase):
'mode': "verified", 'mode': "verified",
} }
) )
self.mock_server_track.reset_mock() self.mock_tracker.emit.reset_mock() # pylint: disable=maybe-no-member
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
((_template, context), _kwargs) = render_mock.call_args ((_template, context), _kwargs) = render_mock.call_args
...@@ -172,8 +164,7 @@ class TestMidCourseReverifyView(TestCase): ...@@ -172,8 +164,7 @@ class TestMidCourseReverifyView(TestCase):
response = self.client.post(url, {'face_image': ','}) response = self.client.post(url, {'face_image': ','})
# Check that submission event was logged # Check that submission event was logged
self.mock_server_track.assert_called_once_with( self.mock_tracker.emit.assert_called_once_with( # pylint: disable=maybe-no-member
sentinel.request,
'edx.course.enrollment.reverify.submitted', 'edx.course.enrollment.reverify.submitted',
{ {
'user_id': self.user.id, 'user_id': self.user.id,
...@@ -181,7 +172,7 @@ class TestMidCourseReverifyView(TestCase): ...@@ -181,7 +172,7 @@ class TestMidCourseReverifyView(TestCase):
'mode': "verified", 'mode': "verified",
} }
) )
self.mock_server_track.reset_mock() self.mock_tracker.emit.reset_mock() # pylint: disable=maybe-no-member
self.assertEquals(response.status_code, 302) self.assertEquals(response.status_code, 302)
try: try:
......
...@@ -88,6 +88,15 @@ TRACKING_BACKENDS.update({ ...@@ -88,6 +88,15 @@ TRACKING_BACKENDS.update({
} }
}) })
EVENT_TRACKING_BACKENDS.update({
'mongo': {
'ENGINE': 'eventtracking.backends.mongodb.MongoBackend',
'OPTIONS': {
'database': 'track'
}
}
})
# Enable asset pipeline # Enable asset pipeline
# Our fork of django-pipeline uses `PIPELINE` instead of `PIPELINE_ENABLED` # Our fork of django-pipeline uses `PIPELINE` instead of `PIPELINE_ENABLED`
......
...@@ -359,6 +359,7 @@ STUDENT_FILEUPLOAD_MAX_SIZE = ENV_TOKENS.get("STUDENT_FILEUPLOAD_MAX_SIZE", STUD ...@@ -359,6 +359,7 @@ STUDENT_FILEUPLOAD_MAX_SIZE = ENV_TOKENS.get("STUDENT_FILEUPLOAD_MAX_SIZE", STUD
# Event tracking # Event tracking
TRACKING_BACKENDS.update(AUTH_TOKENS.get("TRACKING_BACKENDS", {})) TRACKING_BACKENDS.update(AUTH_TOKENS.get("TRACKING_BACKENDS", {}))
EVENT_TRACKING_BACKENDS.update(AUTH_TOKENS.get("EVENT_TRACKING_BACKENDS", {}))
# Student identity verification settings # Student identity verification settings
VERIFY_STUDENT = AUTH_TOKENS.get("VERIFY_STUDENT", VERIFY_STUDENT) VERIFY_STUDENT = AUTH_TOKENS.get("VERIFY_STUDENT", VERIFY_STUDENT)
......
...@@ -394,7 +394,7 @@ LMS_MIGRATION_ALLOWED_IPS = [] ...@@ -394,7 +394,7 @@ LMS_MIGRATION_ALLOWED_IPS = []
############################## EVENT TRACKING ################################# ############################## EVENT TRACKING #################################
# FIXME: Should we be doing this truncation? # FIXME: Should we be doing this truncation?
TRACK_MAX_EVENT = 10000 TRACK_MAX_EVENT = 50000
DEBUG_TRACK_LOG = False DEBUG_TRACK_LOG = False
...@@ -407,19 +407,39 @@ TRACKING_BACKENDS = { ...@@ -407,19 +407,39 @@ TRACKING_BACKENDS = {
} }
} }
# We're already logging events, and we don't want to capture user
# names/passwords. Heartbeat events are likely not interesting.
TRACKING_IGNORE_URL_PATTERNS = [r'^/event', r'^/login', r'^/heartbeat']
EVENT_TRACKING_ENABLED = True
EVENT_TRACKING_BACKENDS = {
'logger': {
'ENGINE': 'eventtracking.backends.logger.LoggerBackend',
'OPTIONS': {
'name': 'tracking',
'max_event_size': TRACK_MAX_EVENT,
}
}
}
EVENT_TRACKING_PROCESSORS = [
{
'ENGINE': 'track.shim.LegacyFieldMappingProcessor'
}
]
# Backwards compatibility with ENABLE_SQL_TRACKING_LOGS feature flag. # Backwards compatibility with ENABLE_SQL_TRACKING_LOGS feature flag.
# In the future, adding the backend to TRACKING_BACKENDS enough. # In the future, adding the backend to TRACKING_BACKENDS should be enough.
if FEATURES.get('ENABLE_SQL_TRACKING_LOGS'): if FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
TRACKING_BACKENDS.update({ TRACKING_BACKENDS.update({
'sql': { 'sql': {
'ENGINE': 'track.backends.django.DjangoBackend' 'ENGINE': 'track.backends.django.DjangoBackend'
} }
}) })
EVENT_TRACKING_BACKENDS.update({
# We're already logging events, and we don't want to capture user 'sql': {
# names/passwords. Heartbeat events are likely not interesting. 'ENGINE': 'track.backends.django.DjangoBackend'
TRACKING_IGNORE_URL_PATTERNS = [r'^/event', r'^/login', r'^/heartbeat'] }
TRACKING_ENABLED = True })
######################## GOOGLE ANALYTICS ########################### ######################## GOOGLE ANALYTICS ###########################
GOOGLE_ANALYTICS_ACCOUNT = 'GOOGLE_ANALYTICS_ACCOUNT_DUMMY' GOOGLE_ANALYTICS_ACCOUNT = 'GOOGLE_ANALYTICS_ACCOUNT_DUMMY'
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
-e git+https://github.com/edx/diff-cover.git@v0.2.9#egg=diff_cover -e git+https://github.com/edx/diff-cover.git@v0.2.9#egg=diff_cover
-e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool -e git+https://github.com/edx/js-test-tool.git@v0.1.5#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 -e git+https://github.com/edx/event-tracking.git@2ee5ace#egg=event-tracking
-e git+https://github.com/edx/bok-choy.git@82b4e82d79b9d4c6d087ebbfa26ea23235728e62#egg=bok_choy -e git+https://github.com/edx/bok-choy.git@82b4e82d79b9d4c6d087ebbfa26ea23235728e62#egg=bok_choy
-e git+https://github.com/edx-solutions/django-splash.git@9965a53c269666a30bb4e2b3f6037c138aef2a55#egg=django-splash -e git+https://github.com/edx-solutions/django-splash.git@9965a53c269666a30bb4e2b3f6037c138aef2a55#egg=django-splash
-e git+https://github.com/edx/acid-block.git@459aff7b63db8f2c5decd1755706c1a64fb4ebb1#egg=acid-xblock -e git+https://github.com/edx/acid-block.git@459aff7b63db8f2c5decd1755706c1a64fb4ebb1#egg=acid-xblock
......
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