def emit_event(self, event_name): """ Emits an event to explicitly track course enrollment and unenrollment. """ try: context = contexts.course_context_from_course_id(self.course_id) assert(isinstance(self.course_id, CourseKey)) data = { 'user_id': self.user.id, 'course_id': self.course_id.to_deprecated_string(), 'mode': self.mode, } with tracker.get_tracker().context(event_name, context): tracker.emit(event_name, data) if settings.FEATURES.get('SEGMENT_IO_LMS') and settings.SEGMENT_IO_LMS_KEY: tracking_context = tracker.get_tracker().resolve_context() analytics.track(self.user_id, event_name, { 'category': 'conversion', 'label': self.course_id.to_deprecated_string(), 'org': self.course_id.org, 'course': self.course_id.course, 'run': self.course_id.run, 'mode': self.mode, }, context={ 'Google Analytics': { 'clientId': tracking_context.get('client_id') } }) except: # pylint: disable=bare-except if event_name and self.course_id: log.exception('Unable to emit event %s for user %s and course %s', event_name, self.user.username, self.course_id)
def process_request(self, request): """ Add a user's tags to the tracking event context. """ match = COURSE_REGEX.match(request.build_absolute_uri()) course_id = None if match: course_id = match.group('course_id') context = {} if course_id: context['course_id'] = course_id if request.user.is_authenticated(): context['course_user_tags'] = dict( UserCourseTag.objects.filter( user=request.user.pk, course_id=course_id ).values_list('key', 'value') ) else: context['course_user_tags'] = {} tracker.get_tracker().enter_context( self.CONTEXT_NAME, context )
def process_request(self, request): """ Add a user's tags to the tracking event context. """ match = COURSE_REGEX.match(request.path) course_key = None if match: course_key = match.group('course_id') try: course_key = CourseKey.from_string(course_key) except InvalidKeyError: course_key = None context = {} if course_key: try: context['course_id'] = course_key.to_deprecated_string() except AttributeError: context['course_id'] = unicode(course_key) if request.user.is_authenticated(): context['course_user_tags'] = dict( UserCourseTag.objects.filter( user=request.user.pk, course_id=course_key, ).values_list('key', 'value') ) else: context['course_user_tags'] = {} tracker.get_tracker().enter_context( self.CONTEXT_NAME, context )
def process_request(self, request): """ Add a user's tags to the tracking event context. """ match = COURSE_REGEX.match(request.build_absolute_uri()) course_id = None if match: course_id = match.group('course_id') try: course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) except InvalidKeyError: course_id = None course_key = None context = {} if course_id: context['course_id'] = course_id if request.user.is_authenticated(): context['course_user_tags'] = dict( UserCourseTag.objects.filter( user=request.user.pk, course_id=course_key, ).values_list('key', 'value') ) else: context['course_user_tags'] = {} tracker.get_tracker().enter_context( self.CONTEXT_NAME, context )
def enter_request_context(self, request): """ Extract information from the request and add it to the tracking context. The following fields are injected into the context: * session - The Django session key that identifies the user's session. * user_id - The numeric ID for the logged in user. * username - The username of the logged in user. * ip - The IP address of the client. * host - The "SERVER_NAME" header, which should be the name of the server running this code. * agent - The client browser identification string. * path - The path part of the requested URL. * client_id - The unique key used by Google Analytics to identify a user """ context = { "session": self.get_session_key(request), "user_id": self.get_user_primary_key(request), "username": self.get_username(request), } for header_name, context_key in META_KEY_TO_CONTEXT_KEY.iteritems(): context[context_key] = request.META.get(header_name, "") # Google Analytics uses the clientId to keep track of unique visitors. A GA cookie looks like # this: _ga=GA1.2.1033501218.1368477899. The clientId is this part: 1033501218.1368477899. google_analytics_cookie = request.COOKIES.get("_ga") if google_analytics_cookie is None: context["client_id"] = None else: context["client_id"] = ".".join(google_analytics_cookie.split(".")[2:]) context.update(contexts.course_context_from_url(request.build_absolute_uri())) tracker.get_tracker().enter_context(CONTEXT_NAME, context)
def process_response(self, request, response): """Exit the context if it exists.""" try: tracker.get_tracker().exit_context(CONTEXT_NAME) except Exception: # pylint: disable=broad-except pass # Set spam query string into cookie spam = request.GET.get('spam') if spam: response.set_cookie('spam', spam) # track session changed try: origin_session = request.COOKIES.get('sessionid') if origin_session and request.session.session_key and request.session.session_key != origin_session: session_md5 = hashlib.md5() session_md5.update(request.session.session_key) origin_session_md5 = hashlib.md5() origin_session_md5.update(origin_session) event = { 'changed_session': session_md5.hexdigest(), 'origin_session': origin_session_md5.hexdigest(), } views.server_track(request, request.META['PATH_INFO'], event) except Exception, e: pass
def user_track(request): """ Log when POST call to "event" URL is made by a user. Uses request.REQUEST to allow for GET calls. GET or POST call should provide "event_type", "event", and "page" arguments. """ try: # TODO: Do the same for many of the optional META parameters username = request.user.username except: username = "******" page = request.REQUEST['page'] with eventtracker.get_tracker().context('edx.course.browser', contexts.course_context_from_url(page)): context = eventtracker.get_tracker().resolve_context() event = { "username": username, "session": context.get('session', ''), "ip": _get_request_header(request, 'REMOTE_ADDR'), "event_source": "browser", "event_type": request.REQUEST['event_type'], "event": request.REQUEST['event'], "agent": _get_request_header(request, 'HTTP_USER_AGENT'), "page": page, "time": datetime.datetime.utcnow(), "host": _get_request_header(request, 'SERVER_NAME'), "context": context, } log_event(event) return HttpResponse('success')
def enter_request_context(self, request): """ Extract information from the request and add it to the tracking context. The following fields are injected in to the context: * session - The Django session key that identifies the user's session. * user_id - The numeric ID for the logged in user. * username - The username of the logged in user. * ip - The IP address of the client. * host - The "SERVER_NAME" header, which should be the name of the server running this code. * agent - The client browser identification string. * path - The path part of the requested URL. """ context = { 'session': self.get_session_key(request), 'user_id': self.get_user_primary_key(request), 'username': self.get_username(request), } for header_name, context_key in META_KEY_TO_CONTEXT_KEY.iteritems(): context[context_key] = request.META.get(header_name, '') context.update(contexts.course_context_from_url(request.build_absolute_uri())) tracker.get_tracker().enter_context( CONTEXT_NAME, context )
def emit_event(self, event_name): """ Emits an event to explicitly track course enrollment and unenrollment. """ try: context = contexts.course_context_from_course_id(self.course_id) assert isinstance(self.course_id, CourseKey) data = {"user_id": self.user.id, "course_id": self.course_id.to_deprecated_string(), "mode": self.mode} with tracker.get_tracker().context(event_name, context): tracker.emit(event_name, data) if settings.FEATURES.get("SEGMENT_IO_LMS") and settings.SEGMENT_IO_LMS_KEY: tracking_context = tracker.get_tracker().resolve_context() analytics.track( self.user_id, event_name, { "category": "conversion", "label": self.course_id.to_deprecated_string(), "org": self.course_id.org, "course": self.course_id.course, "run": self.course_id.run, "mode": self.mode, }, context={"Google Analytics": {"clientId": tracking_context.get("client_id")}}, ) except: # pylint: disable=bare-except if event_name and self.course_id: log.exception( "Unable to emit event %s for user %s and course %s", event_name, self.user.username, self.course_id )
def process_response(self, _request, response): """Exit the context if it exists.""" try: tracker.get_tracker().exit_context(CONTEXT_NAME) except Exception: # pylint: disable=broad-except pass return response
def process_response(self, request, response): # pylint: disable=unused-argument """Exit the context if it exists.""" try: tracker.get_tracker().exit_context(self.CONTEXT_NAME) except: # pylint: disable=bare-except pass return response
def enter_course_context(self, request): """ Extract course information from the request and add it to the tracking context. """ tracker.get_tracker().enter_context( COURSE_CONTEXT_NAME, contexts.course_context_from_url(request.build_absolute_uri()) )
def get_context_for_request(self, request): """Extract the generated event tracking context for the given request.""" self.track_middleware.process_request(request) try: captured_context = tracker.get_tracker().resolve_context() finally: self.track_middleware.process_response(request, None) self.assertEquals(tracker.get_tracker().resolve_context(), {}) return captured_context
def test_request_in_course_context(self): request = self.request_factory.get('/courses/test_org/test_course/test_run/foo') self.track_middleware.process_request(request) self.assertEquals( tracker.get_tracker().resolve_context(), { 'course_id': 'test_org/test_course/test_run', 'org_id': 'test_org' } ) self.track_middleware.process_response(request, None) self.assertEquals( tracker.get_tracker().resolve_context(), {} )
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 )
def _track_reverification_events(self, event_name, user_id, course_id, checkpoint): # pylint: disable=invalid-name """Track re-verification events for a user against a reverification checkpoint of a course. Arguments: event_name (str): Name of event being tracked user_id (str): The ID of the user course_id (unicode): ID associated with the course checkpoint (str): Checkpoint name Returns: None """ log.info( u"In-course reverification: event %s occurred for user '%s' in course '%s' at checkpoint '%s'", event_name, user_id, course_id, checkpoint ) if settings.FEATURES.get('SEGMENT_IO_LMS') and hasattr(settings, 'SEGMENT_IO_LMS_KEY'): tracking_context = tracker.get_tracker().resolve_context() analytics.track( user_id, event_name, { 'category': "verification", 'label': unicode(course_id), 'checkpoint': checkpoint }, context={ 'Google Analytics': { 'clientId': tracking_context.get('client_id') } } )
def _track_reverification_events(self, event_name, user_id, course_id, checkpoint): # pylint: disable=invalid-name """Track re-verification events for user against course checkpoints Arguments: user_id (str): The ID of the user generting the certificate. course_id (unicode): id associated with the course checkpoint (str): checkpoint name Returns: None """ if settings.FEATURES.get('SEGMENT_IO_LMS') and hasattr(settings, 'SEGMENT_IO_LMS_KEY'): tracking_context = tracker.get_tracker().resolve_context() analytics.track( user_id, event_name, { 'category': "verification", 'label': unicode(course_id), 'checkpoint': checkpoint }, context={ 'Google Analytics': { 'clientId': tracking_context.get('client_id') } } )
def server_track(request, event_type, event, page=None): """ Log events related to server requests. Handle the situation where the request may be NULL, as may happen with management commands. """ if event_type.startswith("/event_logs") and request.user.is_staff: return # don't log try: username = request.user.username except: username = "******" # define output: event = { "username": username, "ip": _get_request_header(request, 'REMOTE_ADDR'), "event_source": "server", "event_type": event_type, "event": event, "agent": _get_request_header(request, 'HTTP_USER_AGENT'), "page": page, "time": datetime.datetime.now(UTC), "host": _get_request_header(request, 'SERVER_NAME'), "context": eventtracker.get_tracker().resolve_context(), } log_event(event)
def create_tracking_context(user): """ Assembles attributes from user and request objects to be sent along in ecommerce api calls for tracking purposes. """ return { 'lms_user_id': user.id, 'lms_client_id': tracker.get_tracker().resolve_context().get('client_id') }
def server_track(request, event_type, event, page=None): """Log events related to server requests.""" try: username = request.user.username except: username = "******" try: agent = request.META['HTTP_USER_AGENT'] except: agent = '' event = { "username": username, "ip": request.META['REMOTE_ADDR'], "event_source": "server", "event_type": event_type, "event": event, "agent": agent, "page": page, "time": datetime.datetime.now(UTC), "host": request.META['SERVER_NAME'], "context": eventtracker.get_tracker().resolve_context(), } if event_type.startswith("/event_logs") and request.user.is_staff: return # don't log log_event(event)
def login_analytics(strategy, auth_entry, *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 and hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY: tracking_context = tracker.get_tracker().resolve_context() analytics.track( kwargs['user'].id, event_name, { 'category': "conversion", 'label': None, 'provider': getattr(kwargs['backend'], 'name') }, context={ 'ip': tracking_context.get('ip'), 'Google Analytics': { 'clientId': tracking_context.get('client_id') } } )
def login_analytics(*args, **kwargs): """ Sends login info to Segment.io """ event_name = None action_to_event_name = { 'is_login': '******', 'is_dashboard': 'edx.bi.user.account.linked', 'is_profile': 'edx.bi.user.account.linked', } # Note: we assume only one of the `action` kwargs (is_dashboard, is_login) to be # `True` at any given time for action in action_to_event_name.keys(): if kwargs.get(action): event_name = action_to_event_name[action] if event_name is not None: registration_course_id = kwargs['request'].session.get('registration_course_id') tracking_context = tracker.get_tracker().resolve_context() analytics.track( kwargs['user'].id, event_name, { 'category': "conversion", 'label': registration_course_id, 'provider': getattr(kwargs['backend'], 'name') }, context={ 'Google Analytics': { 'clientId': tracking_context.get('client_id') } } )
def test_user_track(self): request = self.request_factory.get('/event', { 'page': self.url_with_course, 'event_type': sentinel.event_type, 'event': {} }) with tracker.get_tracker().context('edx.request', {'session': sentinel.session}): views.user_track(request) expected_event = { 'username': '******', 'session': sentinel.session, 'ip': '127.0.0.1', 'event_source': 'browser', 'event_type': str(sentinel.event_type), 'event': '{}', 'agent': '', 'page': self.url_with_course, 'time': expected_time, 'host': 'testserver', 'context': { 'course_id': 'foo/bar/baz', 'org_id': 'foo', }, } self.mock_tracker.send.assert_called_once_with(expected_event)
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, )
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' tracking_context = tracker.get_tracker().resolve_context() analytics.track( user_id, event_name, { 'category': 'communication', 'label': organization }, context={ 'ip': tracking_context.get('ip'), 'Google Analytics': { 'clientId': tracking_context.get('client_id') } } )
def _emit_event(name, context, data): """ Do the actual integration into the event-tracker """ try: if context: # try to parse out the org_id from the course_id if 'course_id' in context: try: course_key = CourseKey.from_string(context['course_id']) context['org_id'] = course_key.org except InvalidKeyError: # leave org_id blank pass with tracker.get_tracker().context(name, context): tracker.emit(name, data) else: # if None is passed in then we don't construct the 'with' context stack tracker.emit(name, data) except KeyError: # This happens when a default tracker has not been registered by the host application # aka LMS. This is normal when running unit tests in isolation. log.warning( 'Analytics tracker not properly configured. ' 'If this message appears in a production environment, please investigate' )
def server_track(request, event_type, event, page=None): """ Log events related to server requests. Handle the situation where the request may be NULL, as may happen with management commands. """ if event_type.startswith("/event_logs") and request.user.is_staff: return # don't log try: username = request.user.username except: username = "******" # define output: event = { "username": username, "ip": _get_request_ip(request), "referer": _get_request_header(request, 'HTTP_REFERER'), "accept_language": _get_request_header(request, 'HTTP_ACCEPT_LANGUAGE'), "event_source": "server", "event_type": event_type, "event": event, "agent": _get_request_header(request, 'HTTP_USER_AGENT').decode('latin1'), "page": page, "time": datetime.datetime.utcnow().replace(tzinfo=pytz.utc), "host": _get_request_header(request, 'SERVER_NAME'), "context": eventtracker.get_tracker().resolve_context(), } # Some duplicated fields are passed into event-tracking via the context by track.middleware. # Remove them from the event here since they are captured elsewhere. shim.remove_shim_context(event) log_event(event)
def _emit_grade_calculated_event(grade): """ Emits an edx.grades.subsection.grade_calculated event with data from the passed grade. """ # TODO: remove this context manager after completion of AN-6134 event_name = u'edx.grades.subsection.grade_calculated' context = contexts.course_context_from_course_id(grade.course_id) with tracker.get_tracker().context(event_name, context): tracker.emit( event_name, { 'user_id': unicode(grade.user_id), 'course_id': unicode(grade.course_id), 'block_id': unicode(grade.usage_key), 'course_version': unicode(grade.course_version), 'weighted_total_earned': grade.earned_all, 'weighted_total_possible': grade.possible_all, 'weighted_graded_earned': grade.earned_graded, 'weighted_graded_possible': grade.possible_graded, 'first_attempted': unicode(grade.first_attempted), 'subtree_edited_timestamp': unicode(grade.subtree_edited_timestamp), 'event_transaction_id': unicode(get_event_transaction_id()), 'event_transaction_type': unicode(get_event_transaction_type()), 'visible_blocks_hash': unicode(grade.visible_blocks_id), } )
def user_track(request): """ Log when POST call to "event" URL is made by a user. GET or POST call should provide "event_type", "event", and "page" arguments. """ try: username = request.user.username except: username = "******" name = _get_request_value(request, 'event_type') data = _get_request_value(request, 'event', {}) page = _get_request_value(request, 'page') if isinstance(data, six.string_types) and len(data) > 0: try: data = json.loads(data) _add_user_id_for_username(data) except ValueError: pass context_override = contexts.course_context_from_url(page) context_override['username'] = username context_override['event_source'] = 'browser' context_override['page'] = page with eventtracker.get_tracker().context('edx.course.browser', context_override): eventtracker.emit(name=name, data=data) return HttpResponse('success')
def _track_user_login(user, request): """ Sends a tracking event for a successful login. """ if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY: tracking_context = tracker.get_tracker().resolve_context() analytics.identify( user.id, { 'email': request.POST['email'], 'username': user.username }, { # Disable MailChimp because we don't want to update the user's email # and username in MailChimp on every page load. We only need to capture # this data on registration/activation. 'MailChimp': False } ) analytics.track( user.id, "edx.bi.user.account.authenticated", { 'category': "conversion", 'label': request.POST.get('course_id'), 'provider': None }, context={ 'ip': tracking_context.get('ip'), 'Google Analytics': { 'clientId': tracking_context.get('client_id') } } )
def task_track(request_info, task_info, event_type, event, page=None): """ Logs tracking information for events occuring within celery tasks. The `event_type` is a string naming the particular event being logged, while `event` is a dict containing whatever additional contextual information is desired. The `request_info` is a dict containing information about the original task request. Relevant keys are `username`, `ip`, `agent`, and `host`. While the dict is required, the values in it are not, so that {} can be passed in. In addition, a `task_info` dict provides more information about the current task, to be stored with the `event` dict. This may also be an empty dict. The `page` parameter is optional, and allows the name of the page to be provided. """ # supplement event information with additional information # about the task in which it is running. full_event = dict(event, **task_info) # All fields must be specified, in case the tracking information is # also saved to the TrackingLog model. Get values from the task-level # information, or just add placeholder values. with eventtracker.get_tracker().context('edx.course.task', contexts.course_context_from_url(page)): event = { "username": request_info.get('username', 'unknown'), "ip": request_info.get('ip', 'unknown'), "event_source": "task", "event_type": event_type, "event": full_event, "agent": request_info.get('agent', 'unknown'), "page": page, "time": datetime.datetime.utcnow().replace(tzinfo=pytz.utc), "host": request_info.get('host', 'unknown'), "context": eventtracker.get_tracker().resolve_context(), } log_event(event)
def enter_request_context(self, request): """ Extract information from the request and add it to the tracking context. The following fields are injected into the context: * session - The Django session key that identifies the user's session. * user_id - The numeric ID for the logged in user. * username - The username of the logged in user. * ip - The IP address of the client. * host - The "SERVER_NAME" header, which should be the name of the server running this code. * agent - The client browser identification string. * path - The path part of the requested URL. * client_id - The unique key used by Google Analytics to identify a user """ context = { 'session': self.get_session_key(request), 'user_id': self.get_user_primary_key(request), 'username': self.get_username(request), 'ip': self.get_request_ip_address(request), } for header_name, context_key in META_KEY_TO_CONTEXT_KEY.iteritems(): # HTTP headers may contain Latin1 characters. Decoding using Latin1 encoding here # avoids encountering UnicodeDecodeError exceptions when these header strings are # output to tracking logs. context[context_key] = request.META.get(header_name, '').decode('latin1') # Google Analytics uses the clientId to keep track of unique visitors. A GA cookie looks like # this: _ga=GA1.2.1033501218.1368477899. The clientId is this part: 1033501218.1368477899. google_analytics_cookie = request.COOKIES.get('_ga') if google_analytics_cookie is None: context['client_id'] = request.META.get('HTTP_X_EDX_GA_CLIENT_ID') else: context['client_id'] = '.'.join( google_analytics_cookie.split('.')[2:]) context.update( contexts.course_context_from_url(request.build_absolute_uri())) tracker.get_tracker().enter_context(CONTEXT_NAME, context)
def log_event_to_tracking_log(self, block, event_type, event_data): """ Log this XBlock event to the tracking log """ log_context = track.contexts.context_dict_for_learning_context(block.scope_ids.usage_id.context_key) if self.user_id: log_context['user_id'] = self.user_id log_context['asides'] = {} track_function = make_track_function() with tracker.get_tracker().context(event_type, log_context): track_function(event_type, event_data)
def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix): """ Invoke an XBlock handler, either authenticated or not. Arguments: request (HttpRequest): the current request course_id (str): A string of the form org/course/run usage_id (str): A string of the form i4x://org/course/category/name@revision handler (str): The name of the handler to invoke suffix (str): The suffix to pass to the handler when invoked """ # Check submitted files files = request.FILES or {} error_msg = _check_files_limits(files) if error_msg: return JsonResponse(object={ 'success': False, 'msg': error_msg }, status=413) instance, tracking_context = _get_module_by_usage_id( request, course_id, usage_id) tracking_context_name = 'module_callback_handler' req = django_to_webob_request(request) try: with tracker.get_tracker().context(tracking_context_name, tracking_context): resp = instance.handle(handler, req, suffix) except NoSuchHandlerError: log.exception("XBlock %s attempted to access missing handler %r", instance, handler) raise Http404 # If we can't find the module, respond with a 404 except NotFoundError: log.exception("Module indicating to user that request doesn't exist") raise Http404 # For XModule-specific errors, we log the error and respond with an error message except ProcessingError as err: log.warning("Module encountered an error while processing AJAX call", exc_info=True) return JsonResponse(object={'success': err.args[0]}, status=200) # If any other error occurred, re-raise it to trigger a 500 response except Exception: log.exception("error executing xblock handler") raise return webob_to_django_response(resp)
def emit_event(self, event_name): """ Emits an event to explicitly track course enrollment and unenrollment. """ try: context = contexts.course_context_from_course_id(self.course_id) assert (isinstance(self.course_id, CourseKey)) data = { 'user_id': self.user.id, 'course_id': self.course_id.to_deprecated_string(), 'mode': self.mode, } with tracker.get_tracker().context(event_name, context): tracker.emit(event_name, data) if settings.FEATURES.get( 'SEGMENT_IO_LMS') and settings.SEGMENT_IO_LMS_KEY: tracking_context = tracker.get_tracker().resolve_context() analytics.track( self.user_id, event_name, { 'category': 'conversion', 'label': self.course_id.to_deprecated_string(), 'org': self.course_id.org, 'course': self.course_id.course, 'run': self.course_id.run, 'mode': self.mode, }, context={ 'Google Analytics': { 'clientId': tracking_context.get('client_id') } }) except: # pylint: disable=bare-except if event_name and self.course_id: log.exception( 'Unable to emit event %s for user %s and course %s', event_name, self.user.username, self.course_id)
def user_track(request): """ Log when POST call to "event" URL is made by a user. Uses request.REQUEST to allow for GET calls. GET or POST call should provide "event_type", "event", and "page" arguments. """ try: # TODO: Do the same for many of the optional META parameters username = request.user.username except: username = "******" try: scookie = request.META['HTTP_COOKIE'] # Get cookies scookie = ";".join([ c.split('=')[1] for c in scookie.split(";") if "sessionid" in c ]).strip() # Extract session ID except: scookie = "" page = request.REQUEST['page'] with eventtracker.get_tracker().context( 'edx.course.browser', contexts.course_context_from_url(page)): event = { "username": username, "session": scookie, "ip": _get_request_header(request, 'REMOTE_ADDR'), "event_source": "browser", "event_type": request.REQUEST['event_type'], "event": request.REQUEST['event'], "agent": _get_request_header(request, 'HTTP_USER_AGENT'), "page": page, "time": datetime.datetime.now(UTC), "host": _get_request_header(request, 'SERVER_NAME'), "context": eventtracker.get_tracker().resolve_context(), } log_event(event) return HttpResponse('success')
def user_track(request): """ Log when POST call to "event" URL is made by a user. Uses request.REQUEST to allow for GET calls. GET or POST call should provide "event_type", "event", and "page" arguments. """ try: # TODO: Do the same for many of the optional META parameters username = request.user.username except: username = "******" page = _get_request_value(request, 'page') with eventtracker.get_tracker().context( 'edx.course.browser', contexts.course_context_from_url(page)): context = eventtracker.get_tracker().resolve_context() event = { "username": username, "session": context.get('session', ''), "ip": _get_request_header(request, 'REMOTE_ADDR'), "referer": _get_request_header(request, 'HTTP_REFERER'), "accept_language": _get_request_header(request, 'HTTP_ACCEPT_LANGUAGE'), "event_source": "browser", "event_type": _get_request_value(request, 'event_type'), "event": _get_request_value(request, 'event'), "agent": _get_request_header(request, 'HTTP_USER_AGENT'), "page": page, "time": datetime.datetime.utcnow(), "host": _get_request_header(request, 'SERVER_NAME'), "context": context, } # Some duplicated fields are passed into event-tracking via the context by track.middleware. # Remove them from the event here since they are captured elsewhere. shim.remove_shim_context(event) log_event(event) return HttpResponse('success')
def _track_user_registration(user, profile, params, third_party_provider): """ Track the user's registration. """ if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY: tracking_context = tracker.get_tracker().resolve_context() identity_args = [ user.id, { '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), } ] if hasattr(settings, 'MAILCHIMP_NEW_USER_LIST_ID'): identity_args.append( {"MailChimp": { "listId": settings.MAILCHIMP_NEW_USER_LIST_ID }}) analytics.identify(*identity_args) analytics.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 }, context={ 'ip': tracking_context.get('ip'), 'Google Analytics': { 'clientId': tracking_context.get('client_id') } })
def test_request_with_user(self): request = self.request_factory.get('/courses/') request.user = User(pk=1) self.track_middleware.process_request(request) self.addCleanup(self.track_middleware.process_response, request, None) self.assertEquals( tracker.get_tracker().resolve_context(), { 'course_id': '', 'org_id': '', 'user_id': 1 } )
def publish(block, event_type, event): """A function that allows XModules to publish events.""" if event_type == 'grade' and not is_masquerading_as_specific_student(user, course_id): handle_grade_event(block, event_type, event) else: aside_context = {} for aside in block.runtime.get_asides(block): if hasattr(aside, 'get_event_context'): aside_event_info = aside.get_event_context(event_type, event) if aside_event_info is not None: aside_context[aside.scope_ids.block_type] = aside_event_info with tracker.get_tracker().context('asides', {'asides': aside_context}): track_function(event_type, event)
def create_tracking_context(user): """ Assembles attributes from user and request objects to be sent along in E-Commerce API calls for tracking purposes. """ context_tracker = tracker.get_tracker().resolve_context() # Updated by Mahendra from lms.djangoapps.reg_form.views import userdetails ucountry = userdetails(user.id) usercountry = ucountry.rcountry return { 'lms_user_id': user.id, 'lms_ip': context_tracker.get('ip'), 'lms_client_id': context_tracker.get('client_id'), 'lms_user_country': usercountry, }
def emit_certificate_event(event_name, user, course_id, course_overview=None, event_data=None): """ Utility function responsible for emitting certificate events. We currently track the following events: - `edx.certificate.created` - Emit when a course certificate with the `downloadable` status has been awarded to a learner. - `edx.certificate.revoked`- Emit when a course certificate with the `downloadable` status has been taken away from a learner. - `edx.certificate.shared` - Emit when a learner shares their course certificate to social media (LinkedIn, Facebook, or Twitter). - `edx.certificate.evidence_visited` - Emit when a user (other than the learner who owns a certificate) views a course certificate (e.g., someone views a course certificate shared on a LinkedIn profile). Args: event_name (String) - Text describing the action/event that we are tracking. Examples include `revoked`, `created`, etc. user (User) - The User object of the learner associated with this event. course_id (CourseLocator) - The course-run key associated with this event. course_overview (CourseOverview) - Optional. The CourseOverview of the course-run associated with this event. event_data (dictionary) - Optional. Dictionary containing any additional data we want to be associated with an event. """ event_name = '.'.join(['edx', 'certificate', event_name]) if not course_overview: course_overview = get_course_overview_or_none(course_id) context = {'org_id': course_overview.org, 'course_id': str(course_id)} data = { 'user_id': user.id, 'course_id': str(course_id), 'certificate_url': get_certificate_url(user.id, course_id, uuid=event_data['certificate_id']) } event_data = event_data or {} event_data.update(data) with tracker.get_tracker().context(event_name, context): tracker.emit(event_name, event_data)
def process_request(self, request): """ Add a user's tags to the tracking event context. """ match = COURSE_REGEX.match(request.build_absolute_uri()) course_id = None if match: course_id = match.group('course_id') context = {} if course_id: context['course_id'] = course_id if request.user.is_authenticated(): context['course_user_tags'] = dict( UserCourseTag.objects.filter( user=request.user.pk, course_id=course_id).values_list('key', 'value')) else: context['course_user_tags'] = {} tracker.get_tracker().enter_context(self.CONTEXT_NAME, context)
def track(user_id, event_name, properties=None, context=None, traits=None): """ Wrapper for emitting Segment track event, including augmenting context information from middleware. """ if event_name is not None and hasattr( settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY: properties = properties or {} segment_context = dict(context) if context else {} tracking_context = tracker.get_tracker().resolve_context() if 'ip' not in segment_context and 'ip' in tracking_context: segment_context['ip'] = tracking_context.get('ip') if ('Google Analytics' not in segment_context or 'clientId' not in segment_context['Google Analytics']) and 'client_id' in tracking_context: # lint-amnesty, pylint: disable=line-too-long segment_context['Google Analytics'] = { 'clientId': tracking_context.get('client_id') } if 'userAgent' not in segment_context and 'agent' in tracking_context: segment_context['userAgent'] = tracking_context.get('agent') path = tracking_context.get('path') referer = tracking_context.get('referer') page = tracking_context.get('page') if path and not page: # Try to put together a url from host and path, hardcoding the schema. # (Segment doesn't care about the schema for GA, but will extract the host and path from the url.) host = tracking_context.get('host') if host: parts = ("https", host, path, "", "") page = urlunsplit(parts) if path is not None or referer is not None or page is not None: if 'page' not in segment_context: segment_context['page'] = {} if path is not None and 'path' not in segment_context['page']: segment_context['page']['path'] = path if referer is not None and 'referrer' not in segment_context[ 'page']: segment_context['page']['referrer'] = referer if page is not None and 'url' not in segment_context['page']: segment_context['page']['url'] = page if traits: segment_context['traits'] = traits analytics.track(user_id, event_name, properties, segment_context)
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. """ if getattr(settings, 'LMS_SEGMENT_KEY'): tracking_context = tracker.get_tracker().resolve_context() analytics.track(user_id, event_name, properties, context={ 'ip': tracking_context.get('ip'), 'Google Analytics': { 'clientId': tracking_context.get('client_id') } })
def server_track(request, event_type, event, page=None): """ Log events related to server requests. Handle the situation where the request may be NULL, as may happen with management commands. """ if event_type.startswith("/event_logs") and request.user.is_staff: return # don't log try: username = request.user.username except: username = "******" # define output: event = { "username": username, "ip": _get_request_ip(request), "referer": _get_request_header(request, 'HTTP_REFERER'), "accept_language": _get_request_header(request, 'HTTP_ACCEPT_LANGUAGE'), "event_source": "server", "event_type": event_type, "event": event, "agent": _get_request_header(request, 'HTTP_USER_AGENT').encode().decode('latin1'), "page": page, "time": datetime.datetime.utcnow().replace(tzinfo=pytz.utc), "host": _get_request_header(request, 'SERVER_NAME'), "context": eventtracker.get_tracker().resolve_context(), } # Some duplicated fields are passed into event-tracking via the context by track.middleware. # Remove them from the event here since they are captured elsewhere. shim.remove_shim_context(event) log_event(event)
def course_grade_now_failed(user, course_id): """ Emits an edx.course.grade.now_failed event with data from the course and user failed now . """ event_name = COURSE_GRADE_NOW_FAILED_EVENT_TYPE context = contexts.course_context_from_course_id(course_id) with tracker.get_tracker().context(event_name, context): tracker.emit( event_name, { 'user_id': str(user.id), 'course_id': str(course_id), 'event_transaction_id': str(get_event_transaction_id()), 'event_transaction_type': str(get_event_transaction_type()) } )
def publish_event(event_name, result, **kwargs): """ Helper function to publish an event for analytics purposes """ event_data = { "location": unicode(location), "previous_count": previous_count, "result": result, "max_count": max_count, } event_data.update(kwargs) context = contexts.course_context_from_course_id(location.course_key) if user_id: context['user_id'] = user_id full_event_name = "edx.librarycontentblock.content.{}".format(event_name) with tracker.get_tracker().context(full_event_name, context): tracker.emit(full_event_name, event_data)
def emit_event(self, event_name): """ Emits an event to explicitly track course enrollment and unenrollment. """ try: context = contexts.course_context_from_course_id(self.course_id) data = { 'user_id': self.user.id, 'course_id': self.course_id, 'mode': self.mode, } with tracker.get_tracker().context(event_name, context): tracker.emit(event_name, data) except: # pylint: disable=bare-except if event_name and self.course_id: log.exception('Unable to emit event %s for user %s and course %s', event_name, self.user.username, self.course_id)
def course_grade_passed_first_time(user_id, course_id): """ Emits an event edx.course.grade.passed.first_time with data from the passed course_grade. """ event_name = COURSE_GRADE_PASSED_FIRST_TIME_EVENT_TYPE context = contexts.course_context_from_course_id(course_id) # TODO (AN-6134): remove this context manager with tracker.get_tracker().context(event_name, context): tracker.emit( event_name, { 'user_id': str(user_id), 'course_id': str(course_id), 'event_transaction_id': str(get_event_transaction_id()), 'event_transaction_type': str(get_event_transaction_type()) } )
def track(user_id, event_name, properties=None, context=None): """Wrapper for emitting Segment track event, including augmenting context information from middleware.""" if event_name is not None and hasattr( settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY: properties = properties or {} segment_context = dict(context) if context else {} tracking_context = tracker.get_tracker().resolve_context() if 'ip' not in segment_context and 'ip' in tracking_context: segment_context['ip'] = tracking_context.get('ip') if ('Google Analytics' not in segment_context or 'clientId' not in segment_context['Google Analytics'] ) and 'client_id' in tracking_context: segment_context['Google Analytics'] = { 'clientId': tracking_context.get('client_id') } if 'userAgent' not in segment_context and 'agent' in tracking_context: segment_context['userAgent'] = tracking_context.get('agent') path = tracking_context.get('path') referer = tracking_context.get('referer') page = tracking_context.get('page') if path and not page: # Try to put together a url from host and path: host = tracking_context.get('host') if host: page = urljoin("//{host}".format(host=host), path) if path is not None or referer is not None or page is not None: if 'page' not in segment_context: segment_context['page'] = {} if path is not None and 'path' not in segment_context['page']: segment_context['page']['path'] = path if referer is not None and 'referrer' not in segment_context[ 'page']: segment_context['page']['referrer'] = referer if page is not None and 'url' not in segment_context['page']: segment_context['page']['url'] = page analytics.track(user_id, event_name, properties, segment_context)
def user_track(request): """ Log when POST call to "event" URL is made by a user. GET or POST call should provide "event_type", "event", and "page" arguments. """ try: username = request.user.username except: username = "******" name = _get_request_value(request, 'event_type') data = _get_request_value(request, 'event', {}) page = _get_request_value(request, 'page') if isinstance(data, six.string_types) and len(data) > 0: try: data = json.loads(data) _add_user_id_for_username(data) except ValueError: pass context_override = contexts.course_context_from_url(page) context_override['username'] = username context_override['event_source'] = 'browser' context_override['page'] = page if settings.LRS_ENDPOINT: lrs_data = { 'activity_time': timezone.now().isoformat(), 'actor': request.user.id, 'verb': name, 'activity_object': page, 'extra_data': json.dumps(data, default=str), } attempt_to_store_lrs_record.apply_async(args=[lrs_data], queue=settings.LRS_QUEUE) with eventtracker.get_tracker().context('edx.course.browser', context_override): eventtracker.emit(name=name, data=data) return HttpResponse('success')
def publish(block, event_type, event): """ A function that allows XModules to publish events. """ handle_event = get_event_handler(event_type) if handle_event and not is_masquerading_as_specific_student(user, course_id): handle_event(block, event) else: context = contexts.course_context_from_course_id(course_id) if block.runtime.user_id: context['user_id'] = block.runtime.user_id context['asides'] = {} for aside in block.runtime.get_asides(block): if hasattr(aside, 'get_event_context'): aside_event_info = aside.get_event_context(event_type, event) if aside_event_info is not None: context['asides'][aside.scope_ids.block_type] = aside_event_info with tracker.get_tracker().context(event_type, context): track_function(event_type, event)
def publish(block, event_type, event): """A function that allows XModules to publish events.""" if event_type == 'grade': handle_grade_event(block, event_type, event) elif event_type == 'progress': # expose another special case event type which gets sent # into the CourseCompletions models handle_progress_event(block, event_type, event) else: context = contexts.course_context_from_course_id(course_id) if block.runtime.user_id: context['user_id'] = block.runtime.user_id context['asides'] = {} for aside in block.runtime.get_asides(block): if hasattr(aside, 'get_event_context'): aside_event_info = aside.get_event_context(event_type, event) if aside_event_info is not None: context['asides'][aside.scope_ids.block_type] = aside_event_info with tracker.get_tracker().context(event_type, context): track_function(event_type, event)
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 """ if settings.LMS_SEGMENT_KEY: tracking_context = tracker.get_tracker().resolve_context() context = { 'ip': tracking_context.get('ip'), 'Google Analytics': { 'clientId': tracking_context.get('client_id') } } analytics.track(user.id, event_name, parameters, context=context)
def emit_certificate_event(event_name, user, course_id, course=None, event_data=None): """ Emits certificate event. """ event_name = '.'.join(['edx', 'certificate', event_name]) if course is None: course = modulestore().get_course(course_id, depth=0) context = { 'org_id': course.org, 'course_id': unicode(course_id) } data = { 'user_id': user.id, 'course_id': unicode(course_id), 'certificate_url': get_certificate_url(user.id, course_id) } event_data = event_data or {} event_data.update(data) with tracker.get_tracker().context(event_name, context): tracker.emit(event_name, event_data)
def login_analytics(strategy, auth_entry, *args, **kwargs): """ Sends login info to Segment.io """ event_name = None if auth_entry in [AUTH_ENTRY_LOGIN, AUTH_ENTRY_LOGIN_2]: 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: tracking_context = tracker.get_tracker().resolve_context() analytics.track(kwargs['user'].id, event_name, { 'category': "conversion", 'label': None, 'provider': getattr(kwargs['backend'], 'name') }, context={ 'Google Analytics': { 'clientId': tracking_context.get('client_id') } })
def subsection_grade_calculated(subsection_grade): """ Emits an edx.grades.subsection.grade_calculated event with data from the passed subsection_grade. """ event_name = SUBSECTION_GRADE_CALCULATED context = contexts.course_context_from_course_id( subsection_grade.course_id) # TODO (AN-6134): remove this context manager with tracker.get_tracker().context(event_name, context): tracker.emit( event_name, { 'user_id': unicode(subsection_grade.user_id), 'course_id': unicode(subsection_grade.course_id), 'block_id': unicode(subsection_grade.usage_key), 'course_version': unicode(subsection_grade.course_version), 'weighted_total_earned': subsection_grade.earned_all, 'weighted_total_possible': subsection_grade.possible_all, 'weighted_graded_earned': subsection_grade.earned_graded, 'weighted_graded_possible': subsection_grade.possible_graded, 'first_attempted': unicode(subsection_grade.first_attempted), 'subtree_edited_timestamp': unicode(subsection_grade.subtree_edited_timestamp), 'event_transaction_id': unicode(get_event_transaction_id()), 'event_transaction_type': unicode(get_event_transaction_type()), 'visible_blocks_hash': unicode(subsection_grade.visible_blocks_id), })
def login_analytics(strategy, *args, **kwargs): """ Sends login info to Segment.io """ event_name = None action_to_event_name = { 'is_login': '******', 'is_dashboard': 'edx.bi.user.account.linked', 'is_profile': 'edx.bi.user.account.linked', # Backwards compatibility: during an A/B test for the combined # login/registration form, we introduced a new login end-point. # Since users may continue to have this in their sessions after # the test concludes, we need to continue accepting this action. 'is_login_2': 'edx.bi.user.account.authenticated', } # Note: we assume only one of the `action` kwargs (is_dashboard, is_login) to be # `True` at any given time for action in action_to_event_name.keys(): if kwargs.get(action): event_name = action_to_event_name[action] if event_name is not None: tracking_context = tracker.get_tracker().resolve_context() analytics.track( kwargs['user'].id, event_name, { 'category': "conversion", 'label': strategy.session_get('enroll_course_id'), 'provider': getattr(kwargs['backend'], 'name') }, context={ 'Google Analytics': { 'clientId': tracking_context.get('client_id') } } )
def _emit_grade_calculated_event(grade): """ Emits an edx.grades.subsection.grade_calculated event with data from the passed grade. """ # TODO: remove this context manager after completion of AN-6134 event_name = u'edx.grades.subsection.grade_calculated' context = contexts.course_context_from_course_id(grade.course_id) with tracker.get_tracker().context(event_name, context): tracker.emit( event_name, { 'user_id': unicode(grade.user_id), 'course_id': unicode(grade.course_id), 'block_id': unicode(grade.usage_key), 'course_version': unicode(grade.course_version), 'weighted_total_earned': grade.earned_all, 'weighted_total_possible': grade.possible_all, 'weighted_graded_earned': grade.earned_graded, 'weighted_graded_possible': grade.possible_graded, 'first_attempted': unicode(grade.first_attempted), 'subtree_edited_timestamp': unicode(grade.subtree_edited_timestamp), 'event_transaction_id': unicode(get_event_transaction_id()), 'event_transaction_type': unicode(get_event_transaction_type()), 'visible_blocks_hash': unicode(grade.visible_blocks_id), })