Example #1
0
def _track_notification_sent(message, context):
    """
    Send analytics event for a sent email
    """
    properties = {
        'app_label': 'discussion',
        'name': 'responsenotification',  # This is 'Campaign' in GA
        'language': message.language,
        'uuid': six.text_type(message.uuid),
        'send_uuid': six.text_type(message.send_uuid),
        'thread_id': context['thread_id'],
        'course_id': six.text_type(context['course_id']),
        'thread_created_at': date.deserialize(context['thread_created_at']),
        'nonInteraction': 1,
    }
    tracking_context = {
        'host': context['site'].domain,
        'path': '/',  # make up a value, in order to allow the host to be passed along.
    }
    # The event used to specify the user_id as being the recipient of the email (i.e. the thread_author_id).
    # This has the effect of interrupting the actual chain of events for that author, if any, while the
    # email-sent event should really be associated with the sender, since that is what triggers the event.
    with tracker.get_tracker().context(properties['app_label'], tracking_context):
        segment.track(
            user_id=context['thread_author_id'],
            event_name='edx.bi.email.sent',
            properties=properties
        )
Example #2
0
def _track_notification_sent(message, context):
    """
    Send analytics event for a sent email
    """
    properties = {
        'app_label': 'discussion',
        'name': 'responsenotification',  # This is 'Campaign' in GA
        'language': message.language,
        'uuid': unicode(message.uuid),
        'send_uuid': unicode(message.send_uuid),
        'thread_id': context['thread_id'],
        'course_id': unicode(context['course_id']),
        'thread_created_at': date.deserialize(context['thread_created_at']),
        'nonInteraction': 1,
    }
    tracking_context = {
        'host': context['site'].domain,
        'path': '/',  # make up a value, in order to allow the host to be passed along.
    }
    # The event used to specify the user_id as being the recipient of the email (i.e. the thread_author_id).
    # This has the effect of interrupting the actual chain of events for that author, if any, while the
    # email-sent event should really be associated with the sender, since that is what triggers the event.
    with tracker.get_tracker().context(properties['app_label'], tracking_context):
        segment.track(
            user_id=context['thread_author_id'],
            event_name='edx.bi.email.sent',
            properties=properties
        )
Example #3
0
def _should_randomly_suppress_schedule_creation(
    schedule_config,
    enrollment,
    upgrade_deadline,
    experience_type,
    content_availability_date,
):
    # The hold back ratio is always between 0 and 1. A value of 0 indicates that schedules should be created for all
    # schedules. A value of 1 indicates that no schedules should be created for any enrollments. A value of 0.2 would
    # mean that 20% of enrollments should *not* be given schedules.

    # This allows us to measure the impact of the dynamic schedule experience by comparing this "control" group that
    # does not receive any of benefits of the feature against the group that does.
    if random.random() < schedule_config.hold_back_ratio:
        log.debug('Schedules: Enrollment held back from dynamic schedule experiences.')
        upgrade_deadline_str = None
        if upgrade_deadline:
            upgrade_deadline_str = upgrade_deadline.isoformat()
        segment.track(
            user_id=enrollment.user.id,
            event_name='edx.bi.schedule.suppressed',
            properties={
                'course_id': unicode(enrollment.course_id),
                'experience_type': experience_type,
                'upgrade_deadline': upgrade_deadline_str,
                'content_availability_date': content_availability_date.isoformat(),
            }
        )
        return True

    return False
Example #4
0
def _track_user_login(user, request):
    """
    Sends a tracking event for a successful login.
    """
    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
        },
    )
Example #5
0
 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)
Example #6
0
def _should_randomly_suppress_schedule_creation(
    schedule_config,
    enrollment,
    upgrade_deadline,
    experience_type,
    content_availability_date,
):
    # The hold back ratio is always between 0 and 1. A value of 0 indicates that schedules should be created for all
    # schedules. A value of 1 indicates that no schedules should be created for any enrollments. A value of 0.2 would
    # mean that 20% of enrollments should *not* be given schedules.

    # This allows us to measure the impact of the dynamic schedule experience by comparing this "control" group that
    # does not receive any of benefits of the feature against the group that does.
    if random.random() < schedule_config.hold_back_ratio:
        log.debug(
            'Schedules: Enrollment held back from dynamic schedule experiences.'
        )
        upgrade_deadline_str = None
        if upgrade_deadline:
            upgrade_deadline_str = upgrade_deadline.isoformat()
        segment.track(user_id=enrollment.user.id,
                      event_name='edx.bi.schedule.suppressed',
                      properties={
                          'course_id':
                          six.text_type(enrollment.course_id),
                          'experience_type':
                          experience_type,
                          'upgrade_deadline':
                          upgrade_deadline_str,
                          'content_availability_date':
                          content_availability_date.isoformat(),
                      })
        return True

    return False
Example #7
0
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:
        identity_args = [
            user.id,
            {
                'email': user.email,
                'username': user.username,
                'name': profile.name,
                # Mailchimp requires the age & yearOfBirth to be integers, we send a sane integer default if falsey.
                'age': profile.age or -1,
                'yearOfBirth': profile.year_of_birth or datetime.datetime.now(UTC).year,
                'education': profile.level_of_education_display,
                'address': profile.mailing_address,
                'gender': profile.gender_display,
                'country': text_type(profile.country),
            }
        ]
        # .. pii: Many pieces of PII are sent to Segment here. Retired directly through Segment API call in Tubular.
        # .. pii_types: email_address, username, name, birth_date, location, gender
        # .. pii_retirement: third_party
        segment.identify(*identity_args)
        segment.track(
            user.id,
            "edx.bi.user.account.registered",
            {
                'category': 'conversion',
                # ..pii: Learner email is sent to Segment in following line and will be associated with analytics data.
                'email': user.email,
                'label': params.get('course_id'),
                'provider': third_party_provider.name if third_party_provider else None
            },
        )
Example #8
0
def _track_message_sent(site, user, msg):
    properties = {
        'site': site.domain,
        'app_label': msg.app_label,
        'name': msg.name,
        'language': msg.language,
        'uuid': unicode(msg.uuid),
        'send_uuid': unicode(msg.send_uuid),
        'nonInteraction': 1,
    }
    course_ids = msg.context.get('course_ids', [])
    properties['num_courses'] = len(course_ids)
    if len(course_ids) > 0:
        properties['course_ids'] = course_ids[:10]
        properties['primary_course_id'] = course_ids[0]

    tracking_context = {
        'host': site.domain,
        'path': '/',  # make up a value, in order to allow the host to be passed along.
    }
    # I wonder if the user of this event should be the recipient, as they are not the ones
    # who took an action.  Rather, the system is acting, and they are the object.
    # Admittedly that may be what 'nonInteraction' is meant to address.  But sessionization may
    # get confused by these events if they're attributed in this way, because there's no way for
    # this event to get context that would match with what the user might be doing at the moment.
    # But the events do show up in GA being joined up with existing sessions (i.e. within a half
    # hour in the past), so they don't always break sessions.  Not sure what happens after these.
    # We can put the recipient_user_id into the properties, and then export as a custom dimension.
    with tracker.get_tracker().context(msg.app_label, tracking_context):
        segment.track(
            user_id=user.id,
            event_name='edx.bi.email.sent',
            properties=properties,
        )
Example #9
0
def _track_message_sent(site, user, msg):
    properties = {
        'site': site.domain,
        'app_label': msg.app_label,
        'name': msg.name,
        'language': msg.language,
        'uuid': six.text_type(msg.uuid),
        'send_uuid': six.text_type(msg.send_uuid),
        'nonInteraction': 1,
    }
    course_ids = msg.context.get('course_ids', [])
    properties['num_courses'] = len(course_ids)
    if len(course_ids) > 0:
        properties['course_ids'] = course_ids[:10]
        properties['primary_course_id'] = course_ids[0]

    tracking_context = {
        'host': site.domain,
        'path': '/',  # make up a value, in order to allow the host to be passed along.
    }
    # I wonder if the user of this event should be the recipient, as they are not the ones
    # who took an action.  Rather, the system is acting, and they are the object.
    # Admittedly that may be what 'nonInteraction' is meant to address.  But sessionization may
    # get confused by these events if they're attributed in this way, because there's no way for
    # this event to get context that would match with what the user might be doing at the moment.
    # But the events do show up in GA being joined up with existing sessions (i.e. within a half
    # hour in the past), so they don't always break sessions.  Not sure what happens after these.
    # We can put the recipient_user_id into the properties, and then export as a custom dimension.
    with tracker.get_tracker().context(msg.app_label, tracking_context):
        segment.track(
            user_id=user.id,
            event_name='edx.bi.email.sent',
            properties=properties,
        )
Example #10
0
def _track_user_login(user, request):
    """
    Sends a tracking event for a successful login.
    """
    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
        },
    )
Example #11
0
def _track_user_login(user, request):
    """
    Sends a tracking event for a successful login.
    """
    # .. pii: Username and email are sent to Segment here. Retired directly through Segment API call in Tubular.
    # .. pii_types: email_address, username
    # .. pii_retirement: third_party
    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
        },
    )
Example #12
0
def _track_user_login(user, request):
    """
    Sends a tracking event for a successful login.
    """
    # .. pii: Username and email are sent to Segment here. Retired directly through Segment API call in Tubular.
    # .. pii_types: email_address, username
    # .. pii_retirement: third_party
    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
        },
    )
Example #13
0
def _is_in_holdback(user):
    """
    Return whether the specified user is in the first-purchase-discount holdback group.
    """
    if datetime(2020, 8, 1, tzinfo=pytz.UTC) <= datetime.now(tz=pytz.UTC):
        return False

    # Holdback is 50/50
    bucket = stable_bucketing_hash_group(DISCOUNT_APPLICABILITY_HOLDBACK, 2,
                                         user.username)

    request = get_current_request()
    if hasattr(request, 'session'
               ) and DISCOUNT_APPLICABILITY_HOLDBACK not in request.session:

        properties = {
            'site': request.site.domain,
            'app_label': 'discounts',
            'nonInteraction': 1,
            'bucket': bucket,
            'experiment': 'REVEM-363',
        }
        segment.track(
            user_id=user.id,
            event_name='edx.bi.experiment.user.bucketed',
            properties=properties,
        )

        # Mark that we've recorded this bucketing, so that we don't do it again this session
        request.session[DISCOUNT_APPLICABILITY_HOLDBACK] = True

    return bucket == 0
Example #14
0
def track_event(user_id, event_name, properties):
    """
    Emit a track event to segment (and forwarded to GA) for some parts of the Enterprise workflows.
    """
    # Only call the endpoint if the import was successful.
    if segment:
        segment.track(user_id, event_name, properties)
Example #15
0
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, {
        'goal_key': instance.goal_key,
    })
    segment.track(instance.user.id, name)
Example #16
0
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,
        {
            'goal_key': instance.goal_key,
        }
    )
    segment.track(instance.user.id, name)
Example #17
0
    def _fire_event(self, user, event_name, parameters):
        """
        Fire an analytics event.

        Arguments:
            user (User): The user who submitted photos.
            event_name (str): Name of the analytics event.
            parameters (dict): Event parameters.

        Returns: None

        """
        segment.track(user.id, event_name, parameters)
Example #18
0
    def _fire_event(self, user, event_name, parameters):
        """
        Fire an analytics event.

        Arguments:
            user (User): The user who submitted photos.
            event_name (str): Name of the analytics event.
            parameters (dict): Event parameters.

        Returns: None

        """
        segment.track(user.id, event_name, parameters)
Example #19
0
def login_analytics(strategy, auth_entry, current_partial=None, *args, **kwargs):
    """ Sends login info to Segment """

    event_name = None
    if auth_entry == AUTH_ENTRY_LOGIN:
        event_name = 'edx.bi.user.account.authenticated'
    elif auth_entry in [AUTH_ENTRY_ACCOUNT_SETTINGS]:
        event_name = 'edx.bi.user.account.linked'

    if event_name is not None:
        segment.track(kwargs['user'].id, event_name, {
            'category': "conversion",
            'label': None,
            'provider': kwargs['backend'].name
        })
Example #20
0
def login_analytics(strategy, auth_entry, current_partial=None, *args, **kwargs):
    """ Sends login info to Segment """

    event_name = None
    if auth_entry == AUTH_ENTRY_LOGIN:
        event_name = 'edx.bi.user.account.authenticated'
    elif auth_entry in [AUTH_ENTRY_ACCOUNT_SETTINGS]:
        event_name = 'edx.bi.user.account.linked'

    if event_name is not None:
        segment.track(kwargs['user'].id, event_name, {
            'category': "conversion",
            'label': None,
            'provider': kwargs['backend'].name
        })
Example #21
0
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:
        identity_args = [
            user.id,
            {
                'email':
                user.email,
                'username':
                user.username,
                'name':
                profile.name,
                # Mailchimp requires the age & yearOfBirth to be integers, we send a sane integer default if falsey.
                'age':
                profile.age or -1,
                'yearOfBirth':
                profile.year_of_birth or datetime.datetime.now(UTC).year,
                'education':
                profile.level_of_education_display,
                'address':
                profile.mailing_address,
                'gender':
                profile.gender_display,
                'country':
                text_type(profile.country),
            }
        ]
        # Provide additional context only if needed.
        if hasattr(settings, 'MAILCHIMP_NEW_USER_LIST_ID'):
            identity_args.append(
                {"MailChimp": {
                    "listId": settings.MAILCHIMP_NEW_USER_LIST_ID
                }})

        segment.identify(*identity_args)
        segment.track(
            user.id,
            "edx.bi.user.account.registered",
            {
                'category':
                'conversion',
                'label':
                params.get('course_id'),
                'provider':
                third_party_provider.name if third_party_provider else None
            },
        )
Example #22
0
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:
        identity_args = [
            user.id,
            {
                'email': user.email,
                'username': user.username,
                'name': profile.name,
                # Mailchimp requires the age & yearOfBirth to be integers, we send a sane integer default if falsey.
                'age': profile.age or -1,
                'yearOfBirth': profile.year_of_birth or datetime.datetime.now(UTC).year,
                'education': profile.level_of_education_display,
                'address': profile.mailing_address,
                'gender': profile.gender_display,
                'country': text_type(profile.country),
            }
        ]
        # Provide additional context only if needed.
        if hasattr(settings, 'MAILCHIMP_NEW_USER_LIST_ID'):
            identity_args.append({
                "MailChimp": {
                    "listId": settings.MAILCHIMP_NEW_USER_LIST_ID
                }
            })

        # .. pii: Many pieces of PII are sent to Segment here. Retired directly through Segment API call in Tubular.
        # .. pii_types: email_address, username, name, birth_date, location, gender
        # .. pii_retirement: third_party
        segment.identify(*identity_args)
        segment.track(
            user.id,
            "edx.bi.user.account.registered",
            {
                'category': 'conversion',
                # ..pii: Learner email is sent to Segment in following line and will be associated with analytics data.
                'email': user.email,
                'label': params.get('course_id'),
                'provider': third_party_provider.name if third_party_provider else None
            },
        )
Example #23
0
def _track_update_email_opt_in(user_id, organization, opt_in):
    """Track an email opt-in preference change.

    Arguments:
        user_id (str): The ID of the user making the preference change.
        organization (str): The organization whose emails are being opted into or out of by the user.
        opt_in (bool): Whether the user has chosen to opt-in to emails from the organization.

    Returns:
        None

    """
    event_name = 'edx.bi.user.org_email.opted_in' if opt_in else 'edx.bi.user.org_email.opted_out'
    segment.track(
        user_id,
        event_name,
        {
            'category': 'communication',
            'label': organization
        },
    )
Example #24
0
def _track_update_email_opt_in(user_id, organization, opt_in):
    """Track an email opt-in preference change.

    Arguments:
        user_id (str): The ID of the user making the preference change.
        organization (str): The organization whose emails are being opted into or out of by the user.
        opt_in (bool): Whether the user has chosen to opt-in to emails from the organization.

    Returns:
        None

    """
    event_name = 'edx.bi.user.org_email.opted_in' if opt_in else 'edx.bi.user.org_email.opted_out'
    segment.track(
        user_id,
        event_name,
        {
            'category': 'communication',
            'label': organization
        },
    )
    def test_track_with_standard_context(self):

        # Note that 'host' and 'path' will be urlparsed, so must be strings.
        tracking_context = {
            'accept_language': sentinel.accept_language,
            'referer': sentinel.referer,
            'username': sentinel.username,
            'session': sentinel.session,
            'ip': sentinel.ip,
            'host': 'hostname',
            'agent': sentinel.agent,
            'path': '/this/is/a/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': '/this/is/a/path',
                'referrer': sentinel.referer,
                'url':
                'https://hostname/this/is/a/path'  # Synthesized URL value.
            }
        }
        self.assertEqual((sentinel.user_id, sentinel.name, self.properties,
                          expected_segment_context), args)
Example #26
0
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:
        identity_args = [
            user.id,
            {
                'email': user.email,
                'username': user.username,
                'name': profile.name,
                # Mailchimp requires the age & yearOfBirth to be integers, we send a sane integer default if falsey.
                'age': profile.age or -1,
                'yearOfBirth': profile.year_of_birth or datetime.datetime.now(UTC).year,
                'education': profile.level_of_education_display,
                'address': profile.mailing_address,
                'gender': profile.gender_display,
                'country': text_type(profile.country),
            }
        ]
        # Provide additional context only if needed.
        if hasattr(settings, 'MAILCHIMP_NEW_USER_LIST_ID'):
            identity_args.append({
                "MailChimp": {
                    "listId": settings.MAILCHIMP_NEW_USER_LIST_ID
                }
            })

        segment.identify(*identity_args)
        segment.track(
            user.id,
            "edx.bi.user.account.registered",
            {
                'category': 'conversion',
                'label': params.get('course_id'),
                'provider': third_party_provider.name if third_party_provider else None
            },
        )
Example #27
0
    def test_track_with_standard_context(self):

        # Note that 'host' and 'path' will be urlparsed, so must be strings.
        tracking_context = {
            'accept_language': sentinel.accept_language,
            'referer': sentinel.referer,
            'username': sentinel.username,
            'session': sentinel.session,
            'ip': sentinel.ip,
            'host': 'hostname',
            'agent': sentinel.agent,
            'path': '/this/is/a/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': '/this/is/a/path',
                'referrer': sentinel.referer,
                'url': 'https://hostname/this/is/a/path'  # Synthesized URL value.
            }
        }
        self.assertEqual((sentinel.user_id, sentinel.name, self.properties, expected_segment_context), args)
Example #28
0
    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)
Example #29
0
    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)
Example #30
0
    def get(self, request):
        """
        Return the if the course should be upsold in the mobile app, if the user has appropriate permissions.
        """
        if not MOBILE_UPSELL_FLAG.is_enabled():
            return Response({
                'show_upsell': False,
                'upsell_flag': False,
            })

        if 'course_id' not in request.GET:
            return Response({
                'show_upsell': False,
            })

        # HACK: the url decoding converts plus to space; put them back
        course_id = request.GET.get('course_id').replace(' ', '+')
        course_key = CourseKey.from_string(course_id)
        course = CourseOverview.get_from_id(course_key)
        user = request.user

        enrollment = None
        user_enrollments = None
        has_non_audit_enrollments = False
        try:
            user_enrollments = CourseEnrollment.objects.select_related(
                'course').filter(user_id=user.id)
            has_non_audit_enrollments = user_enrollments.exclude(
                mode__in=CourseMode.UPSELL_TO_VERIFIED_MODES).exists()
            enrollment = CourseEnrollment.objects.select_related('course').get(
                user_id=user.id, course_id=course.id)
        except CourseEnrollment.DoesNotExist:
            pass  # Not enrolled, use the default values

        context = get_base_experiment_metadata_context(course, user,
                                                       enrollment,
                                                       user_enrollments)

        bucket = stable_bucketing_hash_group(MOBILE_UPSELL_EXPERIMENT, 2,
                                             user.username)
        if hasattr(
                request,
                'session') and MOBILE_UPSELL_EXPERIMENT not in request.session:
            properties = {
                'site': request.site.domain,
                'app_label': 'experiments',
                'bucket': bucket,
                'experiment': 'REV-934',
            }
            segment.track(
                user_id=user.id,
                event_name='edx.bi.experiment.user.bucketed',
                properties=properties,
            )

            # Mark that we've recorded this bucketing, so that we don't do it again this session
            request.session[MOBILE_UPSELL_EXPERIMENT] = True

        show_upsell = bucket != 0 and not has_non_audit_enrollments
        if show_upsell:
            return Response({
                'show_upsell': show_upsell,
                'price': context.get('upgrade_price'),
                'basket_url': context.get('upgrade_link'),
            })
        else:
            return Response({
                'show_upsell': show_upsell,
                'upsell_flag': MOBILE_UPSELL_FLAG.is_enabled(),
                'experiment_bucket': bucket,
            })
Example #31
0
 def test_null_key(self):
     segment.track(sentinel.user_id, sentinel.name, self.properties)
     self.assertFalse(self.mock_segment_track.called)
Example #32
0
 def test_missing_name(self):
     segment.track(sentinel.user_id, None, self.properties)
     self.assertFalse(self.mock_segment_track.called)
Example #33
0
 def test_null_key(self):
     segment.track(sentinel.user_id, sentinel.name, self.properties)
     self.assertFalse(self.mock_segment_track.called)
Example #34
0
 def test_missing_name(self):
     segment.track(sentinel.user_id, None, self.properties)
     self.assertFalse(self.mock_segment_track.called)
Example #35
0
 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)
Example #36
0
    def get(self, request):
        """
        Return the if the course should be upsold in the mobile app, if the user has appropriate permissions.
        """
        if not MOBILE_UPSELL_FLAG.is_enabled():
            return Response({
                'show_upsell': False,
                'upsell_flag': False,
            })

        course_id = request.GET.get('course_id')
        try:
            course_key = CourseKey.from_string(course_id)
        except InvalidKeyError:
            return HttpResponseBadRequest("Missing or invalid course_id")

        course = CourseOverview.get_from_id(course_key)
        if not course.has_started() or course.has_ended():
            return Response({
                'show_upsell': False,
                'upsell_flag': MOBILE_UPSELL_FLAG.is_enabled(),
                'course_running': False,
            })

        user = request.user
        try:
            enrollment = CourseEnrollment.objects.select_related('course').get(
                user_id=user.id, course_id=course.id)
            user_upsell = verified_upgrade_link_is_valid(enrollment)
        except CourseEnrollment.DoesNotExist:
            user_upsell = True

        basket_url = EcommerceService().upgrade_url(user, course.id)
        upgrade_price = six.text_type(
            get_cosmetic_verified_display_price(course))
        could_upsell = bool(user_upsell and basket_url)

        bucket = stable_bucketing_hash_group(MOBILE_UPSELL_EXPERIMENT, 2,
                                             user.username)

        if could_upsell and hasattr(
                request,
                'session') and MOBILE_UPSELL_EXPERIMENT not in request.session:
            properties = {
                'site': request.site.domain,
                'app_label': 'experiments',
                'bucket': bucket,
                'experiment': 'REV-934',
            }
            segment.track(
                user_id=user.id,
                event_name='edx.bi.experiment.user.bucketed',
                properties=properties,
            )

            # Mark that we've recorded this bucketing, so that we don't do it again this session
            request.session[MOBILE_UPSELL_EXPERIMENT] = True

        show_upsell = bool(bucket != 0 and could_upsell)
        if show_upsell:
            return Response({
                'show_upsell': show_upsell,
                'price': upgrade_price,
                'basket_url': basket_url,
            })
        else:
            return Response({
                'show_upsell': show_upsell,
                'upsell_flag': MOBILE_UPSELL_FLAG.is_enabled(),
                'experiment_bucket': bucket,
                'user_upsell': user_upsell,
                'basket_url': basket_url,
            })
Example #37
0
    def get_bucket(self, course_key=None, track=True):
        """
        Return which bucket number the specified user is in.

        Bucket 0 is assumed to be the control bucket and will be returned if the experiment is not enabled for
        this user and course.
        """
        # Keep some imports in here, because this class is commonly used at a module level, and we want to avoid
        # circular imports for any models.
        from experiments.models import ExperimentKeyValue

        request = get_current_request()
        if not request:
            return 0

        if not hasattr(request, 'user') or not request.user.id:
            # We need username for stable bucketing and id for tracking, so just skip anonymous (not-logged-in) users
            return 0

        # Use course key in experiment name to separate caches and segment calls per-course-run
        experiment_name = self.namespaced_flag_name + ('.{}'.format(course_key)
                                                       if course_key else '')

        # Check if we have a cache for this request already
        request_cache = RequestCache('experiments')
        cache_response = request_cache.get_cached_response(experiment_name)
        if cache_response.is_found:
            return cache_response.value

        # Check if the main flag is even enabled for this user and course.
        if not self.is_experiment_on(
                course_key):  # grabs user from the current request, if any
            return self._cache_bucket(experiment_name, 0)

        # Check if the enrollment should even be considered (if it started before the experiment wants, we ignore)
        if course_key and self.experiment_id is not None:
            values = ExperimentKeyValue.objects.filter(
                experiment_id=self.experiment_id).values('key', 'value')
            values = {pair['key']: pair['value'] for pair in values}

            if not self._is_enrollment_inside_date_bounds(
                    values, request.user, course_key):
                return self._cache_bucket(experiment_name, 0)

        bucket = stable_bucketing_hash_group(experiment_name, self.num_buckets,
                                             request.user.username)

        # Now check if the user is forced into a particular bucket, using our subordinate bucket flags
        for i, bucket_flag in enumerate(self.bucket_flags):
            if bucket_flag.is_enabled(course_key):
                bucket = i
                break

        session_key = 'tracked.{}'.format(experiment_name)
        if track and hasattr(request,
                             'session') and session_key not in request.session:
            segment.track(user_id=request.user.id,
                          event_name='edx.bi.experiment.user.bucketed',
                          properties={
                              'site': request.site.domain,
                              'app_label': self.waffle_namespace.name,
                              'experiment': self.flag_name,
                              'course_id':
                              str(course_key) if course_key else None,
                              'bucket': bucket,
                              'is_staff': request.user.is_staff,
                              'nonInteraction': 1,
                          })

            # Mark that we've recorded this bucketing, so that we don't do it again this session
            request.session[session_key] = True

        return self._cache_bucket(experiment_name, bucket)
Example #38
0
    def get_template_context(self, user, user_schedules):
        course_id_strs = []
        course_links = []
        first_valid_upsell_context = None
        first_schedule = None
        first_expiration_date = None

        # Experiment code: Skip users who are in the control bucket
        hash_bucket = stable_bucketing_hash_group('fbe_access_expiry_reminder', 2, user.username)
        properties = {
            'site': self.site.domain,  # pylint: disable=no-member
            'app_label': 'course_duration_limits',
            'nonInteraction': 1,
            'bucket': hash_bucket,
            'experiment': 'REVMI-95',
        }
        course_ids = course_id_strs
        properties['num_courses'] = len(course_ids)
        if course_ids:
            properties['course_ids'] = course_ids[:10]
            properties['primary_course_id'] = course_ids[0]

        tracking_context = {
            'host': self.site.domain,  # pylint: disable=no-member
            'path': '/',  # make up a value, in order to allow the host to be passed along.
        }
        # I wonder if the user of this event should be the recipient, as they are not the ones
        # who took an action.  Rather, the system is acting, and they are the object.
        # Admittedly that may be what 'nonInteraction' is meant to address.  But sessionization may
        # get confused by these events if they're attributed in this way, because there's no way for
        # this event to get context that would match with what the user might be doing at the moment.
        # But the events do show up in GA being joined up with existing sessions (i.e. within a half
        # hour in the past), so they don't always break sessions.  Not sure what happens after these.
        # We can put the recipient_user_id into the properties, and then export as a custom dimension.
        with tracker.get_tracker().context('course_duration_limits', tracking_context):
            segment.track(
                user_id=user.id,
                event_name='edx.bi.experiment.user.bucketed',
                properties=properties,
            )
        if hash_bucket == 0:
            raise InvalidContextError()

        for schedule in user_schedules:
            upsell_context = _get_upsell_information_for_schedule(user, schedule)
            if not upsell_context['show_upsell']:
                continue

            if not CourseDurationLimitConfig.enabled_for_enrollment(enrollment=schedule.enrollment):
                LOG.info(u"course duration limits not enabled for %s", schedule.enrollment)
                continue

            expiration_date = get_user_course_expiration_date(user, schedule.enrollment.course)
            if expiration_date is None:
                LOG.info(u"No course expiration date for %s", schedule.enrollment.course)
                continue

            if first_valid_upsell_context is None:
                first_schedule = schedule
                first_valid_upsell_context = upsell_context
                first_expiration_date = expiration_date
            course_id_str = str(schedule.enrollment.course_id)
            course_id_strs.append(course_id_str)
            course_links.append({
                'url': _get_trackable_course_home_url(schedule.enrollment.course_id),
                'name': schedule.enrollment.course.display_name
            })

        if first_schedule is None:
            self.log_debug('No courses eligible for upgrade for user.')
            raise InvalidContextError()

        context = {
            'course_links': course_links,
            'first_course_name': first_schedule.enrollment.course.display_name,
            'cert_image': static('course_experience/images/verified-cert.png'),
            'course_ids': course_id_strs,
            'first_course_expiration_date': first_expiration_date.strftime(_(u"%b. %d, %Y")),
            'time_until_expiration': timeuntil(first_expiration_date.date(), now=datetime.utcnow().date())
        }
        context.update(first_valid_upsell_context)
        return context
Example #39
0
    def get_bucket(self, course_key=None, track=True):
        """
        Return which bucket number the specified user is in.

        Bucket 0 is assumed to be the control bucket and will be returned if the experiment is not enabled for
        this user and course.
        """
        # Keep some imports in here, because this class is commonly used at a module level, and we want to avoid
        # circular imports for any models.
        from experiments.models import ExperimentKeyValue
        from student.models import CourseEnrollment

        request = get_current_request()
        if not request:
            return 0

        if not request.user.id:
            # We need username for stable bucketing and id for tracking, so just skip anonymous (not-logged-in) users
            return 0

        # Use course key in experiment name to separate caches and segment calls per-course-run
        experiment_name = self.namespaced_flag_name + ('.{}'.format(course_key)
                                                       if course_key else '')

        # Check if we have a cache for this request already
        request_cache = RequestCache('experiments')
        cache_response = request_cache.get_cached_response(experiment_name)
        if cache_response.is_found:
            return cache_response.value

        # Check if the main flag is even enabled for this user and course.
        if not self._is_enabled(
                course_key):  # grabs user from the current request, if any
            return self._cache_bucket(experiment_name, 0)

        # Check if the enrollment should even be considered (if it started before the experiment wants, we ignore)
        if course_key and self.experiment_id is not None:
            start_val = ExperimentKeyValue.objects.filter(
                experiment_id=self.experiment_id, key='enrollment_start')
            if start_val:
                try:
                    start_date = dateutil.parser.parse(
                        start_val.first().value).replace(tzinfo=pytz.UTC)
                except ValueError:
                    log.exception(
                        'Could not parse enrollment start date for experiment %d',
                        self.experiment_id)
                    return self._cache_bucket(experiment_name, 0)
                enrollment = CourseEnrollment.get_enrollment(
                    request.user, course_key)
                # Only bail if they have an enrollment and it's old -- if they don't have an enrollment, we want to do
                # normal bucketing -- consider the case where the experiment has bits that show before you enroll. We
                # want to keep your bucketing stable before and after you do enroll.
                if enrollment and enrollment.created < start_date:
                    return self._cache_bucket(experiment_name, 0)

        bucket = stable_bucketing_hash_group(experiment_name, self.num_buckets,
                                             request.user.username)

        # Now check if the user is forced into a particular bucket, using our subordinate bucket flags
        for i, bucket_flag in enumerate(self.bucket_flags):
            if bucket_flag.is_enabled(course_key):
                bucket = i
                break

        session_key = 'tracked.{}'.format(experiment_name)
        if track and hasattr(request,
                             'session') and session_key not in request.session:
            segment.track(user_id=request.user.id,
                          event_name='edx.bi.experiment.user.bucketed',
                          properties={
                              'site': request.site.domain,
                              'app_label': self.waffle_namespace.name,
                              'experiment': self.flag_name,
                              'course_id':
                              str(course_key) if course_key else None,
                              'bucket': bucket,
                              'is_staff': request.user.is_staff,
                              'nonInteraction': 1,
                          })

            # Mark that we've recorded this bucketing, so that we don't do it again this session
            request.session[session_key] = True

        return self._cache_bucket(experiment_name, bucket)
Example #40
0
    def get_template_context(self, user, user_schedules):
        course_id_strs = []
        course_links = []
        first_valid_upsell_context = None
        first_schedule = None
        first_expiration_date = None

        self.log_info(u"Found %s schedules for %s", len(user_schedules), user.username)

        for schedule in user_schedules:
            upsell_context = _get_upsell_information_for_schedule(user, schedule)
            if not upsell_context['show_upsell']:
                self.log_info(u"No upsell available for %r", schedule.enrollment)
                continue

            if not CourseDurationLimitConfig.enabled_for_enrollment(enrollment=schedule.enrollment):
                self.log_info(u"course duration limits not enabled for %r", schedule.enrollment)
                continue

            expiration_date = get_user_course_expiration_date(user, schedule.enrollment.course)
            if expiration_date is None:
                self.log_info(u"No course expiration date for %r", schedule.enrollment.course)
                continue

            if first_valid_upsell_context is None:
                first_schedule = schedule
                first_valid_upsell_context = upsell_context
                first_expiration_date = expiration_date
            course_id_str = str(schedule.enrollment.course_id)
            course_id_strs.append(course_id_str)
            course_links.append({
                'url': _get_trackable_course_home_url(schedule.enrollment.course_id),
                'name': schedule.enrollment.course.display_name
            })

        if first_schedule is None:
            self.log_info(u'No courses eligible for upgrade for user %s.', user.username)
            raise InvalidContextError()

        # Experiment code: Skip users who are in the control bucket
        hash_bucket = stable_bucketing_hash_group('fbe_access_expiry_reminder', 2, user.username)
        properties = {
            'site': self.site.domain,  # pylint: disable=no-member
            'app_label': 'course_duration_limits',
            'nonInteraction': 1,
            'bucket': hash_bucket,
            'experiment': 'REVMI-95',
        }
        course_ids = course_id_strs
        properties['num_courses'] = len(course_ids)
        if course_ids:
            properties['course_ids'] = course_ids[:10]
            properties['primary_course_id'] = course_ids[0]

        tracking_context = {
            'host': self.site.domain,  # pylint: disable=no-member
            'path': '/',  # make up a value, in order to allow the host to be passed along.
        }
        # I wonder if the user of this event should be the recipient, as they are not the ones
        # who took an action.  Rather, the system is acting, and they are the object.
        # Admittedly that may be what 'nonInteraction' is meant to address.  But sessionization may
        # get confused by these events if they're attributed in this way, because there's no way for
        # this event to get context that would match with what the user might be doing at the moment.
        # But the events do show up in GA being joined up with existing sessions (i.e. within a half
        # hour in the past), so they don't always break sessions.  Not sure what happens after these.
        # We can put the recipient_user_id into the properties, and then export as a custom dimension.
        with tracker.get_tracker().context('course_duration_limits', tracking_context):
            segment.track(
                user_id=user.id,
                event_name='edx.bi.experiment.user.bucketed',
                properties=properties,
            )
        if hash_bucket == 0:
            raise InvalidContextError()

        context = {
            'course_links': course_links,
            'first_course_name': first_schedule.enrollment.course.display_name,
            'cert_image': static('course_experience/images/verified-cert.png'),
            'course_ids': course_id_strs,
            'first_course_expiration_date': first_expiration_date.strftime(_(u"%b. %d, %Y")),
            'time_until_expiration': timeuntil(first_expiration_date.date(), now=datetime.utcnow().date())
        }
        context.update(first_valid_upsell_context)
        return context
Example #41
0
    def get_bucket(self, course_key=None, track=True):
        """
        Return which bucket number the specified user is in.

        The user may be force-bucketed if matching subordinate flags of the form
        "main_flag.BUCKET_NUM" exist. Otherwise, they will be hashed into a default
        bucket based on their username, the experiment name, and the course-run key.

        If `self.use_course_aware_bucketing` is False, the course-run key will
        be omitted from the hashing formula, thus making it so a given user
        has the same default bucket across all course runs; however, subordinate
        flags that match the course-run key will still apply.

        If `course_key` argument is omitted altogether, then subordinate flags
        will be evaluated outside of the course-run context, and the default bucket
        will be calculated as if `self.use_course_aware_bucketing` is False.

        Finally, Bucket 0 is assumed to be the control bucket and will be returned if the
        experiment is not enabled for this user and course.

        Arguments:
            course_key (Optional[CourseKey])
            track (bool):
                Whether an analytics event should be generated if the user is
                bucketed for the first time.

        Returns: int
        """
        # Keep some imports in here, because this class is commonly used at a module level, and we want to avoid
        # circular imports for any models.
        from experiments.models import ExperimentKeyValue
        from lms.djangoapps.courseware.masquerade import get_specific_masquerading_user

        request = get_current_request()
        if not request:
            return 0

        if not hasattr(request, 'user') or not request.user.id:
            # We need username for stable bucketing and id for tracking, so just skip anonymous (not-logged-in) users
            return 0

        user = get_specific_masquerading_user(request.user, course_key)
        if user is None:
            user = request.user
            masquerading_as_specific_student = False
        else:
            masquerading_as_specific_student = True

        # If a course key is passed in, include it in the experiment name
        # in order to separate caches and analytics calls per course-run.
        # If we are using course-aware bucketing, then also append that course key
        # to `bucketing_group_name`, such that users can be hashed into different
        # buckets for different course-runs.
        experiment_name = bucketing_group_name = self.namespaced_flag_name
        if course_key:
            experiment_name += ".{}".format(course_key)
        if course_key and self.use_course_aware_bucketing:
            bucketing_group_name += ".{}".format(course_key)

        # Check if we have a cache for this request already
        request_cache = RequestCache('experiments')
        cache_response = request_cache.get_cached_response(experiment_name)
        if cache_response.is_found:
            return cache_response.value

        # Check if the main flag is even enabled for this user and course.
        if not self.is_experiment_on(
                course_key):  # grabs user from the current request, if any
            return self._cache_bucket(experiment_name, 0)

        # Check if the enrollment should even be considered (if it started before the experiment wants, we ignore)
        if course_key and self.experiment_id is not None:
            values = ExperimentKeyValue.objects.filter(
                experiment_id=self.experiment_id).values('key', 'value')
            values = {pair['key']: pair['value'] for pair in values}

            if not self._is_enrollment_inside_date_bounds(
                    values, user, course_key):
                return self._cache_bucket(experiment_name, 0)

        # Determine the user's bucket.
        # First check if forced into a particular bucket, using our subordinate bucket flags.
        # If not, calculate their default bucket using a consistent hash function.
        for i, bucket_flag in enumerate(self.bucket_flags):
            if bucket_flag.is_enabled(course_key):
                bucket = i
                break
        else:
            bucket = stable_bucketing_hash_group(bucketing_group_name,
                                                 self.num_buckets,
                                                 user.username)

        session_key = 'tracked.{}'.format(experiment_name)
        if (track and hasattr(request, 'session')
                and session_key not in request.session
                and not masquerading_as_specific_student):
            segment.track(user_id=user.id,
                          event_name='edx.bi.experiment.user.bucketed',
                          properties={
                              'site': request.site.domain,
                              'app_label': self.waffle_namespace.name,
                              'experiment': self.flag_name,
                              'course_id':
                              str(course_key) if course_key else None,
                              'bucket': bucket,
                              'is_staff': user.is_staff,
                              'nonInteraction': 1,
                          })

            # Mark that we've recorded this bucketing, so that we don't do it again this session
            request.session[session_key] = True

        return self._cache_bucket(experiment_name, bucket)