diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js
index 7328e21fea8dcbebbbf803030bc3eb3f8b05e543..e01cb576cc4b51a08526164834a982a55081b62b 100644
--- a/resources/static/dialog/controllers/actions.js
+++ b/resources/static/dialog/controllers/actions.js
@@ -11,6 +11,7 @@ BrowserID.Modules.Actions = (function() {
       serviceManager = bid.module,
       user = bid.User,
       errors = bid.Errors,
+      mediator = bid.Mediator,
       dialogHelpers = bid.Helpers.Dialog,
       runningService,
       onsuccess,
@@ -26,6 +27,7 @@ BrowserID.Modules.Actions = (function() {
       runningService = name;
     }
 
+    mediator.publish("service", { name: name });
     bid.resize();
 
     return module;
diff --git a/resources/static/dialog/controllers/pick_email.js b/resources/static/dialog/controllers/pick_email.js
index 61e65c469480a0f29a03415f979895d3506b5abf..4382645c75ce2777cfb4008e5e50d40cb2e5bebf 100644
--- a/resources/static/dialog/controllers/pick_email.js
+++ b/resources/static/dialog/controllers/pick_email.js
@@ -89,8 +89,12 @@ BrowserID.Modules.PickEmail = (function() {
 
       dom.addClass("body", "pickemail");
 
+      var identities = getSortedIdentities();
+
+      self.publish("emails_displayed", { count: identities.length });
+
       self.renderDialog("pick_email", {
-        identities: getSortedIdentities(),
+        identities: identities,
         siteemail: storage.site.get(origin, "email"),
         privacy_url: options.privacyURL,
         tos_url: options.tosURL
diff --git a/resources/static/lib/hub.js b/resources/static/lib/hub.js
index 8da4476cb5ff89f30bac25e01a5b004868737aa9..f1be782aed42655546b7035ef3abc39aa24cc2af 100644
--- a/resources/static/lib/hub.js
+++ b/resources/static/lib/hub.js
@@ -34,6 +34,11 @@ Hub = (function() {
   }
 
   function fire(message) {
+    for(var j = 0, glistener; glistener = globalListeners[j]; ++j) {
+      // global listeners get the message name as the first argument
+      glistener.callback.apply(null, arguments);
+    }
+
     var messageListeners = listeners[message];
 
     if(messageListeners) {
@@ -45,11 +50,6 @@ Hub = (function() {
         listener.callback.apply(null, arguments);
       }
     }
-
-    for(var j = 0, glistener; glistener = globalListeners[j]; ++j) {
-      // global listeners get the message name as the first argument
-      glistener.callback.apply(null, arguments);
-    }
   }
 
   function off(id) {
diff --git a/resources/static/shared/modules/interaction_data.js b/resources/static/shared/modules/interaction_data.js
index 0ef57fd67381c5180d8e61a0734124032d6bf1bd..f9de4013f329732d62db7fe36bfe7c5423408999 100644
--- a/resources/static/shared/modules/interaction_data.js
+++ b/resources/static/shared/modules/interaction_data.js
@@ -30,6 +30,43 @@ BrowserID.Modules.InteractionData = (function() {
       dom = bid.DOM,
       sc;
 
+  /**
+   * This is a translation table from a message on the mediator to a KPI name.
+   * Names can be modified or added to the KPI storage directly.
+   * A name can be translated by using either a string or a function.
+   *
+   * value side contains - purpose
+   * null - no translation, use mediator name for KPI name.
+   * string - translate from mediator name to string.
+   * function - function takes two arguments, msg and data.  These come
+   *   directly from the mediator.  Function returns a value.  If no value is
+   *   returned, field will not be saved to KPI data set.
+   */
+  var MediatorToKPINameTable = {
+    service: function(msg, data) { return "screen." + data.name; },
+    cancel_state: "screen.cancel",
+    primary_user_authenticating: "window.redirect_to_primary",
+    window_unload: "window.unload",
+    generate_assertion: null,
+    assertion_generated: null,
+    emails_displayed: function(msg, data) { return "user.email_count:" + data.count; },
+    user_staged: "user.user_staged",
+    user_confirmed: "user.user_confirmed",
+    email_staged: "user.email_staged",
+    email_confirmed: "user.email_confrimed",
+    notme: "user.logout",
+  };
+
+  function getKPIName(msg, data) {
+    var self=this,
+        kpiInfo = self.mediatorToKPINameTable[msg];
+
+    var type = typeof kpiInfo;
+    if(kpiInfo === null) return msg;
+    if(type === "string") return kpiInfo;
+    if(type === "function") return kpiInfo(msg, data);
+  }
+
   function onSessionContext(msg, result) {
     var self=this;
 
@@ -101,10 +138,13 @@ BrowserID.Modules.InteractionData = (function() {
   }
 
 
-  function addEvent(eventName) {
+  function addEvent(msg, data) {
     var self=this;
     if (self.samplingEnabled === false) return;
 
+    var eventName = getKPIName.call(self, msg, data);
+    if (!eventName) return;
+
     var eventData = [ eventName, new Date() - self.startTime ];
     if (self.samplesBeingStored) {
       var d = model.getCurrent() || {};
@@ -142,6 +182,7 @@ BrowserID.Modules.InteractionData = (function() {
       options = options || {};
 
       var self = this;
+      self.mediatorToKPINameTable = MediatorToKPINameTable;
 
       // options.samplingEnabled is used for testing purposes.
       //
@@ -198,6 +239,13 @@ BrowserID.Modules.InteractionData = (function() {
     getCurrent: getCurrent,
     getCurrentEventStream: getCurrentEventStream,
     publishStored: publishStored
+
+    // BEGIN TEST API
+    ,
+    setNameTable: function(table) {
+      this.mediatorToKPINameTable = table;
+    }
+    // END TEST API
   });
 
   sc = Module.sc;
diff --git a/resources/static/test/cases/controllers/pick_email.js b/resources/static/test/cases/controllers/pick_email.js
index 555d2f4631c63ae0c9949339feab0e389c180e2c..a4419af1df70e98e88503b7fd9d61b0529b4c87b 100644
--- a/resources/static/test/cases/controllers/pick_email.js
+++ b/resources/static/test/cases/controllers/pick_email.js
@@ -37,11 +37,16 @@
     controller.start({});
   }
 
-  test("multiple emails - print emails in alphabetical order", function() {
+  asyncTest("multiple emails - print emails in alphabetical order, emails_displayed triggered", function() {
     storage.addEmail("third@testuser.com", {});
     storage.addEmail("second@testuser.com", {});
     storage.addEmail("first@testuser.com", {});
 
+    register("emails_displayed", function(msg, data) {
+      equal(data.count, 3, "emails_displayed triggered with correct email count");
+      start();
+    });
+
     createController();
 
     var inputs = $(".inputs input[type=radio]");
diff --git a/resources/static/test/cases/shared/modules/interaction_data.js b/resources/static/test/cases/shared/modules/interaction_data.js
index c3daf5632d88f963af41f9332389fb3a8f710986..f2b6b675d0ae32888178c1e63639f8f270c3e7dd 100644
--- a/resources/static/test/cases/shared/modules/interaction_data.js
+++ b/resources/static/test/cases/shared/modules/interaction_data.js
@@ -26,10 +26,29 @@
     }
   });
 
-  function createController(config) {
+  function createController(setKPINameTable, config) {
+    if (typeof setKPINameTable !== "boolean") {
+      config = setKPINameTable;
+      setKPINameTable = false;
+    }
+
     config = _.extend({ samplingEnabled: true }, config);
     controller = BrowserID.Modules.InteractionData.create();
     controller.start(config);
+
+    controller.setNameTable({
+      before_session_context: null,
+      after_session_context: null,
+      session1_before_session_context: null,
+      session1_after_session_context: null,
+      session2_before_session_context: null,
+      session2_after_session_context: null,
+      initial_string_name: "translated_name",
+      initial_function_name: function(msg, data) {
+        return "function_translation." + msg;
+      }
+    });
+
   }
 
   function indexOfEvent(eventStream, eventName) {
@@ -68,12 +87,23 @@
       testHelpers.testKeysInObject(data, ["event_stream", "sample_rate", "timestamp", "lang"]);
 
       controller.addEvent("after_session_context");
+      controller.addEvent("after_session_context");
+
+      // The next two are translated from mediator names to names usable by the
+      // KPI backend.
+
+      // translated to "translated_name"
+      controller.addEvent("initial_string_name");
+      // translated to "function_translation.initial_function_name"
+      controller.addEvent("initial_function_name");
 
       events = controller.getCurrentEventStream();
       // Make sure both the before_session_context and after_session_context
       // are both on the event stream.
       ok(indexOfEvent(events, "before_session_context") > -1, "before_session_context correctly saved to current event stream");
       ok(indexOfEvent(events, "after_session_context") > -1, "after_session_context correctly saved to current event stream");
+      ok(indexOfEvent(events, "translated_name") > -1, "string translation - translated_name correctly saved to current event stream");
+      ok(indexOfEvent(events, "function_translation.initial_function_name") > -1, "function translation - function_translation.initial_function_name correctly saved to current event stream");
 
 
       // Ensure that the event name as well as relative time are saved for an