diff --git a/lib/static_resources.js b/lib/static_resources.js
index f877ca51a84f7d4b4c7e3b9ae0a94ddf9a07373a..8f4d85c9947b850a366f82f39d8dc1e0a64cd5ac 100644
--- a/lib/static_resources.js
+++ b/lib/static_resources.js
@@ -80,7 +80,8 @@ var dialog_js = und.flatten([
     '/shared/command.js',
     '/shared/history.js',
     '/shared/state_machine.js',
-    '/shared/interaction_data.js',
+
+    '/shared/modules/interaction_data.js',
 
     '/dialog/resources/internal_api.js',
     '/dialog/resources/helpers.js',
diff --git a/resources/static/dialog/start.js b/resources/static/dialog/start.js
index 45e04d4fb15c4d2f7c590eed8a4a213e9b78bfbe..1d0655877c632dfd28b18d2fe025877c464dca4f 100644
--- a/resources/static/dialog/start.js
+++ b/resources/static/dialog/start.js
@@ -14,6 +14,12 @@
   xhr.init({ time_until_delay: 10 * 1000 });
   network.init();
 
+  var hash = window.location.hash || "",
+      continuation = hash.indexOf("#CREATE_EMAIL") > -1 || hash.indexOf("#ADD_EMAIL") > -1;
+
+  moduleManager.register("interaction_data", modules.InteractionData);
+  moduleManager.start("interaction_data", { continuation: continuation });
+
   moduleManager.register("cookie_check", modules.CookieCheck);
   moduleManager.start("cookie_check", {
     ready: function(status) {
diff --git a/resources/static/shared/interaction_data.js b/resources/static/shared/interaction_data.js
deleted file mode 100644
index 82c131d366bb6d1c694d95a08a5ff17dd4dd42bf..0000000000000000000000000000000000000000
--- a/resources/static/shared/interaction_data.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-/**
- * interaction_data is a module responsible for collecting and
- * reporting anonymous interaction data that represents a user's
- * interaction with the dialog.  It aggregates information that is not
- * user specific like the user's OS, Browser, and the interface
- * elements they've clicked on.  It stores this information in
- * localstorage, and at initialization reports previous interaction
- * data to the server.  This data is then used to optimize the user
- * experience of the Persona dialog.
- *
- * More information about interaction data and 'Key Performance Indicators'
- * stats that are derived from it:
- *
- *  https://wiki.mozilla.org/Privacy/Reviews/KPI_Backend
- */
-
-// TODO:
-//  * should code explicitly call .addEvent?  or instead should this module
-//    listen for events via the mediator?
-//  * the primary flow will cause unload and reload.  omg.  How do we deal?
-
-(function() {
-  var bid = BrowserID,
-      mediator = bid.Mediator,
-      storage = bid.Storage.interactionData,
-      network = bid.Network;
-
-  var startTime = new Date();
-
-  // sample rate is specified from the server.  it's set at the
-  // first 'context_info' event, which corresponds to the first time
-  // we get 'session_context' from the server.  When sampleRate is
-  // not undefined, then the interaction data is initialized.
-  // sample will be true or false
-  var sample = undefined;
-
-  var currentData = {
-    event_stream: [
-    ]
-  };
-
-  // whenever session_context is hit, let's hear about it so we can
-  // extract the information that's important to us (like, whether we
-  // should be running or not)
-  mediator.subscribe('context_info', onSessionContext);
-
-  function onSessionContext(msg, result) {
-    // defend against onSessionContext being called multiple times
-    if (sample !== undefined) return;
-
-    // set the sample rate as defined by the server.  It's a value
-    // between 0..1, integer or float, and it specifies the percentage
-    // of the time that we should capture
-    var sampleRate = result.data_sample_rate || 0;
-
-    currentData.sample_rate = sampleRate;
-
-    // now that we've got sample rate, let's smash it into a boolean
-    // probalistically
-    sample = (Math.random() <= sampleRate);
-
-    // if we're not going to sample, kick out early.
-    if (!sample) {
-      currentData = undefined;
-      return;
-    }
-
-    // set current time
-    currentData.timestamp = result.server_time;
-
-    // language
-    currentData.lang = $('html').attr('lang');
-
-    if (window.screen) {
-      currentData.screen_size = {
-        width: window.screen.width,
-        height: window.screen.height
-      };
-    }
-
-    // XXX: implement me!
-    currentData.user_agent = {
-      os: null,
-      browser: null,
-      version: null
-    };
-
-    // cool.  now let's persist this data
-    storage.push(currentData);
-    currentData = undefined;
-
-    // finally, let's try to publish any old data
-    setTimeout(publishOld, 10);
-  }
-
-  // At every load, after session_context returns, we'll try to publish
-  // past interaction data to the server if it exists.  The psuedo
-  // transactional model employed here is to attempt to post, and only
-  // once we receive a server response do we purge data.  We don't
-  // care if the post is a success or failure as this data is not
-  // critical to the functioning of the system (and some failure scenarios
-  // simply won't resolve with retries - like corrupt data, or too much
-  // data)
-  function publishOld() {
-    var data = storage.get();
-    if (data.length === 0) return;
-    network.sendInteractionData(data, complete, complete);
-    return;
-
-    function complete() {
-      storage.clear();
-    }
-  }
-
-  // on all events, update event_stream
-  mediator.subscribeAll(function(msg, data) {
-    if (sample === false) return;
-
-    if (currentData) {
-      currentData.event_stream.push([ msg, new Date() - startTime ]);
-    } else {
-      var d = storage.current();
-      if (!d.event_stream) d.event_stream = [];
-      d.event_stream.push([ msg, new Date() - startTime ]);
-      storage.setCurrent(d);
-    }
-  });
-}());
diff --git a/resources/static/shared/modules/interaction_data.js b/resources/static/shared/modules/interaction_data.js
new file mode 100644
index 0000000000000000000000000000000000000000..458478ed645672d646fc768a615114a80735b4dd
--- /dev/null
+++ b/resources/static/shared/modules/interaction_data.js
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * interaction_data is a module responsible for collecting and
+ * reporting anonymous interaction data that represents a user's
+ * interaction with the dialog.  It aggregates information that is not
+ * user specific like the user's OS, Browser, and the interface
+ * elements they've clicked on.  It stores this information in
+ * localstorage, and at initialization reports previous interaction
+ * data to the server.  This data is then used to optimize the user
+ * experience of the Persona dialog.
+ *
+ * More information about interaction data and 'Key Performance Indicators'
+ * stats that are derived from it:
+ *
+ *  https://wiki.mozilla.org/Privacy/Reviews/KPI_Backend
+ */
+
+// TODO:
+//  * should code explicitly call .addEvent?  or instead should this module
+//    listen for events via the mediator?
+
+BrowserID.Modules.InteractionData = (function() {
+  var bid = BrowserID,
+      storage = bid.Storage.interactionData,
+      network = bid.Network,
+      complete = bid.Helpers.complete,
+      dom = bid.DOM,
+      sc;
+
+  function onSessionContext(msg, result) {
+    var self=this;
+
+    // defend against onSessionContext being called multiple times
+    if (self.sessionContextHandled) return;
+    self.sessionContextHandled = true;
+
+    // Publish any outstanding data.  Unless this is a continuation, previous
+    // session data must be published independently of whether the current
+    // dialog session is allowed to sample data. This is because the original
+    // dialog session has already decided whether to collect data.
+    publishStored();
+
+    // set the sample rate as defined by the server.  It's a value
+    // between 0..1, integer or float, and it specifies the percentage
+    // of the time that we should capture
+    var sampleRate = result.data_sample_rate || 0;
+
+    if (typeof self.samplingEnabled === "undefined") {
+      // now that we've got sample rate, let's smash it into a boolean
+      // probalistically
+      self.samplingEnabled = Math.random() <= sampleRate;
+    }
+
+    // if we're not going to sample, kick out early.
+    if (!self.samplingEnabled) {
+      return;
+    }
+
+    var currentData = {
+      event_stream: self.initialEventStream,
+      sample_rate: sampleRate,
+      timestamp: result.server_time,
+      local_timestamp: self.startTime.toString(),
+      lang: dom.getAttr('html', 'lang') || null,
+    };
+
+    if (window.screen) {
+      currentData.screen_size = {
+        width: window.screen.width,
+        height: window.screen.height
+      };
+    }
+
+    // cool.  now let's persist the initial data.  This data will be published
+    // as soon as the first session_context completes for the next dialog
+    // session.  Use a push because old data *may not* have been correctly
+    // published to a down server or erroring web service.
+    storage.push(currentData);
+
+    self.initialEventStream = null;
+
+    self.samplesBeingStored = true;
+  }
+
+  // At every load, after session_context returns, we'll try to publish
+  // past interaction data to the server if it exists.  The psuedo
+  // transactional model employed here is to attempt to post, and only
+  // once we receive a server response do we purge data.  We don't
+  // care if the post is a success or failure as this data is not
+  // critical to the functioning of the system (and some failure scenarios
+  // simply won't resolve with retries - like corrupt data, or too much
+  // data)
+  function publishStored(oncomplete) {
+    var data = storage.get();
+
+    if (data && data.length !== 0) {
+      network.sendInteractionData(data, function() {
+        storage.clear();
+        complete(oncomplete, true);
+      });
+    }
+    else {
+      complete(oncomplete, false);
+    }
+  }
+
+
+  function addEvent(eventName) {
+    var self=this;
+
+    if (self.samplingEnabled === false) return;
+
+    var eventData = [ eventName, new Date() - self.startTime ];
+    if (self.samplesBeingStored) {
+      var d = storage.current() || {};
+      if (!d.event_stream) d.event_stream = [];
+      d.event_stream.push(eventData);
+      storage.setCurrent(d);
+    } else {
+      self.initialEventStream.push(eventData);
+    }
+  }
+
+  var Module = bid.Modules.PageModule.extend({
+    start: function(options) {
+      options = options || {};
+
+      var self = this;
+
+      // options.samplingEnabled is used for testing purposes.
+      //
+      // If samplingEnabled is not specified in the options, and this is not
+      // a continuation, samplingEnabled will be decided on the first "
+      // context_info" event, which corresponds to the first time
+      // 'session_context' returns from the server.
+      self.samplingEnabled = options.samplingEnabled;
+
+      // continuation means the users dialog session is continuing, probably
+      // due to a redirect to an IdP and then a return after authentication.
+      if (options.continuation) {
+        var previousData = storage.current();
+
+        var samplingEnabled = self.samplingEnabled = !!previousData.event_stream;
+        if (samplingEnabled) {
+          self.startTime = Date.parse(previousData.local_timestamp);
+
+          if (typeof self.samplingEnabled === "undefined") {
+            self.samplingEnabled = samplingEnabled;
+          }
+
+          // instead of waiting for session_context to start appending data to
+          // localStorage, start saving into localStorage now.
+          self.samplesBeingStored = true;
+        }
+        else {
+          // If there was no previous event stream, that means data collection
+          // was not allowed for the previous session.  Return with no further
+          // action, data collection is not allowed for this session either.
+          return;
+        }
+      }
+      else {
+        self.startTime = new Date();
+
+        // The initialEventStream is used to store events until onSessionContext
+        // is called.  Once onSessionContext is called and it is known whether
+        // the user's data will be saved, initialEventStream will either be
+        // discarded or added to the data set that is saved to localStorage.
+        self.initialEventStream = [];
+        self.samplesBeingStored = false;
+
+        // whenever session_context is hit, let's hear about it so we can
+        // extract the information that's important to us (like, whether we
+        // should be running or not)
+        self.contextInfoHandle = this.subscribe('context_info', onSessionContext);
+      }
+
+      // on all events, update event_stream
+      this.subscribeAll(addEvent);
+    },
+
+    addEvent: addEvent,
+
+    getCurrentStoredData: function() {
+      var und;
+      return this.samplesBeingStored ? storage.current() : und;
+    },
+
+    getEventStream: function() {
+      return this.samplesBeingStored ? storage.current().event_stream : this.initialEventStream || [];
+    },
+
+    publishStored: publishStored
+  });
+
+  sc = Module.sc;
+
+  return Module;
+
+}());
diff --git a/resources/static/shared/modules/page_module.js b/resources/static/shared/modules/page_module.js
index b47ecf8ac7a2a496634f1b6c4ed87e437fb3e3d0..a86f40cf99d4dfafd53bcb26f1a74a257aa43841 100644
--- a/resources/static/shared/modules/page_module.js
+++ b/resources/static/shared/modules/page_module.js
@@ -176,7 +176,17 @@ BrowserID.Modules.PageModule = (function() {
      * @param {object} [context] - context, if not given, use this.
      */
     subscribe: function(message, callback, context) {
-      mediator.subscribe(message, callback.bind(context || this));
+      mediator.subscribe(message, callback, context || this);
+    },
+
+    /**
+     * Subscribe to all messages on the mediator.
+     * @method subscribeAll
+     * @param {function} callback
+     * @param {object} [context] - context, if not given, use this.
+     */
+    subscribeAll: function(callback, context) {
+      mediator.subscribeAll(callback, context || this);
     },
 
     /**
diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js
index 60651aff69af869b2a98583e7e39253cb9404906..d1e0cfc1d4391ae2f1408be4985de6ada84683e2 100644
--- a/resources/static/shared/network.js
+++ b/resources/static/shared/network.js
@@ -157,6 +157,10 @@ BrowserID.Network = (function() {
       }, onFailure);
     },
 
+    withContext: function(onComplete, onFailure) {
+      withContext(onComplete, onFailure)
+    },
+
     /**
      * clear local cache, including authentication status and
      * other session data.
diff --git a/resources/static/shared/storage.js b/resources/static/shared/storage.js
index f87cff94401569a58f25c75d168abbdd0583b5de..dd5c8ce0f17105f804b70af5a1a03c3d89e851a9 100644
--- a/resources/static/shared/storage.js
+++ b/resources/static/shared/storage.js
@@ -394,7 +394,7 @@ BrowserID.Storage = (function() {
     try {
       return JSON.parse(storage.interactionData)[0];
     } catch(e) {
-      alert(e);
+      //alert(e);
       return {};
     }
   }
@@ -405,7 +405,7 @@ BrowserID.Storage = (function() {
       id = JSON.parse(storage.interactionData);
       id[0] = data;
     } catch(e) {
-      alert(e);
+      //alert(e);
       id = [ data ];
     }
     storage.interactionData = JSON.stringify(id);
@@ -413,21 +413,19 @@ BrowserID.Storage = (function() {
 
   function getAllInteractionData() {
     try {
-      var id = JSON.parse(storage.interactionData);
-      return id.slice(1);
+      return JSON.parse(storage.interactionData);
     } catch(e) {
-      alert(e);
+      //alert(e);
       return [];
     }
   }
 
   function clearInteractionData() {
     try {
-      var id = JSON.parse(storage.interactionData);
-      storage.interactionData = JSON.stringify(id.slice(0,1));
+      storage.interactionData = JSON.stringify([]);
     } catch(e) {
-      alert(e);
-      delete storage.interactionData;
+  //    alert(e);
+    //  delete storage.interactionData;
     }
   }
 
diff --git a/resources/static/test/cases/shared/modules/interaction_data.js b/resources/static/test/cases/shared/modules/interaction_data.js
new file mode 100644
index 0000000000000000000000000000000000000000..951c506903538a898a6e0c80c8fa8913aeff3593
--- /dev/null
+++ b/resources/static/test/cases/shared/modules/interaction_data.js
@@ -0,0 +1,186 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+(function() {
+  "use strict";
+
+  var bid = BrowserID,
+      testHelpers = bid.TestHelpers,
+      network = bid.Network,
+      storage = bid.Storage,
+      controller;
+
+  module("shared/modules/interaction_data", {
+    setup: testHelpers.setup,
+    teardown: function() {
+      testHelpers.teardown();
+
+      controller.destroy();
+    }
+  });
+
+  function createController(config) {
+    config = _.extend({ samplingEnabled: true }, config);
+    controller = BrowserID.Modules.InteractionData.create();
+    controller.start(config);
+  }
+
+  function indexOfEvent(eventStream, eventName) {
+    for(var event, i = 0; event = eventStream[i]; ++i) {
+      if(event[0] === eventName) return i;
+    }
+
+    return -1;
+  }
+
+  asyncTest("samplingEnabled - ensure data collection working as expected", function() {
+    createController();
+
+    controller.addEvent("before_session_context");
+
+    var events = controller.getEventStream();
+    ok(indexOfEvent(events, "before_session_context") > -1, "before_session_context correctly saved to event stream");
+    ok(indexOfEvent(events, "after_session_context") === -1, "after_session_context not yet added to current event stream");
+
+    // with context initializes the current stored data.
+    network.withContext(function() {
+      var data = controller.getCurrentStoredData();
+
+      // Make sure expected items are in the current stored data.
+      testHelpers.testKeysInObject(data, ["event_stream", "sample_rate", "timestamp", "lang"]);
+
+      controller.addEvent("after_session_context");
+
+      var events = controller.getEventStream();
+
+      // 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");
+
+
+      // Ensure that the event name as well as relative time are saved for an
+      // event.
+      var index = indexOfEvent(events, "after_session_context");
+      var event = events[index];
+
+      ok(index > -1, "after_session_context correctly saved to current event stream");
+      equal(event[0], "after_session_context", "name stored");
+      equal(typeof event[1], "number", "time stored");
+
+      start();
+    });
+
+  });
+
+  asyncTest("publish data", function() {
+    createController();
+
+    // force saved data to be cleared.
+    storage.interactionData.clear();
+    controller.publishStored(function(status) {
+      equal(status, false, "no data to publish");
+
+      // session context is required start saving events to localStorage.
+      network.withContext(function() {
+
+        // Add an event which should allow us to publish
+        controller.addEvent("something_special");
+        controller.publishStored(function(status) {
+          equal(status, true, "data correctly published");
+
+          start();
+        });
+      });
+    });
+  });
+
+  asyncTest("samplingEnabled set to false - no data collection occurs", function() {
+    createController({ samplingEnabled: false });
+
+    // the initial with_context will send off any stored data, there should be
+    // no stored data.
+    network.withContext(function() {
+      controller.addEvent("after_session_context");
+      var events = controller.getEventStream();
+
+      var index = indexOfEvent(events, "after_session_context");
+      equal(index, -1, "events not being stored");
+
+      equal(typeof controller.getCurrentStoredData(), "undefined", "no stored data");
+
+      controller.publishStored(function(status) {
+        equal(status, false, "there was no data to publish");
+        start();
+      });
+    });
+  });
+
+  asyncTest("continue: true, data collection permitted on previous session - continue appending data to previous session", function() {
+    createController();
+
+    controller.addEvent("session1_before_session_context");
+    network.withContext(function() {
+      controller.addEvent("session1_after_session_context");
+
+      // simulate a restart of the dialog.  Clear the session_context and then
+      // re-get session context.
+      controller = null;
+      network.clearContext();
+      createController({ continuation: true });
+
+      controller.addEvent("session2_before_session_context");
+      network.withContext(function() {
+        controller.addEvent("session2_after_session_context");
+
+        var events = controller.getEventStream();
+
+        ok(indexOfEvent(events, "session1_before_session_context") > -1, "session1_before_session_context correctly saved to current event stream");
+        ok(indexOfEvent(events, "session1_after_session_context") > -1, "session1_after_session_context correctly saved to current event stream");
+        ok(indexOfEvent(events, "session2_before_session_context") > -1, "session2_before_session_context correctly saved to current event stream");
+        ok(indexOfEvent(events, "session2_after_session_context") > -1, "session2_after_session_context correctly saved to current event stream");
+
+      });
+
+      start();
+    });
+
+  });
+
+  asyncTest("continue: true, data collection not permitted in previous session - no data collected", function() {
+    createController({ samplingEnabled: false });
+
+    controller.addEvent("session1_before_session_context");
+    network.withContext(function() {
+      controller.addEvent("session1_after_session_context");
+
+      // simulate a restart of the dialog.  Clear the session_context and then
+      // re-get session context.
+      controller = null;
+      network.clearContext();
+      createController({ continuation: true });
+
+      controller.addEvent("session2_before_session_context");
+      network.withContext(function() {
+        controller.addEvent("session2_after_session_context");
+
+        var events = controller.getEventStream();
+
+        ok(indexOfEvent(events, "session1_before_session_context") === -1, "no data collected");
+        ok(indexOfEvent(events, "session1_after_session_context") === -1, "no data collected");
+        ok(indexOfEvent(events, "session2_before_session_context") === -1, "no data collected");
+        ok(indexOfEvent(events, "session2_after_session_context") === -1, "no data collected");
+
+        controller.publishStored(function(status) {
+          equal(status, false, "there was no data to publish");
+          start();
+        });
+      });
+    });
+
+  });
+
+
+}());
diff --git a/resources/static/test/cases/shared/network.js b/resources/static/test/cases/shared/network.js
index 7d50cfaff4865df05562ee6e20236ec4079b8cc4..1fc02f083dcd60aa6bec431d9312f8faed97066e 100644
--- a/resources/static/test/cases/shared/network.js
+++ b/resources/static/test/cases/shared/network.js
@@ -614,4 +614,18 @@
     network.prolongSession(testHelpers.unexpectedSuccess, testHelpers.expectedXHRFailure);
   });
 
+  asyncTest("sendInteractionData success - call success", function() {
+    var data = {};
+    network.sendInteractionData(data, function(status) {
+      equal(status, true, "complete with correct status");
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
+  asyncTest("sendInteractionData with XHR failure - call failure", function() {
+    var data = {};
+    transport.useResult("ajaxError");
+    network.sendInteractionData(data, testHelpers.unexpectedSuccess, testHelpers.expectedXHRFailure);
+  });
+
 }());
diff --git a/resources/static/test/cases/shared/storage.js b/resources/static/test/cases/shared/storage.js
index 5bab4b78ed7d0164c80d29957d29a63da4da76ba..c6f459e6264d103e8fc589b9479047dc287e0fb6 100644
--- a/resources/static/test/cases/shared/storage.js
+++ b/resources/static/test/cases/shared/storage.js
@@ -189,15 +189,13 @@
           "after clearing, interaction data is zero length");
   });
 
-  test("get interaction data returns everything except current (in-progress) data", function() {
+  test("get interaction data returns all data", function() {
     storage.interactionData.push({ foo: "old2" });
     storage.interactionData.clear();
     storage.interactionData.push({ foo: "old1" });
-    storage.interactionData.push({ foo: "current" });
     var d = storage.interactionData.get();
-    equal(d.length, 2, "get() returns complete unpublished data blobs");
+    equal(d.length, 1, "get() returns complete unpublished data blobs");
     equal(d[0].foo, 'old1', "get() returns complete unpublished data blobs");
-    equal(d[1].foo, 'old2', "get() returns complete unpublished data blobs");
   });
 }());
 
diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js
index 17aa6c9851933562e057afbd92c26cb68fa1d4af..d9048e7bfd80a82e99b6b6f8525c8c0e1c2f7630 100644
--- a/resources/static/test/mocks/xhr.js
+++ b/resources/static/test/mocks/xhr.js
@@ -13,7 +13,8 @@ BrowserID.Mocks.xhr = (function() {
       authenticated: false,
       auth_level: undefined,
       code_version: "ABC123",
-      random_seed: "H+ZgKuhjVckv/H4i0Qvj/JGJEGDVOXSIS5RCOjY9/Bo="
+      random_seed: "H+ZgKuhjVckv/H4i0Qvj/JGJEGDVOXSIS5RCOjY9/Bo=",
+      data_sample_rate: 1
     };
 
   // this cert is meaningless, but it has the right format
@@ -118,6 +119,8 @@ BrowserID.Mocks.xhr = (function() {
       "post /wsapi/prolong_session valid": { success: true },
       "post /wsapi/prolong_session unauthenticated": 400,
       "post /wsapi/prolong_session ajaxError": undefined,
+      "post /wsapi/interaction_data valid": { success: true },
+      "post /wsapi/interaction_data ajaxError": undefined
     },
 
     setContextInfo: function(field, value) {
diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js
index 31ac2d086649287fd4b4d68810fed152a9cdeab7..910db36bd27f94bb2265896c3c2e103077c7a510 100644
--- a/resources/static/test/testHelpers/helpers.js
+++ b/resources/static/test/testHelpers/helpers.js
@@ -197,7 +197,14 @@ BrowserID.TestHelpers = (function() {
         str += (i % 10);
       }
       return str;
+    },
+
+    testKeysInObject: function(objToTest, expected, msg) {
+      for(var i=0, key; key=expected[i]; ++i) {
+        ok(key in objToTest, msg || ("object contains " + key));
+      }
     }
+
   };
 
   return TestHelpers;
diff --git a/resources/views/test.ejs b/resources/views/test.ejs
index a1fb2cd58698ae93864018bbfaeee0935584d76c..3d55b817e19316d0226c033ec9dbec85ddc13020 100644
--- a/resources/views/test.ejs
+++ b/resources/views/test.ejs
@@ -108,6 +108,7 @@
     <script src="/shared/modules/xhr_delay.js"></script>
     <script src="/shared/modules/xhr_disable_form.js"></script>
     <script src="/shared/modules/cookie_check.js"></script>
+    <script src="/shared/modules/interaction_data.js"></script>
 
     <script src="/dialog/resources/internal_api.js"></script>
     <script src="/dialog/resources/helpers.js"></script>
@@ -159,6 +160,7 @@
     <script src="cases/shared/modules/xhr_delay.js"></script>
     <script src="cases/shared/modules/xhr_disable_form.js"></script>
     <script src="cases/shared/modules/cookie_check.js"></script>
+    <script src="cases/shared/modules/interaction_data.js"></script>
 
     <script src="cases/pages/browserid.js"></script>
     <script src="cases/pages/page_helpers.js"></script>