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)
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 }, )
def _is_in_holdback_and_bucket(user): """ Return whether the specified user is in the first-purchase-discount holdback group. This will also stable bucket the user. """ if datetime(2020, 8, 1, tzinfo=pytz.UTC) <= datetime.now(tz=pytz.UTC): return False # Holdback is 10% bucket = stable_bucketing_hash_group(DISCOUNT_APPLICABILITY_HOLDBACK, 10, user) 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
def _track_message_sent(site, user, msg): # lint-amnesty, pylint: disable=missing-function-docstring 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, )
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
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': str(message.uuid), 'send_uuid': str(message.send_uuid), 'thread_id': context['thread_id'], 'course_id': str(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)
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 # lint-amnesty, pylint: disable=unused-variable expected_segment_context = {} self.assertEqual((sentinel.user_id, sentinel.name, self.properties, expected_segment_context), args)
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 }, )
def emit_course_goal_event(sender, instance, **kwargs): # lint-amnesty, pylint: disable=unused-argument """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)
def emit_webinar_registration_event(user_id, topic): """ Emit an event for a user's webinar registration Arguments: user_id (id): Id of the registering user topic (str): Topic of the registered webinar """ segment.track(user_id, WEBINAR_REGISTRATION_EVENT, {'topic': topic})
def _track_user_registration(user, profile, params, third_party_provider, registration): """ 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': str(profile.country), 'email_subscribe': 'unsubscribed' if settings.MARKETING_EMAILS_OPT_IN and params.get('marketing_emails_opt_in') == 'false' else 'subscribed', } ] # .. 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) properties = { '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, 'is_gender_selected': bool(profile.gender_display), 'is_year_of_birth_selected': bool(profile.year_of_birth), 'is_education_selected': bool(profile.level_of_education_display), 'is_goal_set': bool(profile.goals), 'total_registration_time': round(float(params.get('totalRegistrationTime', '0'))), 'activation_key': registration.activation_key if registration else None, } # VAN-738 - added below properties to experiment marketing emails opt in/out events on Braze. if params.get('marketing_emails_opt_in') and settings.MARKETING_EMAILS_OPT_IN: properties['marketing_emails_opt_in'] = params.get('marketing_emails_opt_in') == 'true' # DENG-803: For segment events forwarded along to Hubspot, duplicate the `properties` section of # the event payload into the `traits` section so that they can be received. This is a temporary # fix until we implement this behavior outside of the LMS. # TODO: DENG-805: remove the properties duplication in the event traits. segment_traits = dict(properties) segment_traits['user_id'] = user.id segment_traits['joined_date'] = user.date_joined.strftime("%Y-%m-%d") segment.track( user.id, "edx.bi.user.account.registered", properties=properties, traits=segment_traits, )
def emit_course_goal_event(sender, instance, **kwargs): # lint-amnesty, pylint: disable=unused-argument """Emit events for both tracking logs and for Segment.""" name = 'edx.course.goal.added' if kwargs.get( 'created', False) else 'edx.course.goal.updated' properties = { 'courserun_key': str(instance.course_key), 'days_per_week': instance.days_per_week, 'subscribed_to_reminders': instance.subscribed_to_reminders, } tracker.emit(name, properties) segment.track(instance.user.id, name, properties)
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)
def emit_registration_event(user): """ Track and Identify required attributes when a new user is created Arguments: user (User): User to track """ user_properties = { 'email': user.email, 'username': user.username, 'name': user.profile.name, 'city': user.profile.city, } segment.identify(user.id, user_properties) segment.track(user.id, USER_REGISTRATION_EVENT, user_properties)
def login_analytics(strategy, auth_entry, current_partial=None, *args, **kwargs): # lint-amnesty, pylint: disable=keyword-arg-before-vararg """ 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 })
def emit_course_status_event_for_program_prereq_courses(user_id, course, course_status): """ Emit the progress status for Program prereq courses i.e Started or Completed Arguments: user_id (id): Id of the user for which to emit the Omni course progress update course (CourseOverview): Course for which to emit the event for course_status (str): The status of the given course """ course_name_status_key = f'{course.id.course}Status' segment.identify(user_id, {course_name_status_key: course_status}) course_update_event_name = f'ProgramPrereq{course_status}' segment.track(user_id, course_update_event_name, { 'course': course.id.course, 'label': text_type(course.id), 'org': course.id.org, 'run': course.id.run, })
def get_celebrations_dict(user, enrollment, course, browser_timezone): """ Returns a dict of celebrations that should be performed. """ if not enrollment: return { 'first_section': False, 'streak_length_to_celebrate': None, 'streak_discount_enabled': False, } streak_length_to_celebrate = UserCelebration.perform_streak_updates( user, course.id, browser_timezone) celebrations = { 'first_section': CourseEnrollmentCelebration.should_celebrate_first_section(enrollment), 'streak_length_to_celebrate': streak_length_to_celebrate, 'streak_discount_enabled': False, } if streak_length_to_celebrate: # We only want to offer the streak discount # if the course has not ended, is upgradeable and the user is not an enterprise learner if can_show_streak_discount_coupon(user, course): # Send course streak coupon event course_key = str(course.id) modes_dict = CourseMode.modes_for_course_dict( course_id=course_key, include_expired=False) verified_mode = modes_dict.get('verified', None) if verified_mode: celebrations['streak_discount_enabled'] = True segment.track( user_id=user.id, event_name='edx.bi.course.streak_discount_enabled', properties={ 'course_id': str(course_key), 'sku': verified_mode.sku, }) return celebrations
def emit_user_info_update(user, application_step): """ Emit an event to update the user information at Segment after different steps of the application. Arguments: user (User): User for which to emit the event application_step (str): Step of the application to update the user information for """ if application_step == CONTACT_INFO: attributes = { 'isSaudi': user.extended_profile.saudi_national, 'age': user.extended_profile.age, 'heardAboutUs': user.extended_profile.hear_about_omni, } elif application_step == EXPERIENCE: first_education = user.application.educations.first() education_start_date, education_end_date = _get_start_date_and_end_date(first_education) attributes = { 'school': first_education.name_of_school, 'degree': first_education.get_degree_display(), 'educationStartDate': education_start_date, 'educationEndDate': education_end_date, } first_experience = user.application.workexperiences.first() if first_experience: experience_start_date, experience_end_date = _get_start_date_and_end_date(first_experience) attributes.update({ 'workOrganization': first_experience.name_of_organization, 'jobTitle': first_experience.job_position_title, 'workStartDate': experience_start_date, 'workEndDate': experience_end_date }) else: attributes = { 'businessLine': user.application.business_line.title } segment.identify(user.id, attributes) segment.track(user.id, APPLICATION_EVENTS.get(application_step), attributes)
def check_pwned_password_and_send_track_event(user_id, password, internal_user=False): """ Check the Pwned Databases and send its event to Segment """ try: password = hashlib.sha1(password.encode('utf-8')).hexdigest() pwned_response = PwnedPasswordsAPI.range(password) if pwned_response is not None: properties = get_pwned_properties(pwned_response, password) properties['internal_user'] = internal_user segment.track(user_id, 'edx.bi.user.pwned.password.status', properties) except Exception: # pylint: disable=W0703 log.exception( 'Unable to get response from pwned password api for user_id: "%s"', user_id, ) return None # lint-amnesty, pylint: disable=raise-missing-from
def check_pwned_password_and_send_track_event(user_id, password, internal_user=False, is_new_user=False): """ Check the Pwned Databases and send its event to Segment. """ try: pwned_properties = check_pwned_password(password) if pwned_properties: pwned_properties['internal_user'] = internal_user pwned_properties['new_user'] = is_new_user segment.track(user_id, 'edx.bi.user.pwned.password.status', pwned_properties) except Exception: # pylint: disable=W0703 log.exception( 'Unable to get response from pwned password api for user_id: "%s"', user_id, ) return None # lint-amnesty, pylint: disable=raise-missing-from
def emit_application_progress(user): """ Emit the progress for the application of the given user. The progress statuses can be Started, Submitted, Waitlisted and Accepted. Arguments: user (User): User to emit progress update for """ application_progress = user.application_hub.application_progress application_status = APPLICATION_PROGRESS.get(application_progress, APPLICATION_PROGRESS[APPLICATION_STARTED]) segment.identify(user.id, {'applicationStatus': application_status}) application_event = APPLICATION_EVENTS.get(application_progress, APPLICATION_EVENTS[APPLICATION_STARTED]) event_properties = { 'applicationStatus': application_status } if hasattr(user, 'application'): event_properties['applicationId'] = user.application.id segment.track(user.id, application_event, event_properties)
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 emit_course_progress_event(user, course, grade=None): """ Emit the progress of a course when a course is Started, Completed or Failed. Also calls `emit_course_status_event_for_program_prereq_courses` function to emit an event if a program prereq course is started or passed. Arguments: user (User): User associated with the course course (CourseOverview): The course for which we are emitting the progress grade (PersistentCourseGrade): The grade for the given course and user """ from openedx.adg.lms.applications.helpers import has_attempted_all_modules is_program_prereq_course = ( hasattr(course, 'multilingual_course') and course.multilingual_course.multilingual_course_group.is_program_prerequisite ) course_language_name = get_language_info(course.language).get('name', course.language) course_properties = {'courseName': course.id.course, 'courseLanguage': course_language_name} if grade: course_properties['letterGrade'] = text_type(grade.letter_grade) course_properties['percentageGrade'] = convert_float_point_to_percentage(grade.percent_grade) if grade.passed_timestamp and grade.letter_grade: course_event = COURSE_COMPLETED if is_program_prereq_course: emit_course_status_event_for_program_prereq_courses(user.id, course, COURSE_COMPLETED) else: course_event = COURSE_FAILED if has_attempted_all_modules(user, course) else None else: course_event = COURSE_STARTED if is_program_prereq_course: emit_course_status_event_for_program_prereq_courses(user.id, course, COURSE_STARTED) if course_event: segment.track(user.id, course_event, course_properties)
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 # lint-amnesty, pylint: disable=unused-variable 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)
def emit_event(self, user, program, suggested_course_run, completed_course_run): """ Emit the Segment event which will be used by Braze to send the email """ event_properties = { 'COURSE_ONE_NAME': completed_course_run['title'], 'PROGRAM_TYPE': program['type'], 'PROGRAM_TITLE': program['title'], 'COURSE_TWO_NAME': suggested_course_run['title'], 'COURSE_TWO_SHORT_DESCRIPTION': suggested_course_run['short_description'], 'COURSE_TWO_LINK': urljoin(settings.MKTG_URLS.get('ROOT'), suggested_course_run['marketing_url']), 'COURSE_TWO_IMAGE_LINK': suggested_course_run['image'].get('src'), } segment.track(user.id, 'edx.bi.program.course-enrollment.nudge', event_properties) LOGGER.info( '[Program Course Nudge Email] Segment event fired to suggested. ' 'Completed Course: [%s], Program: [%s], Suggested Course: [%s], User: [%s].', completed_course_run['key'], program['uuid'], suggested_course_run['key'], user.username, )
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 # lint-amnesty, pylint: disable=unused-variable assert (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 assert (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) assert self.mock_segment_track.called args, kwargs = self.mock_segment_track.call_args assert (sentinel.user_id, sentinel.name, self.properties, provided_context) == args
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)
def test_missing_name(self): segment.track(sentinel.user_id, None, self.properties) assert not self.mock_segment_track.called
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 = can_show_verified_upgrade(user, 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) 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, })
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 lms.djangoapps.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)