From 8f4cf6829a713743dfc3131da31ec70604e7921b Mon Sep 17 00:00:00 2001 From: Mike Dikan Date: Tue, 20 Dec 2016 18:55:28 -0500 Subject: [PATCH] Adding support for UTM Tracking to apply to free purhases ECOM-6450 Updating commerce api endpoint to capture user UTM cookies and resend them in the server request to the ecommerce IDA. Currently, the non-free purchases are tracked because the user makes the request directly on ecommerce and user cookie contains UTM data, but for free purchases, the UTM cookie isn't sent in the background server request to the ecommerce IDA. --- .../commerce/api/v0/tests/test_views.py | 32 +++++++++++++++---- lms/djangoapps/commerce/api/v0/views.py | 26 ++++++++++----- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/lms/djangoapps/commerce/api/v0/tests/test_views.py b/lms/djangoapps/commerce/api/v0/tests/test_views.py index f3b8491540..ac3a66a458 100644 --- a/lms/djangoapps/commerce/api/v0/tests/test_views.py +++ b/lms/djangoapps/commerce/api/v0/tests/test_views.py @@ -31,6 +31,11 @@ from student.tests.tests import EnrollmentEventTestMixin from xmodule.modulestore.django import modulestore from commerce.api.v0.views import SAILTHRU_CAMPAIGN_COOKIE +UTM_COOKIE_NAME = 'edx.test.utm' +UTM_COOKIE_CONTENTS = { + 'utm_source': 'test-source' +} + @attr(shard=1) @ddt.ddt @@ -39,7 +44,7 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase) """ Tests for the commerce orders view. """ - def _post_to_view(self, course_id=None, marketing_email_opt_in=False): + def _post_to_view(self, course_id=None, marketing_email_opt_in=False, include_utm_cookie=False): """ POST to the view being tested. @@ -55,6 +60,8 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase) payload["email_opt_in"] = True self.client.cookies[SAILTHRU_CAMPAIGN_COOKIE] = 'sailthru id' + if include_utm_cookie: + self.client.cookies[UTM_COOKIE_NAME] = json.dumps(UTM_COOKIE_CONTENTS) return self.client.post(self.url, payload) def assertResponseMessage(self, response, expected_msg): @@ -160,12 +167,12 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase) self.assertValidEcommerceInternalRequestErrorResponse(response) self.assertUserNotEnrolled() - def _test_successful_ecommerce_api_call(self, is_completed=True): + def _test_successful_ecommerce_api_call(self, is_completed=True, utm_tracking_present=False): """ Verifies that the view contacts the E-Commerce API with the correct data and headers. """ with mock.patch('commerce.api.v0.views.audit_log') as mock_audit_log: - response = self._post_to_view() + response = self._post_to_view(include_utm_cookie=utm_tracking_present) # Verify that an audit message was logged self.assertTrue(mock_audit_log.called) @@ -177,9 +184,15 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase) else: self.assertResponsePaymentData(response) - # make sure ecommerce API call forwards Sailthru cookie + # Make sure ecommerce API call forwards Sailthru cookie self.assertIn('{}=sailthru id'.format(SAILTHRU_CAMPAIGN_COOKIE), httpretty.last_request().headers['cookie']) + # Check that UTM tracking cookie is passed along in request to ecommerce for attribution + if utm_tracking_present: + cookie_string = '{cookie_name}={cookie_contents}'.format( + cookie_name=UTM_COOKIE_NAME, cookie_contents=json.dumps(UTM_COOKIE_CONTENTS)) + self.assertIn(cookie_string, httpretty.last_request().headers['cookie']) + @ddt.data(True, False) def test_course_with_honor_seat_sku(self, user_is_active): """ @@ -193,7 +206,14 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase) return_value = {'id': TEST_BASKET_ID, 'payment_data': None, 'order': {'number': TEST_ORDER_NUMBER}} with mock_create_basket(response=return_value): + # Test that call without utm tracking works self._test_successful_ecommerce_api_call() + with mock.patch('student.models.RegistrationCookieConfiguration.current') as config: + instance = config.return_value + instance.utm_cookie_name = UTM_COOKIE_NAME + + # Test that call with cookie passes cookie along + self._test_successful_ecommerce_api_call(utm_tracking_present=True) @ddt.data(True, False) def test_course_with_paid_seat_sku(self, user_is_active): @@ -207,7 +227,7 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase) return_value = {'id': TEST_BASKET_ID, 'payment_data': TEST_PAYMENT_DATA, 'order': None} with mock_create_basket(response=return_value): - self._test_successful_ecommerce_api_call(False) + self._test_successful_ecommerce_api_call(is_completed=False) def _test_course_without_sku(self, enrollment_mode=CourseMode.DEFAULT_MODE_SLUG): """ @@ -334,7 +354,7 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase) self.assertIsNotNone(get_enrollment(self.user.username, unicode(self.course.id))) with mock_create_basket(): - self._test_successful_ecommerce_api_call(False) + self._test_successful_ecommerce_api_call(is_completed=False) @mock.patch('commerce.api.v0.views.update_email_opt_in') @ddt.data(*itertools.product((False, True), (False, True), (False, True))) diff --git a/lms/djangoapps/commerce/api/v0/views.py b/lms/djangoapps/commerce/api/v0/views.py index 3ea812412b..b427c89236 100644 --- a/lms/djangoapps/commerce/api/v0/views.py +++ b/lms/djangoapps/commerce/api/v0/views.py @@ -22,7 +22,7 @@ from openedx.core.djangoapps.commerce.utils import ecommerce_api_client from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser from openedx.core.lib.log_utils import audit_log -from student.models import CourseEnrollment +from student.models import CourseEnrollment, RegistrationCookieConfiguration from util.json_request import JsonResponse @@ -150,13 +150,11 @@ class BasketsView(APIView): # Make the API call try: # Pass along Sailthru campaign id - campaign_cookie = request.COOKIES.get(SAILTHRU_CAMPAIGN_COOKIE) - if campaign_cookie: - cookie = {SAILTHRU_CAMPAIGN_COOKIE: campaign_cookie} - if api_session.cookies: - requests.utils.add_dict_to_cookiejar(api_session.cookies, cookie) - else: - api_session.cookies = requests.utils.cookiejar_from_dict(cookie) + self._add_request_cookie_to_api_session(api_session, request, SAILTHRU_CAMPAIGN_COOKIE) + + # Pass along UTM tracking info + utm_cookie_name = RegistrationCookieConfiguration.current().utm_cookie_name + self._add_request_cookie_to_api_session(api_session, request, utm_cookie_name) response_data = api.baskets.post({ 'products': [{'sku': default_enrollment_mode.sku}], @@ -194,6 +192,18 @@ class BasketsView(APIView): self._handle_marketing_opt_in(request, course_key, user) return response + def _add_request_cookie_to_api_session(self, server_session, request, cookie_name): + """ Add cookie from user request into server session """ + user_cookie = None + if cookie_name: + user_cookie = request.COOKIES.get(cookie_name) + if user_cookie: + server_cookie = {cookie_name: user_cookie} + if server_session.cookies: + requests.utils.add_dict_to_cookiejar(server_session.cookies, server_cookie) + else: + server_session.cookies = requests.utils.cookiejar_from_dict(server_cookie) + class BasketOrderView(APIView): """ Retrieve the order associated with a basket. """