Commit a776733d by Bill DeRusha Committed by GitHub

Merge pull request #13638 from edx/bderusha/utm-tracking

Add UTM tracking for registrations
parents ed7f32ee 9877c6ad
......@@ -11,7 +11,8 @@ from xmodule.modulestore.django import modulestore
from config_models.admin import ConfigurationModelAdmin
from student.models import (
UserProfile, UserTestGroup, CourseEnrollmentAllowed, DashboardConfiguration, CourseEnrollment, Registration,
PendingNameChange, CourseAccessRole, LinkedInAddToProfileConfiguration, UserAttribute, LogoutViewConfiguration
PendingNameChange, CourseAccessRole, LinkedInAddToProfileConfiguration, UserAttribute, LogoutViewConfiguration,
RegistrationCookieConfiguration
)
from student.roles import REGISTERED_ACCESS_ROLES
......@@ -184,6 +185,7 @@ admin.site.register(Registration)
admin.site.register(PendingNameChange)
admin.site.register(DashboardConfiguration, ConfigurationModelAdmin)
admin.site.register(LogoutViewConfiguration, ConfigurationModelAdmin)
admin.site.register(RegistrationCookieConfiguration, ConfigurationModelAdmin)
# We must first un-register the User model since it may also be registered by the auth app.
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('student', '0006_logoutviewconfiguration'),
]
operations = [
migrations.CreateModel(
name='RegistrationCookieConfiguration',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('utm_cookie_name', models.CharField(help_text='Name of the UTM cookie', max_length=255)),
('affiliate_cookie_name', models.CharField(help_text='Name of the affiliate cookie', max_length=255)),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
options={
'ordering': ('-change_date',),
'abstract': False,
},
),
]
......@@ -2250,6 +2250,28 @@ class EnrollmentRefundConfiguration(ConfigurationModel):
self.refund_window_microseconds = int(refund_window.total_seconds() * 1000000)
class RegistrationCookieConfiguration(ConfigurationModel):
"""
Configuration for registration cookies.
"""
utm_cookie_name = models.CharField(
max_length=255,
help_text=_("Name of the UTM cookie")
)
affiliate_cookie_name = models.CharField(
max_length=255,
help_text=_("Name of the affiliate cookie")
)
def __unicode__(self):
"""Unicode representation of this config. """
return u"UTM: {utm_name}; AFFILIATE: {affiliate_name}".format(
utm_name=self.utm_cookie_name,
affiliate_name=self.affiliate_cookie_name
)
class UserAttribute(TimeStampedModel):
"""
Record additional metadata about a user, stored as key/value pairs of text.
......
......@@ -12,6 +12,7 @@ from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.utils.importlib import import_module
import mock
import pytz
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from lang_pref import LANGUAGE_KEY
......@@ -19,7 +20,7 @@ from notification_prefs import NOTIFICATION_PREF_KEY
from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
import student
from student.models import UserAttribute
from student.views import REGISTRATION_AFFILIATE_ID
from student.views import REGISTRATION_AFFILIATE_ID, REGISTRATION_UTM_PARAMETERS, REGISTRATION_UTM_CREATED_AT
TEST_CS_URL = 'https://comments.service.test:123/'
......@@ -280,7 +281,7 @@ class TestCreateAccount(TestCase):
self.assertIsNone(preference)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_referral_attribution(self):
def test_affiliate_referral_attribution(self):
"""
Verify that a referral attribution is recorded if an affiliate
cookie is present upon a new user's registration.
......@@ -291,11 +292,73 @@ class TestCreateAccount(TestCase):
self.assertEqual(UserAttribute.get_user_attribute(user, REGISTRATION_AFFILIATE_ID), affiliate_id)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_utm_referral_attribution(self):
"""
Verify that a referral attribution is recorded if an affiliate
cookie is present upon a new user's registration.
"""
utm_cookie_name = 'edx.test.utm'
with mock.patch('student.models.RegistrationCookieConfiguration.current') as config:
instance = config.return_value
instance.utm_cookie_name = utm_cookie_name
timestamp = 1475521816879
utm_cookie = {
'utm_source': 'test-source',
'utm_medium': 'test-medium',
'utm_campaign': 'test-campaign',
'utm_term': 'test-term',
'utm_content': 'test-content',
'created_at': timestamp
}
created_at = datetime.fromtimestamp(timestamp / float(1000), tz=pytz.UTC)
self.client.cookies[utm_cookie_name] = json.dumps(utm_cookie)
user = self.create_account_and_fetch_profile().user
self.assertEqual(
UserAttribute.get_user_attribute(user, REGISTRATION_UTM_PARAMETERS.get('utm_source')),
utm_cookie.get('utm_source')
)
self.assertEqual(
UserAttribute.get_user_attribute(user, REGISTRATION_UTM_PARAMETERS.get('utm_medium')),
utm_cookie.get('utm_medium')
)
self.assertEqual(
UserAttribute.get_user_attribute(user, REGISTRATION_UTM_PARAMETERS.get('utm_campaign')),
utm_cookie.get('utm_campaign')
)
self.assertEqual(
UserAttribute.get_user_attribute(user, REGISTRATION_UTM_PARAMETERS.get('utm_term')),
utm_cookie.get('utm_term')
)
self.assertEqual(
UserAttribute.get_user_attribute(user, REGISTRATION_UTM_PARAMETERS.get('utm_content')),
utm_cookie.get('utm_content')
)
self.assertEqual(
UserAttribute.get_user_attribute(user, REGISTRATION_UTM_CREATED_AT),
str(created_at)
)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_no_referral(self):
"""Verify that no referral is recorded when a cookie is not present."""
self.assertIsNone(self.client.cookies.get(settings.AFFILIATE_COOKIE_NAME)) # pylint: disable=no-member
user = self.create_account_and_fetch_profile().user
self.assertIsNone(UserAttribute.get_user_attribute(user, REGISTRATION_AFFILIATE_ID))
utm_cookie_name = 'edx.test.utm'
with mock.patch('student.models.RegistrationCookieConfiguration.current') as config:
instance = config.return_value
instance.utm_cookie_name = utm_cookie_name
self.assertIsNone(self.client.cookies.get(settings.AFFILIATE_COOKIE_NAME)) # pylint: disable=no-member
self.assertIsNone(self.client.cookies.get(utm_cookie_name)) # pylint: disable=no-member
user = self.create_account_and_fetch_profile().user
self.assertIsNone(UserAttribute.get_user_attribute(user, REGISTRATION_AFFILIATE_ID))
self.assertIsNone(UserAttribute.get_user_attribute(user, REGISTRATION_UTM_PARAMETERS.get('utm_source')))
self.assertIsNone(UserAttribute.get_user_attribute(user, REGISTRATION_UTM_PARAMETERS.get('utm_medium')))
self.assertIsNone(UserAttribute.get_user_attribute(user, REGISTRATION_UTM_PARAMETERS.get('utm_campaign')))
self.assertIsNone(UserAttribute.get_user_attribute(user, REGISTRATION_UTM_PARAMETERS.get('utm_term')))
self.assertIsNone(UserAttribute.get_user_attribute(user, REGISTRATION_UTM_PARAMETERS.get('utm_content')))
self.assertIsNone(UserAttribute.get_user_attribute(user, REGISTRATION_UTM_CREATED_AT))
@ddt.ddt
......
......@@ -54,7 +54,7 @@ from student.models import (
CourseEnrollmentAllowed, UserStanding, LoginFailures,
create_comments_service_user, PasswordHistory, UserSignupSource,
DashboardConfiguration, LinkedInAddToProfileConfiguration, ManualEnrollmentAudit, ALLOWEDTOENROLL_TO_ENROLLED,
LogoutViewConfiguration)
LogoutViewConfiguration, RegistrationCookieConfiguration)
from student.forms import AccountCreationForm, PasswordResetFormNoActive, get_registration_extension_form
from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=import-error
......@@ -133,6 +133,14 @@ ReverifyInfo = namedtuple('ReverifyInfo', 'course_id course_name course_number d
SETTING_CHANGE_INITIATED = 'edx.user.settings.change_initiated'
# Used as the name of the user attribute for tracking affiliate registrations
REGISTRATION_AFFILIATE_ID = 'registration_affiliate_id'
REGISTRATION_UTM_PARAMETERS = {
'utm_source': 'registration_utm_source',
'utm_medium': 'registration_utm_medium',
'utm_campaign': 'registration_utm_campaign',
'utm_term': 'registration_utm_term',
'utm_content': 'registration_utm_content',
}
REGISTRATION_UTM_CREATED_AT = 'registration_utm_created_at'
# used to announce a registration
REGISTER_USER = Signal(providing_args=["user", "profile"])
......@@ -1817,7 +1825,11 @@ def create_account_with_params(request, params):
login(request, new_user)
request.session.set_expiry(0)
_record_registration_attribution(request, new_user)
try:
record_registration_attributions(request, new_user)
# Don't prevent a user from registering due to attribution errors.
except Exception: # pylint: disable=broad-except
log.exception('Error while attributing cookies to user registration.')
# TODO: there is no error checking here to see that the user actually logged in successfully,
# and is not yet an active user.
......@@ -1859,16 +1871,54 @@ def _enroll_user_in_pending_courses(student):
)
def _record_registration_attribution(request, user):
def record_affiliate_registration_attribution(request, user):
"""
Attribute this user's registration to the referring affiliate, if
applicable.
"""
affiliate_id = request.COOKIES.get(settings.AFFILIATE_COOKIE_NAME)
if user is not None and affiliate_id is not None:
if user and affiliate_id:
UserAttribute.set_user_attribute(user, REGISTRATION_AFFILIATE_ID, affiliate_id)
def record_utm_registration_attribution(request, user):
"""
Attribute this user's registration to the latest UTM referrer, if
applicable.
"""
utm_cookie_name = RegistrationCookieConfiguration.current().utm_cookie_name
utm_cookie = request.COOKIES.get(utm_cookie_name)
if user and utm_cookie:
utm = json.loads(utm_cookie)
for utm_parameter in REGISTRATION_UTM_PARAMETERS:
UserAttribute.set_user_attribute(
user,
REGISTRATION_UTM_PARAMETERS.get(utm_parameter),
utm.get(utm_parameter)
)
created_at_unixtime = utm.get('created_at')
if created_at_unixtime:
# We divide by 1000 here because the javascript timestamp generated is in milliseconds not seconds.
# PYTHON: time.time() => 1475590280.823698
# JS: new Date().getTime() => 1475590280823
created_at_datetime = datetime.datetime.fromtimestamp(int(created_at_unixtime) / float(1000), tz=UTC)
else:
created_at_datetime = None
UserAttribute.set_user_attribute(
user,
REGISTRATION_UTM_CREATED_AT,
created_at_datetime
)
def record_registration_attributions(request, user):
"""
Attribute this user's registration based on referrer cookies.
"""
record_affiliate_registration_attribution(request, user)
record_utm_registration_attribution(request, user)
@csrf_exempt
def create_account(request, post_override=None):
"""
......
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