diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index b495d8b63c0c31cd8baca02dfb0367260a76c9e5..c1d7f74cc25adad5845d8b904f926960706f04b3 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -28,7 +28,7 @@ from django.contrib.auth.hashers import make_password
 from django.contrib.auth.signals import user_logged_in, user_logged_out
 from django.db import models, IntegrityError
 from django.db.models import Count
-from django.db.models.signals import pre_save
+from django.db.models.signals import pre_save, post_save
 from django.dispatch import receiver, Signal
 from django.core.exceptions import ObjectDoesNotExist
 from django.utils.translation import ugettext_noop
@@ -37,6 +37,7 @@ from config_models.models import ConfigurationModel
 from track import contexts
 from eventtracking import tracker
 from importlib import import_module
+from model_utils import FieldTracker
 
 from opaque_keys.edx.locations import SlashSeparatedCourseKey
 
@@ -57,6 +58,7 @@ UNENROLL_DONE = Signal(providing_args=["course_enrollment", "skip_refund"])
 log = logging.getLogger(__name__)
 AUDIT_LOG = logging.getLogger("audit")
 SessionStore = import_module(settings.SESSION_ENGINE).SessionStore  # pylint: disable=invalid-name
+USER_SETTINGS_CHANGED_EVENT_NAME = 'edx.user.settings.changed'
 
 
 class AnonymousUserId(models.Model):
@@ -253,6 +255,9 @@ class UserProfile(models.Model):
     bio = models.CharField(blank=True, null=True, max_length=3000, db_index=False)
     profile_image_uploaded_at = models.DateTimeField(null=True)
 
+    # Use FieldTracker to track changes to model instances.
+    tracker = FieldTracker()
+
     @property
     def has_profile_image(self):
         """
@@ -332,6 +337,50 @@ def user_profile_pre_save_callback(sender, **kwargs):
         user_profile.profile_image_uploaded_at = None
 
 
+@receiver(post_save, sender=UserProfile)
+def user_profile_post_save_callback(sender, **kwargs):
+    """
+    Emit analytics events after saving the UserProfile.
+    """
+    user_profile = kwargs['instance']
+    # pylint: disable=protected-access
+    emit_field_changed_events(
+        user_profile, user_profile.user, USER_SETTINGS_CHANGED_EVENT_NAME, sender._meta.db_table, ['meta']
+    )
+
+
+def emit_field_changed_events(instance, user, event_name, db_table, excluded_fields=None):
+    """ For the given model instance, save the fields that have changed since the last save.
+
+    Requires that the Model uses 'model_utils.FieldTracker' and has assigned it to 'tracker'.
+
+    Args:
+        instance (Model instance): the model instance that is being saved
+        user (User): the user that this instance is associated with
+        event_name (str): the name of the event to be emitted
+        db_table (str): the name of the table that we're modifying
+        excluded_fields (list): a list of field names for which events should
+            not be emitted
+
+    Returns:
+        None
+    """
+    excluded_fields = excluded_fields or []
+    changed_fields = instance.tracker.changed()
+    for field in changed_fields:
+        if field not in excluded_fields:
+            tracker.emit(
+                event_name,
+                {
+                    "setting": field,
+                    'old': changed_fields[field],
+                    'new': getattr(instance, field),
+                    "user_id": user.id,
+                    "table": db_table
+                }
+            )
+
+
 class UserSignupSource(models.Model):
     """
     This table contains information about users registering
diff --git a/common/djangoapps/student/tests/test_events.py b/common/djangoapps/student/tests/test_events.py
new file mode 100644
index 0000000000000000000000000000000000000000..463e0b34328e9384425e25ef09ce3cec8bded21b
--- /dev/null
+++ b/common/djangoapps/student/tests/test_events.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+"""
+Test that various events are fired for models in the student app.
+"""
+from django.test import TestCase
+
+from student.tests.factories import UserFactory
+from student.tests.tests import UserProfileEventTestMixin
+
+
+class TestUserProfileEvents(UserProfileEventTestMixin, TestCase):
+    """
+    Test that we emit field change events when UserProfile models are changed.
+    """
+    def setUp(self):
+        super(TestUserProfileEvents, self).setUp()
+        self.user = UserFactory.create()
+        self.profile = self.user.profile
+        self.reset_tracker()
+
+    def test_change_one_field(self):
+        """
+        Verify that we emit an event when a single field changes on the user
+        profile.
+        """
+        self.profile.year_of_birth = 1900
+        self.profile.save()
+        self.assert_profile_event_emitted(setting='year_of_birth', old=None, new=self.profile.year_of_birth)
+
+    def test_change_many_fields(self):
+        """
+        Verify that we emit one event per field when many fields change on the
+        user profile in one transaction.
+        """
+        self.profile.gender = u'o'
+        self.profile.bio = 'test bio'
+        self.profile.save()
+        self.assert_profile_event_emitted(setting='bio', old=None, new=self.profile.bio)
+        self.assert_profile_event_emitted(setting='gender', old=u'm', new=u'o')
+
+    def test_unicode(self):
+        """
+        Verify that the events we emit can handle unicode characters.
+        """
+        old_name = self.profile.name
+        self.profile.name = u'Dånîél'
+        self.profile.save()
+        self.assert_profile_event_emitted(setting='name', old=old_name, new=self.profile.name)
diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py
index 65b2b7a61dcf3e2c5a92fc6aed4bd25f8a05ae25..b282e526b1f8b482399953d2e81dc678e337cdd2 100644
--- a/common/djangoapps/student/tests/tests.py
+++ b/common/djangoapps/student/tests/tests.py
@@ -22,11 +22,12 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
 
 from student.models import (
     anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment, unique_id_for_user,
-    LinkedInAddToProfileConfiguration
+    LinkedInAddToProfileConfiguration, USER_SETTINGS_CHANGED_EVENT_NAME
 )
 from student.views import (process_survey_link, _cert_info,
                            change_enrollment, complete_course_mode_info)
 from student.tests.factories import UserFactory, CourseModeFactory
+from util.testing import EventTestMixin
 from xmodule.modulestore.tests.factories import CourseFactory
 from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
 
@@ -485,19 +486,31 @@ class DashboardTest(ModuleStoreTestCase):
         self.assertContains(response, expected_url)
 
 
-class EnrollmentEventTestMixin(object):
-    """ Mixin with assertions for validating enrollment events. """
-
+class UserProfileEventTestMixin(EventTestMixin):
+    """
+    Mixin for verifying that UserProfile events were emitted during a test.
+    """
     def setUp(self):
-        super(EnrollmentEventTestMixin, self).setUp()
-        patcher = patch('student.models.tracker')
-        self.mock_tracker = patcher.start()
-        self.addCleanup(patcher.stop)
+        super(UserProfileEventTestMixin, self).setUp('student.models.tracker')
 
-    def assert_no_events_were_emitted(self):
-        """Ensures no events were emitted since the last event related assertion"""
-        self.assertFalse(self.mock_tracker.emit.called)  # pylint: disable=maybe-no-member
-        self.mock_tracker.reset_mock()
+    def assert_profile_event_emitted(self, **kwargs):
+        """
+        Helper method to assert that we emit the expected user settings events.
+
+        Expected settings are passed in via `kwargs`.
+        """
+        self.assert_event_emitted(
+            USER_SETTINGS_CHANGED_EVENT_NAME,
+            table='auth_userprofile',
+            user_id=self.user.id,
+            **kwargs
+        )
+
+
+class EnrollmentEventTestMixin(EventTestMixin):
+    """ Mixin with assertions for validating enrollment events. """
+    def setUp(self):
+        super(EnrollmentEventTestMixin, self).setUp('student.models.tracker')
 
     def assert_enrollment_mode_change_event_was_emitted(self, user, course_key, mode):
         """Ensures an enrollment mode change event was emitted"""
diff --git a/common/test/acceptance/tests/lms/test_account_settings.py b/common/test/acceptance/tests/lms/test_account_settings.py
index 5aca9f089b9d3b8255166e3e89449963b3817a1d..e047ae56999ac9341109cedcb6e6388a8f705dbd 100644
--- a/common/test/acceptance/tests/lms/test_account_settings.py
+++ b/common/test/acceptance/tests/lms/test_account_settings.py
@@ -9,6 +9,7 @@ from bok_choy.web_app_test import WebAppTest
 from ...pages.lms.account_settings import AccountSettingsPage
 from ...pages.lms.auto_auth import AutoAuthPage
 from ...pages.lms.dashboard import DashboardPage
+from ..helpers import EventsTestMixin
 
 from ..helpers import EventsTestMixin
 
@@ -206,6 +207,8 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest):
             [u'another name', self.USERNAME],
         )
 
+        self.assert_event_emitted_num_times('edx.user.settings.changed', self.start_time, self.user_id, 2)
+
     def test_email_field(self):
         """
         Test behaviour of "Email" field.
@@ -287,6 +290,7 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest):
             u'',
             [u'Bachelor\'s degree', u''],
         )
+        self.assert_event_emitted_num_times('edx.user.settings.changed', self.start_time, self.user_id, 2)
 
     def test_gender_field(self):
         """
@@ -298,11 +302,13 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest):
             u'',
             [u'Female', u''],
         )
+        self.assert_event_emitted_num_times('edx.user.settings.changed', self.start_time, self.user_id, 2)
 
     def test_year_of_birth_field(self):
         """
         Test behaviour of "Year of Birth" field.
         """
+        # Note that when we clear the year_of_birth here we're firing an event.
         self.assertEqual(self.account_settings_page.value_for_dropdown_field('year_of_birth', ''), '')
         self._test_dropdown_field(
             u'year_of_birth',
@@ -310,6 +316,7 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest):
             u'',
             [u'1980', u''],
         )
+        self.assert_event_emitted_num_times('edx.user.settings.changed', self.start_time, self.user_id, 3)
 
     def test_country_field(self):
         """
diff --git a/lms/djangoapps/commerce/tests/test_views.py b/lms/djangoapps/commerce/tests/test_views.py
index 3b525689cdd631f55fab2797e1a8a1ab0dbe84be..f73110174bfeec0ad6df3213b74877e74980c25e 100644
--- a/lms/djangoapps/commerce/tests/test_views.py
+++ b/lms/djangoapps/commerce/tests/test_views.py
@@ -77,6 +77,9 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
                 sku=uuid4().hex.decode('ascii')
             )
 
+        # Ignore events fired from UserFactory creation
+        self.reset_tracker()
+
     def test_login_required(self):
         """
         The view should return HTTP 403 status if the user is not logged in.
diff --git a/openedx/core/djangoapps/profile_images/tests/test_views.py b/openedx/core/djangoapps/profile_images/tests/test_views.py
index fa3dc913d1e065d5b1a44eb1893f26c1c7e8353c..daa31daaafdb9ae8d96d29c5dc36c3c5f407307f 100644
--- a/openedx/core/djangoapps/profile_images/tests/test_views.py
+++ b/openedx/core/djangoapps/profile_images/tests/test_views.py
@@ -14,6 +14,7 @@ from PIL import Image
 from rest_framework.test import APITestCase, APIClient
 
 from student.tests.factories import UserFactory
+from student.tests.tests import UserProfileEventTestMixin
 
 from ...user_api.accounts.image_helpers import (
     set_has_profile_image,
@@ -28,7 +29,7 @@ TEST_PASSWORD = "test"
 TEST_UPLOAD_DT = datetime.datetime(2002, 1, 9, 15, 43, 01, tzinfo=UTC)
 
 
-class ProfileImageEndpointTestCase(APITestCase):
+class ProfileImageEndpointTestCase(UserProfileEventTestMixin, APITestCase):
     """
     Base class / shared infrastructure for tests of profile_image "upload" and
     "remove" endpoints.
@@ -50,6 +51,10 @@ class ProfileImageEndpointTestCase(APITestCase):
         # assume user.profile.has_profile_image is False by default
         self.assertFalse(self.user.profile.has_profile_image)
 
+        # Reset the mock event tracker so that we're not considering the
+        # initial profile creation events.
+        self.reset_tracker()
+
     def tearDown(self):
         super(ProfileImageEndpointTestCase, self).tearDown()
         for name in get_profile_image_names(self.user.username).values():
@@ -104,6 +109,15 @@ class ProfileImageUploadTestCase(ProfileImageEndpointTestCase):
     """
     _view_name = "profile_image_upload"
 
+    def check_upload_event_emitted(self):
+        """
+        Make sure we emit a UserProfile event corresponding to the
+        profile_image_uploaded_at field changing.
+        """
+        self.assert_profile_event_emitted(
+            setting='profile_image_uploaded_at', old=None, new=TEST_UPLOAD_DT
+        )
+
     def test_unsupported_methods(self, mock_log):
         """
         Test that GET, PUT, PATCH, and DELETE are not supported.
@@ -113,6 +127,7 @@ class ProfileImageUploadTestCase(ProfileImageEndpointTestCase):
         self.assertEqual(405, self.client.patch(self.url).status_code)
         self.assertEqual(405, self.client.delete(self.url).status_code)
         self.assertFalse(mock_log.info.called)
+        self.assert_no_events_were_emitted()
 
     def test_anonymous_access(self, mock_log):
         """
@@ -122,6 +137,7 @@ class ProfileImageUploadTestCase(ProfileImageEndpointTestCase):
         response = anonymous_client.post(self.url)
         self.assertEqual(401, response.status_code)
         self.assertFalse(mock_log.info.called)
+        self.assert_no_events_were_emitted()
 
     @patch('openedx.core.djangoapps.profile_images.views._make_upload_dt', return_value=TEST_UPLOAD_DT)
     def test_upload_self(self, mock_make_image_version, mock_log):  # pylint: disable=unused-argument
@@ -137,12 +153,15 @@ class ProfileImageUploadTestCase(ProfileImageEndpointTestCase):
             LOG_MESSAGE_CREATE,
             {'image_names': get_profile_image_names(self.user.username).values(), 'user_id': self.user.id}
         )
+        self.check_upload_event_emitted()
 
     def test_upload_other(self, mock_log):
         """
         Test that an authenticated user cannot POST to another user's upload endpoint.
         """
         different_user = UserFactory.create(password=TEST_PASSWORD)
+        # Ignore UserProfileFactory creation events.
+        self.reset_tracker()
         different_client = APIClient()
         different_client.login(username=different_user.username, password=TEST_PASSWORD)
         with make_image_file() as image_file:
@@ -151,12 +170,15 @@ class ProfileImageUploadTestCase(ProfileImageEndpointTestCase):
             self.check_images(False)
             self.check_has_profile_image(False)
         self.assertFalse(mock_log.info.called)
+        self.assert_no_events_were_emitted()
 
     def test_upload_staff(self, mock_log):
         """
         Test that an authenticated staff cannot POST to another user's upload endpoint.
         """
         staff_user = UserFactory(is_staff=True, password=TEST_PASSWORD)
+        # Ignore UserProfileFactory creation events.
+        self.reset_tracker()
         staff_client = APIClient()
         staff_client.login(username=staff_user.username, password=TEST_PASSWORD)
         with make_image_file() as image_file:
@@ -165,6 +187,7 @@ class ProfileImageUploadTestCase(ProfileImageEndpointTestCase):
             self.check_images(False)
             self.check_has_profile_image(False)
         self.assertFalse(mock_log.info.called)
+        self.assert_no_events_were_emitted()
 
     def test_upload_missing_file(self, mock_log):
         """
@@ -179,6 +202,7 @@ class ProfileImageUploadTestCase(ProfileImageEndpointTestCase):
         self.check_images(False)
         self.check_has_profile_image(False)
         self.assertFalse(mock_log.info.called)
+        self.assert_no_events_were_emitted()
 
     def test_upload_not_a_file(self, mock_log):
         """
@@ -194,6 +218,7 @@ class ProfileImageUploadTestCase(ProfileImageEndpointTestCase):
         self.check_images(False)
         self.check_has_profile_image(False)
         self.assertFalse(mock_log.info.called)
+        self.assert_no_events_were_emitted()
 
     def test_upload_validation(self, mock_log):
         """
@@ -214,6 +239,7 @@ class ProfileImageUploadTestCase(ProfileImageEndpointTestCase):
                 self.check_images(False)
                 self.check_has_profile_image(False)
         self.assertFalse(mock_log.info.called)
+        self.assert_no_events_were_emitted()
 
     @patch('PIL.Image.open')
     def test_upload_failure(self, image_open, mock_log):
@@ -228,6 +254,7 @@ class ProfileImageUploadTestCase(ProfileImageEndpointTestCase):
             self.check_images(False)
             self.check_has_profile_image(False)
         self.assertFalse(mock_log.info.called)
+        self.assert_no_events_were_emitted()
 
 
 @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Profile Image API is only supported in LMS')
@@ -244,6 +271,17 @@ class ProfileImageRemoveTestCase(ProfileImageEndpointTestCase):
             create_profile_images(image_file, get_profile_image_names(self.user.username))
             self.check_images()
             set_has_profile_image(self.user.username, True, TEST_UPLOAD_DT)
+            # Ignore previous event
+            self.reset_tracker()
+
+    def check_remove_event_emitted(self):
+        """
+        Make sure we emit a UserProfile event corresponding to the
+        profile_image_uploaded_at field changing.
+        """
+        self.assert_profile_event_emitted(
+            setting='profile_image_uploaded_at', old=TEST_UPLOAD_DT, new=None
+        )
 
     def test_unsupported_methods(self, mock_log):
         """
@@ -254,6 +292,7 @@ class ProfileImageRemoveTestCase(ProfileImageEndpointTestCase):
         self.assertEqual(405, self.client.patch(self.url).status_code)
         self.assertEqual(405, self.client.delete(self.url).status_code)
         self.assertFalse(mock_log.info.called)
+        self.assert_no_events_were_emitted()
 
     def test_anonymous_access(self, mock_log):
         """
@@ -264,6 +303,7 @@ class ProfileImageRemoveTestCase(ProfileImageEndpointTestCase):
             response = request(self.url)
             self.assertEqual(401, response.status_code)
         self.assertFalse(mock_log.info.called)
+        self.assert_no_events_were_emitted()
 
     def test_remove_self(self, mock_log):
         """
@@ -278,6 +318,7 @@ class ProfileImageRemoveTestCase(ProfileImageEndpointTestCase):
             LOG_MESSAGE_DELETE,
             {'image_names': get_profile_image_names(self.user.username).values(), 'user_id': self.user.id}
         )
+        self.check_remove_event_emitted()
 
     def test_remove_other(self, mock_log):
         """
@@ -285,6 +326,8 @@ class ProfileImageRemoveTestCase(ProfileImageEndpointTestCase):
         profile images.
         """
         different_user = UserFactory.create(password=TEST_PASSWORD)
+        # Ignore UserProfileFactory creation events.
+        self.reset_tracker()
         different_client = APIClient()
         different_client.login(username=different_user.username, password=TEST_PASSWORD)
         response = different_client.post(self.url)
@@ -292,6 +335,7 @@ class ProfileImageRemoveTestCase(ProfileImageEndpointTestCase):
         self.check_images(True)  # thumbnails should remain intact.
         self.check_has_profile_image(True)
         self.assertFalse(mock_log.info.called)
+        self.assert_no_events_were_emitted()
 
     def test_remove_staff(self, mock_log):
         """
@@ -309,6 +353,7 @@ class ProfileImageRemoveTestCase(ProfileImageEndpointTestCase):
             LOG_MESSAGE_DELETE,
             {'image_names': get_profile_image_names(self.user.username).values(), 'user_id': self.user.id}
         )
+        self.check_remove_event_emitted()
 
     @patch('student.models.UserProfile.save')
     def test_remove_failure(self, user_profile_save, mock_log):
@@ -322,3 +367,4 @@ class ProfileImageRemoveTestCase(ProfileImageEndpointTestCase):
         self.check_images(True)  # thumbnails should remain intact.
         self.check_has_profile_image(True)
         self.assertFalse(mock_log.info.called)
+        self.assert_no_events_were_emitted()