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:
Gabe Mulley
2018-11-15 16:13:14 -05:00
committed by Brian Wilson
26 changed files with 394 additions and 274 deletions

View File

@@ -21,7 +21,6 @@ from functools import total_ordering
from importlib import import_module
from urllib import urlencode
import analytics
from config_models.models import ConfigurationModel
from django.apps import apps
from django.conf import settings
@@ -66,7 +65,7 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.xmodule_django.models import NoneToEmptyManager
from openedx.core.djangolib.model_mixins import DeletableByUserValue
from track import contexts
from track import contexts, segment
from util.milestones_helpers import is_entrance_exams_enabled
from util.model_utils import emit_field_changed_events, get_changed_fields_dict
from util.query import use_read_replica_if_available
@@ -739,7 +738,7 @@ class Registration(models.Model):
has_segment_key = getattr(settings, 'LMS_SEGMENT_KEY', None)
has_mailchimp_id = hasattr(settings, 'MAILCHIMP_NEW_USER_LIST_ID')
if has_segment_key and has_mailchimp_id:
identity_args = [
segment.identify(
self.user.id, # pylint: disable=no-member
{
'email': self.user.email,
@@ -751,8 +750,7 @@ class Registration(models.Model):
"listId": settings.MAILCHIMP_NEW_USER_LIST_ID
}
}
]
analytics.identify(*identity_args)
)
class PendingNameChange(DeletableByUserValue, models.Model):
@@ -1429,25 +1427,17 @@ class CourseEnrollment(models.Model):
'course_id': text_type(self.course_id),
'mode': self.mode,
}
segment_properties = {
'category': 'conversion',
'label': text_type(self.course_id),
'org': self.course_id.org,
'course': self.course_id.course,
'run': self.course_id.run,
'mode': self.mode,
}
with tracker.get_tracker().context(event_name, context):
tracker.emit(event_name, data)
if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
tracking_context = tracker.get_tracker().resolve_context()
analytics.track(self.user_id, event_name, {
'category': 'conversion',
'label': text_type(self.course_id),
'org': self.course_id.org,
'course': self.course_id.course,
'run': self.course_id.run,
'mode': self.mode,
}, context={
'ip': tracking_context.get('ip'),
'Google Analytics': {
'clientId': tracking_context.get('client_id')
}
})
segment.track(self.user_id, event_name, segment_properties)
except: # pylint: disable=bare-except
if event_name and self.course_id:

View File

@@ -72,7 +72,7 @@ class TestActivateAccount(TestCase):
LMS_SEGMENT_KEY="testkey",
MAILCHIMP_NEW_USER_LIST_ID="listid"
)
@patch('student.models.analytics.identify')
@patch('student.models.segment.identify')
def test_activation_with_keys(self, mock_segment_identify):
expected_segment_payload = {
'email': self.email,
@@ -98,16 +98,16 @@ class TestActivateAccount(TestCase):
)
@override_settings(LMS_SEGMENT_KEY="testkey")
@patch('student.models.analytics.identify')
@patch('student.models.segment.identify')
def test_activation_without_mailchimp_key(self, mock_segment_identify):
self.assert_no_tracking(mock_segment_identify)
@override_settings(MAILCHIMP_NEW_USER_LIST_ID="listid")
@patch('student.models.analytics.identify')
@patch('student.models.segment.identify')
def test_activation_without_segment_key(self, mock_segment_identify):
self.assert_no_tracking(mock_segment_identify)
@patch('student.models.analytics.identify')
@patch('student.models.segment.identify')
def test_activation_without_keys(self, mock_segment_identify):
self.assert_no_tracking(mock_segment_identify)

View File

@@ -66,7 +66,6 @@ from collections import OrderedDict
from logging import getLogger
from smtplib import SMTPException
import analytics
from django.conf import settings
from django.contrib.auth.models import User
from django.core.mail.message import EmailMessage
@@ -79,12 +78,12 @@ from social_core.pipeline import partial
from social_core.pipeline.social_auth import associate_by_email
from edxmako.shortcuts import render_to_string
from eventtracking import tracker
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_authn import cookies as user_authn_cookies
from lms.djangoapps.verify_student.models import SSOVerification
from lms.djangoapps.verify_student.utils import earliest_allowed_verification_date
from third_party_auth.utils import user_exists
from track import segment
from . import provider
@@ -657,23 +656,12 @@ def login_analytics(strategy, auth_entry, current_partial=None, *args, **kwargs)
elif auth_entry in [AUTH_ENTRY_ACCOUNT_SETTINGS]:
event_name = 'edx.bi.user.account.linked'
if event_name is not None and hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
tracking_context = tracker.get_tracker().resolve_context()
analytics.track(
kwargs['user'].id,
event_name,
{
'category': "conversion",
'label': None,
'provider': kwargs['backend'].name
},
context={
'ip': tracking_context.get('ip'),
'Google Analytics': {
'clientId': tracking_context.get('client_id')
}
}
)
if event_name is not None:
segment.track(kwargs['user'].id, event_name, {
'category': "conversion",
'label': None,
'provider': kwargs['backend'].name
})
@partial.partial

View File

@@ -522,13 +522,13 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
def test_canceling_authentication_redirects_to_root_when_auth_entry_not_set(self):
self.assert_exception_redirect_looks_correct('/')
def test_full_pipeline_succeeds_for_linking_account(self):
@mock.patch('third_party_auth.pipeline.segment.track')
def test_full_pipeline_succeeds_for_linking_account(self, _mock_segment_track):
# First, create, the request and strategy that store pipeline state,
# configure the backend, and mock out wire traffic.
request, strategy = self.get_request_and_strategy(
auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete')
request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy))
pipeline.analytics.track = mock.MagicMock()
request.user = self.create_user_models_for_existing_account(
strategy, 'user@example.com', 'password', self.get_username(), skip_social_auth=True)
@@ -677,13 +677,13 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
self.assert_account_settings_context_looks_correct(
account_settings_context(request), duplicate=True, linked=True)
def test_full_pipeline_succeeds_for_signing_in_to_existing_active_account(self):
@mock.patch('third_party_auth.pipeline.segment.track')
def test_full_pipeline_succeeds_for_signing_in_to_existing_active_account(self, _mock_segment_track):
# First, create, the request and strategy that store pipeline state,
# configure the backend, and mock out wire traffic.
request, strategy = self.get_request_and_strategy(
auth_entry=pipeline.AUTH_ENTRY_LOGIN, redirect_uri='social:complete')
strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy))
pipeline.analytics.track = mock.MagicMock()
user = self.create_user_models_for_existing_account(
strategy, 'user@example.com', 'password', self.get_username())
self.assert_social_auth_exists_for_user(user, strategy)

View 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)

View 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)

View File

@@ -38,13 +38,13 @@ class TestCourseGoalsAPI(EventTrackingTestCase, SharedModuleStoreTestCase):
self.apiUrl = reverse('course_goals_api:v0:course_goal-list')
@mock.patch('lms.djangoapps.course_goals.views.update_google_analytics')
@mock.patch('lms.djangoapps.course_goals.views.segment.track')
@override_settings(LMS_SEGMENT_KEY="foobar")
def test_add_valid_goal(self, ga_call):
""" Ensures a correctly formatted post succeeds."""
response = self.post_course_goal(valid=True, goal_key='certify')
self.assertEqual(self.get_event(-1)['name'], EVENT_NAME_ADDED)
ga_call.assert_called_with(EVENT_NAME_ADDED, self.user.id)
ga_call.assert_called_with(self.user.id, EVENT_NAME_ADDED)
self.assertEqual(response.status_code, 201)
current_goals = CourseGoal.objects.filter(user=self.user, course_key=self.course.id)
@@ -68,7 +68,7 @@ class TestCourseGoalsAPI(EventTrackingTestCase, SharedModuleStoreTestCase):
status_code=400
)
@mock.patch('lms.djangoapps.course_goals.views.update_google_analytics')
@mock.patch('lms.djangoapps.course_goals.views.segment.track')
@override_settings(LMS_SEGMENT_KEY="foobar")
def test_update_goal(self, ga_call):
""" Ensures that repeated course goal post events do not create new instances of the goal. """
@@ -77,7 +77,7 @@ class TestCourseGoalsAPI(EventTrackingTestCase, SharedModuleStoreTestCase):
self.post_course_goal(valid=True, goal_key='unsure')
self.assertEqual(self.get_event(-1)['name'], EVENT_NAME_UPDATED)
ga_call.assert_called_with(EVENT_NAME_UPDATED, self.user.id)
ga_call.assert_called_with(self.user.id, EVENT_NAME_UPDATED)
current_goals = CourseGoal.objects.filter(user=self.user, course_key=self.course.id)
self.assertEqual(len(current_goals), 1)
self.assertEqual(current_goals[0].goal_key, 'unsure')

View File

@@ -2,7 +2,6 @@
Course Goals Views - includes REST API
"""
from __future__ import absolute_import
import analytics
from django.contrib.auth import get_user_model
from django.conf import settings
@@ -17,6 +16,8 @@ from rest_framework import permissions, serializers, viewsets, status
from rest_framework.authentication import SessionAuthentication
from rest_framework.response import Response
from track import segment
from .api import get_course_goal_options
from .models import CourseGoal, GOAL_KEY_CHOICES
@@ -110,6 +111,7 @@ class CourseGoalViewSet(viewsets.ModelViewSet):
@receiver(post_save, sender=CourseGoal, dispatch_uid="emit_course_goals_event")
def emit_course_goal_event(sender, instance, **kwargs):
"""Emit events for both tracking logs and for Segment."""
name = 'edx.course.goal.added' if kwargs.get('created', False) else 'edx.course.goal.updated'
tracker.emit(
name,
@@ -117,21 +119,4 @@ def emit_course_goal_event(sender, instance, **kwargs):
'goal_key': instance.goal_key,
}
)
if settings.LMS_SEGMENT_KEY:
update_google_analytics(name, instance.user.id)
def update_google_analytics(name, user_id):
""" Update student course goal for Google Analytics using Segment. """
tracking_context = tracker.get_tracker().resolve_context()
context = {
'ip': tracking_context.get('ip'),
'Google Analytics': {
'clientId': tracking_context.get('client_id')
}
}
analytics.track(
user_id,
name,
context=context
)
segment.track(instance.user.id, name)

View File

@@ -102,8 +102,9 @@ class FieldOverrideProvider(object):
"""
__metaclass__ = ABCMeta
def __init__(self, user):
def __init__(self, user, fallback_field_data):
self.user = user
self.fallback_field_data = fallback_field_data
@abstractmethod
def get(self, block, name, default): # pragma no cover
@@ -196,7 +197,7 @@ class OverrideFieldData(FieldData):
def __init__(self, user, fallback, providers):
self.fallback = fallback
self.providers = tuple(provider(user) for provider in providers)
self.providers = tuple(provider(user, fallback) for provider in providers)
def get_override(self, block, name):
"""

View File

@@ -2191,9 +2191,9 @@ class GenerateUserCertTests(ModuleStoreTestCase):
def test_user_with_passing_grade(self, mock_is_course_passed):
# If user has above passing grading then json will return cert generating message and
# status valid code
# mocking xqueue and analytics
# mocking xqueue and Segment analytics
analytics_patcher = patch('courseware.views.views.analytics')
analytics_patcher = patch('courseware.views.views.segment')
mock_tracker = analytics_patcher.start()
self.addCleanup(analytics_patcher.stop)
@@ -2211,11 +2211,6 @@ class GenerateUserCertTests(ModuleStoreTestCase):
'category': 'certificates',
'label': unicode(self.course.id)
},
context={
'ip': '127.0.0.1',
'Google Analytics': {'clientId': None}
}
)
mock_tracker.reset_mock()

View File

@@ -7,7 +7,6 @@ import urllib
from collections import OrderedDict, namedtuple
from datetime import datetime
import analytics
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import AnonymousUser, User
@@ -98,6 +97,7 @@ from openedx.features.enterprise_support.api import data_sharing_consent_require
from openedx.features.journals.api import get_journals_context
from shoppingcart.utils import is_shopping_cart_enabled
from student.models import CourseEnrollment, UserTestGroup
from track import segment
from util.cache import cache, cache_if_anonymous
from util.db import outer_atomic
from util.milestones_helpers import get_prerequisite_courses_display
@@ -1429,24 +1429,11 @@ def _track_successful_certificate_generation(user_id, course_id):
None
"""
if settings.LMS_SEGMENT_KEY:
event_name = 'edx.bi.user.certificate.generate'
tracking_context = tracker.get_tracker().resolve_context()
analytics.track(
user_id,
event_name,
{
'category': 'certificates',
'label': text_type(course_id)
},
context={
'ip': tracking_context.get('ip'),
'Google Analytics': {
'clientId': tracking_context.get('client_id')
}
}
)
event_name = 'edx.bi.user.certificate.generate'
segment.track(user_id, event_name, {
'category': 'certificates',
'label': text_type(course_id)
})
@require_http_methods(["GET", "POST"])

View File

@@ -11,7 +11,6 @@ from datetime import datetime, timedelta
from decimal import Decimal
from io import BytesIO
import analytics
import pytz
from boto.exception import BotoServerError # this is a super-class of SESError and catches connection errors
from config_models.models import ConfigurationModel
@@ -37,9 +36,11 @@ from courseware.courses import get_course_by_id
from edxmako.shortcuts import render_to_string
from eventtracking import tracker
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangolib.markup import HTML, Text
from shoppingcart.pdf import PDFInvoice
from student.models import CourseEnrollment, EnrollStatusChange
from student.signals import UNENROLL_DONE
from track import segment
from util.query import use_read_replica_if_available
from xmodule.modulestore.django import modulestore
@@ -520,19 +521,12 @@ class Order(models.Model):
"""
try:
if settings.LMS_SEGMENT_KEY:
tracking_context = tracker.get_tracker().resolve_context()
analytics.track(self.user.id, event_name, {
'orderId': self.id,
'total': str(self.total_cost),
'currency': self.currency,
'products': [item.analytics_data() for item in orderitems]
}, context={
'ip': tracking_context.get('ip'),
'Google Analytics': {
'clientId': tracking_context.get('client_id')
}
})
segment.track(self.user.id, event_name, {
'orderId': self.id,
'total': str(self.total_cost),
'currency': self.currency,
'products': [item.analytics_data() for item in orderitems]
})
except Exception: # pylint: disable=broad-except
# Capturing all exceptions thrown while tracking analytics events. We do not want
@@ -1578,7 +1572,7 @@ class PaidCourseRegistration(OrderItem):
item.unit_cost = cost
item.list_price = cost
item.line_desc = _(u'Registration for Course: {course_name}').format(
course_name=course.display_name_with_default_escaped)
course_name=course.display_name_with_default)
item.currency = currency
order.currency = currency
item.report_comments = item.csv_report_comments
@@ -1618,12 +1612,12 @@ class PaidCourseRegistration(OrderItem):
Generates instructions when the user has purchased a PaidCourseRegistration.
Basically tells the user to visit the dashboard to see their new classes
"""
notification = _(
notification = Text(_(
u"Please visit your {link_start}dashboard{link_end} "
u"to see your new course."
).format(
link_start=u'<a href="{url}">'.format(url=reverse('dashboard')),
link_end=u'</a>',
)).format(
link_start=HTML(u'<a href="{url}">').format(url=reverse('dashboard')),
link_end=HTML(u'</a>'),
)
return self.pk_with_subclass, set([notification])
@@ -1761,7 +1755,7 @@ class CourseRegCodeItem(OrderItem):
item.list_price = cost
item.qty = qty
item.line_desc = _(u'Enrollment codes for Course: {course_name}').format(
course_name=course.display_name_with_default_escaped)
course_name=course.display_name_with_default)
item.currency = currency
order.currency = currency
item.report_comments = item.csv_report_comments

View File

@@ -81,7 +81,7 @@ class OrderTest(ModuleStoreTestCase):
self.cost = 40
# Add mock tracker for event testing.
patcher = patch('shoppingcart.models.analytics')
patcher = patch('shoppingcart.models.segment')
self.mock_tracker = patcher.start()
self.addCleanup(patcher.stop)
@@ -288,7 +288,6 @@ class OrderTest(ModuleStoreTestCase):
}
]
},
context={'ip': None, 'Google Analytics': {'clientId': None}}
)
def test_purchase_item_failure(self):
@@ -862,7 +861,7 @@ class CertificateItemTest(ModuleStoreTestCase):
self.mock_tracker = patcher.start()
self.addCleanup(patcher.stop)
analytics_patcher = patch('shoppingcart.models.analytics')
analytics_patcher = patch('shoppingcart.models.segment')
self.mock_analytics_tracker = analytics_patcher.start()
self.addCleanup(analytics_patcher.stop)
@@ -888,7 +887,6 @@ class CertificateItemTest(ModuleStoreTestCase):
}
]
},
context={'ip': None, 'Google Analytics': {'clientId': None}}
)
def test_existing_enrollment(self):
@@ -912,7 +910,7 @@ class CertificateItemTest(ModuleStoreTestCase):
@override_settings(LMS_SEGMENT_KEY="foobar")
@patch.dict(settings.FEATURES, {'STORE_BILLING_INFO': True})
@patch('lms.djangoapps.course_goals.views.update_google_analytics', Mock(return_value=True))
@patch('lms.djangoapps.course_goals.views.segment.track', Mock(return_value=True))
@patch('student.models.CourseEnrollment.refund_cutoff_date')
def test_refund_cert_callback_no_expiration(self, cutoff_date):
# When there is no expiration date on a verified mode, the user can always get a refund
@@ -951,7 +949,7 @@ class CertificateItemTest(ModuleStoreTestCase):
@override_settings(LMS_SEGMENT_KEY="foobar")
@patch.dict(settings.FEATURES, {'STORE_BILLING_INFO': True})
@patch('lms.djangoapps.course_goals.views.update_google_analytics', Mock(return_value=True))
@patch('lms.djangoapps.course_goals.views.segment.track', Mock(return_value=True))
@patch('student.models.CourseEnrollment.refund_cutoff_date')
def test_refund_cert_callback_before_expiration(self, cutoff_date):
# If the expiration date has not yet passed on a verified mode, the user can be refunded

View File

@@ -8,7 +8,6 @@ import json
import logging
import urllib
import analytics
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.staticfiles.storage import staticfiles_storage
@@ -49,6 +48,7 @@ from openedx.core.lib.log_utils import audit_log
from shoppingcart.models import CertificateItem, Order
from shoppingcart.processors import get_purchase_endpoint, get_signed_purchase_params
from student.models import CourseEnrollment
from track import segment
from util.db import outer_atomic
from util.json_request import JsonResponse
from xmodule.modulestore.django import modulestore
@@ -913,7 +913,7 @@ class SubmitPhotosView(View):
return response
# Submit the attempt
attempt = self._submit_attempt(request.user, face_image, photo_id_image, initial_verification)
self._submit_attempt(request.user, face_image, photo_id_image, initial_verification)
self._fire_event(request.user, "edx.bi.verify.submitted", {"category": "verification"})
self._send_confirmation_email(request.user)
@@ -1096,15 +1096,7 @@ class SubmitPhotosView(View):
Returns: None
"""
if settings.LMS_SEGMENT_KEY:
tracking_context = tracker.get_tracker().resolve_context()
context = {
'ip': tracking_context.get('ip'),
'Google Analytics': {
'clientId': tracking_context.get('client_id')
}
}
analytics.track(user.id, event_name, parameters, context=context)
segment.track(user.id, event_name, parameters)
@require_POST

View File

@@ -2,7 +2,6 @@ import datetime
import logging
import random
import analytics
from django.db.models.signals import post_save
from django.dispatch import receiver
@@ -17,6 +16,7 @@ from openedx.core.djangoapps.schedules.models import ScheduleExperience
from openedx.core.djangoapps.schedules.content_highlights import course_has_highlights
from openedx.core.djangoapps.theming.helpers import get_current_site
from student.models import CourseEnrollment
from track import segment
from .config import CREATE_SCHEDULE_WAFFLE_FLAG
from .models import Schedule, ScheduleConfig
from .tasks import update_course_schedules
@@ -53,7 +53,7 @@ def create_schedule(sender, **kwargs): # pylint: disable=unused-argument
))
def update_schedules_on_course_start_changed(sender, updated_course_overview, previous_start_date, **kwargs):
def update_schedules_on_course_start_changed(sender, updated_course_overview, previous_start_date, **kwargs): # pylint: disable=unused-argument
"""
Updates all course schedules start and upgrade_deadline dates based off of
the new course overview start date.
@@ -136,9 +136,9 @@ def _should_randomly_suppress_schedule_creation(
upgrade_deadline_str = None
if upgrade_deadline:
upgrade_deadline_str = upgrade_deadline.isoformat()
analytics.track(
segment.track(
user_id=enrollment.user.id,
event='edx.bi.schedule.suppressed',
event_name='edx.bi.schedule.suppressed',
properties={
'course_id': unicode(enrollment.course_id),
'experience_type': experience_type,

View File

@@ -97,7 +97,7 @@ class CreateScheduleTests(SharedModuleStoreTestCase):
self.assert_schedule_created(experience_type=ScheduleExperience.EXPERIENCES.course_updates)
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
@patch('analytics.track')
@patch('openedx.core.djangoapps.schedules.signals.segment.track')
@patch('openedx.core.djangoapps.schedules.signals.random.random', return_value=0.2)
@ddt.data(
(0, True),
@@ -121,7 +121,7 @@ class CreateScheduleTests(SharedModuleStoreTestCase):
else:
self.assert_schedule_not_created()
mock_track.assert_called_once()
assert mock_track.call_args[1].get('event') == 'edx.bi.schedule.suppressed'
assert mock_track.call_args[1].get('event_name') == 'edx.bi.schedule.suppressed'
@patch('openedx.core.djangoapps.schedules.signals.log.exception')
@patch('openedx.core.djangoapps.schedules.signals.Schedule.objects.create')

View File

@@ -2,8 +2,6 @@
API for managing user preferences.
"""
import logging
import analytics
from eventtracking import tracker
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
@@ -17,6 +15,7 @@ from pytz import common_timezones, common_timezones_set, country_timezones
from six import text_type
from student.models import User, UserProfile
from track import segment
from ..errors import (
UserAPIInternalError, UserAPIRequestError, UserNotFound, UserNotAuthorized,
PreferenceValidationError, PreferenceUpdateError, CountryCodeError
@@ -284,21 +283,13 @@ def _track_update_email_opt_in(user_id, organization, opt_in):
"""
event_name = 'edx.bi.user.org_email.opted_in' if opt_in else 'edx.bi.user.org_email.opted_out'
tracking_context = tracker.get_tracker().resolve_context()
analytics.track(
segment.track(
user_id,
event_name,
{
'category': 'communication',
'label': organization
},
context={
'ip': tracking_context.get('ip'),
'Google Analytics': {
'clientId': tracking_context.get('client_id')
}
}
)

View File

@@ -6,7 +6,6 @@ Much of this file was broken out from views.py, previous history can be found th
import logging
import analytics
from django.conf import settings
from django.contrib.auth import authenticate, login as django_login
from django.contrib.auth.decorators import login_required
@@ -19,7 +18,6 @@ from django.views.decorators.http import require_http_methods
from ratelimitbackend.exceptions import RateLimitException
from edxmako.shortcuts import render_to_response
from eventtracking import tracker
from openedx.core.djangoapps.user_authn.cookies import set_logged_in_cookies, refresh_jwt_cookies
from openedx.core.djangoapps.user_authn.exceptions import AuthFailedError
import openedx.core.djangoapps.external_auth.views
@@ -34,6 +32,7 @@ from student.models import (
)
from student.views import send_reactivation_email_for_user
from student.forms import send_password_reset_email_for_user
from track import segment
import third_party_auth
from third_party_auth import pipeline, provider
from util.json_request import JsonResponse
@@ -274,37 +273,28 @@ def _track_user_login(user, request):
"""
Sends a tracking event for a successful login.
"""
if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
tracking_context = tracker.get_tracker().resolve_context()
analytics.identify(
user.id,
{
'email': request.POST['email'],
'username': user.username
},
{
# Disable MailChimp because we don't want to update the user's email
# and username in MailChimp on every page load. We only need to capture
# this data on registration/activation.
'MailChimp': False
}
)
analytics.track(
user.id,
"edx.bi.user.account.authenticated",
{
'category': "conversion",
'label': request.POST.get('course_id'),
'provider': None
},
context={
'ip': tracking_context.get('ip'),
'Google Analytics': {
'clientId': tracking_context.get('client_id')
}
}
)
segment.identify(
user.id,
{
'email': request.POST.get('email'),
'username': user.username
},
{
# Disable MailChimp because we don't want to update the user's email
# and username in MailChimp on every page load. We only need to capture
# this data on registration/activation.
'MailChimp': False
}
)
segment.track(
user.id,
"edx.bi.user.account.authenticated",
{
'category': "conversion",
'label': request.POST.get('course_id'),
'provider': None
},
)
@login_required

View File

@@ -6,7 +6,6 @@ import datetime
import json
import logging
import analytics
import dogstats_wrapper as dog_stats_api
from django.conf import settings
from django.contrib.auth import login as django_login
@@ -17,7 +16,6 @@ from django.db import transaction
from django.dispatch import Signal
from django.utils.translation import get_language
from django.utils.translation import ugettext as _
from eventtracking import tracker
# Note that this lives in LMS, so this dependency should be refactored.
from notification_prefs.views import enable_notifications
from pytz import UTC
@@ -44,6 +42,7 @@ from student.models import (
create_comments_service_user,
)
from student.views import compose_and_send_activation_email
from track import segment
import third_party_auth
from third_party_auth import pipeline, provider
from third_party_auth.saml import SAP_SUCCESSFACTORS_SAML_KEY
@@ -316,7 +315,6 @@ def _link_user_to_third_party_provider(
def _track_user_registration(user, profile, params, third_party_provider):
""" Track the user's registration. """
if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
tracking_context = tracker.get_tracker().resolve_context()
identity_args = [
user.id,
{
@@ -332,7 +330,7 @@ def _track_user_registration(user, profile, params, third_party_provider):
'country': text_type(profile.country),
}
]
# Provide additional context only if needed.
if hasattr(settings, 'MAILCHIMP_NEW_USER_LIST_ID'):
identity_args.append({
"MailChimp": {
@@ -340,9 +338,8 @@ def _track_user_registration(user, profile, params, third_party_provider):
}
})
analytics.identify(*identity_args)
analytics.track(
segment.identify(*identity_args)
segment.track(
user.id,
"edx.bi.user.account.registered",
{
@@ -350,12 +347,6 @@ def _track_user_registration(user, profile, params, third_party_provider):
'label': params.get('course_id'),
'provider': third_party_provider.name if third_party_provider else None
},
context={
'ip': tracking_context.get('ip'),
'Google Analytics': {
'clientId': tracking_context.get('client_id')
}
}
)

View File

@@ -181,8 +181,8 @@ class TestCreateAccount(SiteMixin, TestCase):
"Microsites not implemented in this environment"
)
@override_settings(LMS_SEGMENT_KEY="testkey")
@mock.patch('openedx.core.djangoapps.user_authn.views.register.analytics.track')
@mock.patch('openedx.core.djangoapps.user_authn.views.register.analytics.identify')
@mock.patch('openedx.core.djangoapps.user_authn.views.register.segment.track')
@mock.patch('openedx.core.djangoapps.user_authn.views.register.segment.identify')
def test_segment_tracking(self, mock_segment_identify, _):
year = datetime.now().year
year_of_birth = year - 14

View File

@@ -4,7 +4,7 @@ students in the Unlocked Group of the ContentTypeGating partition.
"""
from django.conf import settings
from lms.djangoapps.courseware.field_overrides import FieldOverrideProvider, disable_overrides
from lms.djangoapps.courseware.field_overrides import FieldOverrideProvider
from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID
from openedx.features.course_duration_limits.config import (
CONTENT_TYPE_GATING_FLAG,
@@ -31,9 +31,23 @@ class ContentTypeGatingFieldOverride(FieldOverrideProvider):
if not problem_eligible_for_content_gating:
return default
# Read the group_access from the fallback field-data service
with disable_overrides():
original_group_access = block.group_access
# We want to fetch the value set by course authors since it should take precedence.
# We cannot simply call "block.group_access" to fetch that value even if we disable
# field overrides since it will set the group access field to "dirty" with
# the value read from the course content. Since most content does not have any
# value for this field it will usually be the default empty dict. This field
# override changes the value, however, resulting in the LMS thinking that the
# field data needs to be written back out to the store. This doesn't work,
# however, since this is a read-only setting in the LMS context. After this
# call to get() returns, the _dirty_fields dict will be set correctly to contain
# the value from this field override. This prevents the system from attempting
# to save the overridden value when it thinks it has changed when it hasn't.
original_group_access = None
if self.fallback_field_data.has(block, 'group_access'):
raw_value = self.fallback_field_data.get(block, 'group_access')
group_access_field = block.fields.get('group_access')
if group_access_field is not None:
original_group_access = group_access_field.from_json(raw_value)
if original_group_access is None:
original_group_access = {}

View File

@@ -3,17 +3,17 @@ Test audit user's access to various content based on content-gating features.
"""
import ddt
from django.http import Http404
from django.conf import settings
from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.urls import reverse
from mock import patch
from course_modes.tests.factories import CourseModeFactory
from courseware.access_response import IncorrectPartitionGroupError
from lms.djangoapps.courseware.module_render import load_single_xblock
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from openedx.core.lib.url_utils import quote_slashes
from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID
from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
from student.tests.factories import TEST_PASSWORD, AdminFactory, CourseEnrollmentFactory, UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
@@ -82,14 +82,14 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
cls.graded_score_weight_blocks[(graded, has_score, weight)] = block
# add LTI blocks to default course
cls.lti_block = ItemFactory.create(
cls.blocks_dict['lti_block'] = ItemFactory.create(
parent=cls.blocks_dict['vertical'],
category='lti_consumer',
display_name='lti_consumer',
has_score=True,
graded=True,
)
cls.lti_block_not_scored = ItemFactory.create(
cls.blocks_dict['lti_block_not_scored'] = ItemFactory.create(
parent=cls.blocks_dict['vertical'],
category='lti_consumer',
display_name='lti_consumer_2',
@@ -97,19 +97,32 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
)
# add ungraded problem for xblock_handler test
cls.graded_problem = ItemFactory.create(
cls.blocks_dict['graded_problem'] = ItemFactory.create(
parent=cls.blocks_dict['vertical'],
category='problem',
display_name='graded_problem',
graded=True,
)
cls.ungraded_problem = ItemFactory.create(
cls.blocks_dict['ungraded_problem'] = ItemFactory.create(
parent=cls.blocks_dict['vertical'],
category='problem',
display_name='ungraded_problem',
graded=False,
)
cls.blocks_dict['audit_visible_graded_problem'] = ItemFactory.create(
parent=cls.blocks_dict['vertical'],
category='problem',
display_name='audit_visible_graded_problem',
graded=True,
group_access={
CONTENT_GATING_PARTITION_ID: [
settings.CONTENT_TYPE_GATE_GROUP_IDS['limited_access'],
settings.CONTENT_TYPE_GATE_GROUP_IDS['full_access']
]
},
)
# audit_only course only has an audit track available
cls.courses['audit_only'] = cls._create_course(
run='audit_only_course_run_1',
@@ -205,14 +218,14 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
display_name='Lesson 1 Vertical - Unit 1'
)
for problem_type in component_types:
for component_type in component_types:
block = ItemFactory.create(
parent=blocks_dict['vertical'],
category=problem_type,
display_name=problem_type,
category=component_type,
display_name=component_type,
graded=True,
)
blocks_dict[problem_type] = block
blocks_dict[component_type] = block
return {
'course': course,
@@ -224,14 +237,8 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
"""
Asserts that a block in a specific course is gated for a specific user
This functions asserts whether the passed in block is gated by content type gating.
This is determined by checking whether the has_access method called the IncorrectPartitionGroupError.
This error gets swallowed up and is raised as a 404, which is why we are checking for a 404 being raised.
However, the 404 could also be caused by other errors, which is why the actual assertion is checking
whether the IncorrectPartitionGroupError was called.
Arguments:
block: some soft of xblock descriptor, must implement .scope_ids.usage_id
block: some sort of xblock descriptor, must implement .scope_ids.usage_id
is_gated (bool): if True, this user is expected to be gated from this block
user_id (int): id of user, if not set will be set to self.audit_user.id
course_id (CourseLocator): id of course, if not set will be set to self.course.id
@@ -239,61 +246,52 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
fake_request = self.factory.get('')
mock_get_current_request.return_value = fake_request
with patch.object(IncorrectPartitionGroupError, '__init__',
wraps=IncorrectPartitionGroupError.__init__) as mock_access_error:
if is_gated:
with self.assertRaises(Http404):
load_single_xblock(
request=fake_request,
user_id=user_id,
course_id=unicode(course_id),
usage_key_string=unicode(block.scope_ids.usage_id),
course=None
)
# check that has_access raised the IncorrectPartitionGroupError in order to gate the block
self.assertTrue(mock_access_error.called)
else:
load_single_xblock(
request=fake_request,
user_id=user_id,
course_id=unicode(course_id),
usage_key_string=unicode(block.scope_ids.usage_id),
course=None
)
# check that has_access did not raise the IncorrectPartitionGroupError thereby not gating the block
self.assertFalse(mock_access_error.called)
# Load a block we know will pass access control checks
vertical_xblock = load_single_xblock(
request=fake_request,
user_id=user_id,
course_id=unicode(course_id),
usage_key_string=unicode(self.blocks_dict['vertical'].scope_ids.usage_id),
course=None
)
def test_lti_audit_access(self):
"""
LTI stands for learning tools interoperability and is a 3rd party iframe that pulls in learning content from
outside sources. This tests that audit users cannot see LTI components with graded content but can see the LTI
components which do not have graded content.
"""
self._assert_block_is_gated(
block=self.lti_block,
user_id=self.audit_user.id,
course_id=self.course.id,
is_gated=True
)
self._assert_block_is_gated(
block=self.lti_block_not_scored,
user_id=self.audit_user.id,
course_id=self.course.id,
is_gated=False
)
runtime = vertical_xblock.runtime
# This method of fetching the block from the descriptor bypassess access checks
problem_block = runtime.get_module(block)
# Attempt to render the block, this should return different fragments if the content is gated or not.
frag = runtime.render(problem_block, 'student_view')
if is_gated:
assert 'content-paywall' in frag.content
else:
assert 'content-paywall' not in frag.content
@ddt.data(
*PROBLEM_TYPES
('problem', True),
('openassessment', True),
('drag-and-drop-v2', True),
('done', True),
('edx_sga', True),
('lti_block', True),
('ungraded_problem', False),
('lti_block_not_scored', False),
('audit_visible_graded_problem', False),
)
def test_audit_fails_access_graded_problems(self, prob_type):
block = self.blocks_dict[prob_type]
is_gated = True
@ddt.unpack
def test_access_to_problems(self, prob_type, is_gated):
self._assert_block_is_gated(
block=block,
user_id=self.audit_user.id,
block=self.blocks_dict[prob_type],
user_id=self.users['audit'].id,
course_id=self.course.id,
is_gated=is_gated
)
self._assert_block_is_gated(
block=self.blocks_dict[prob_type],
user_id=self.users['verified'].id,
course_id=self.course.id,
is_gated=False
)
@ddt.data(
*GRADED_SCORE_WEIGHT_TEST_CASES

View File

@@ -23,7 +23,7 @@
# 4. If the package is not needed in production, add it to another file such
# as development.in or testing.in instead.
analytics-python==1.1.0 # Used for Segment analytics
analytics-python==1.2.9 # Used for Segment analytics
attrs # Reduces boilerplate code involving class attributes
Babel==1.3 # Internationalization utilities, used for date formatting in a few places
bleach==2.1.4 # Allowed-list-based HTML sanitizing library that escapes or strips markup and attributes; used for capa and LTI

View File

@@ -40,7 +40,7 @@ git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.1.6#egg=xblock-d
git+https://github.com/open-craft/xblock-poll@add89e14558c30f3c8dc7431e5cd6536fff6d941#egg=xblock-poll==1.5.1
-e common/lib/xmodule
amqp==1.4.9 # via kombu
analytics-python==1.1.0
analytics-python==1.2.9
aniso8601==4.0.1 # via tincan
anyjson==0.3.3 # via kombu
appdirs==1.4.3 # via fs

View File

@@ -44,7 +44,7 @@ git+https://github.com/open-craft/xblock-poll@add89e14558c30f3c8dc7431e5cd6536ff
-e common/lib/xmodule
alabaster==0.7.12 # via sphinx
amqp==1.4.9
analytics-python==1.1.0
analytics-python==1.2.9
aniso8601==4.0.1
anyjson==0.3.3
apipkg==1.5

View File

@@ -41,7 +41,7 @@ git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.1.6#egg=xblock-d
git+https://github.com/open-craft/xblock-poll@add89e14558c30f3c8dc7431e5cd6536fff6d941#egg=xblock-poll==1.5.1
-e common/lib/xmodule
amqp==1.4.9
analytics-python==1.1.0
analytics-python==1.2.9
aniso8601==4.0.1
anyjson==0.3.3
apipkg==1.5 # via execnet