diff --git a/lib/static_resources.js b/lib/static_resources.js
index 15fce2a8b3782857152b448697115ef8c3d19931..92f5c95cefa19b66e9206ed015cf9f95de18d371 100644
--- a/lib/static_resources.js
+++ b/lib/static_resources.js
@@ -80,6 +80,9 @@ var dialog_js = und.flatten([
     '/shared/history.js',
     '/shared/state_machine.js',
 
+    '/shared/models/models.js',
+    '/shared/models/interaction_data.js',
+
     '/shared/modules/interaction_data.js',
 
     '/dialog/resources/internal_api.js',
diff --git a/resources/static/shared/models/interaction_data.js b/resources/static/shared/models/interaction_data.js
new file mode 100644
index 0000000000000000000000000000000000000000..c1bfe743328bc66f13c136e9aa401bf7c56fcc70
--- /dev/null
+++ b/resources/static/shared/models/interaction_data.js
@@ -0,0 +1,164 @@
+/*globals 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/. */
+
+BrowserID.Models.InteractionData = (function() {
+  "use strict";
+
+  var bid = BrowserID,
+      storage = bid.getStorage(),
+      network = bid.Network,
+      complete = bid.Helpers.complete;
+
+  function getInteractionData() {
+    var interactionData;
+    try {
+      interactionData = JSON.parse(storage.interaction_data);
+    } catch(e) {
+    }
+
+    return interactionData || {};
+  }
+
+  function setInteractionData(data) {
+    try {
+      storage.interaction_data = JSON.stringify(data);
+    } catch(e) {
+      console.log(e);
+      storage.removeItem("interaction_data");
+    }
+  }
+
+  function push(newData) {
+    stageCurrent();
+
+    var interactionData = getInteractionData();
+    interactionData.current = newData;
+
+    setInteractionData(interactionData);
+  }
+
+  function getCurrent() {
+    var interactionData = getInteractionData();
+
+    return interactionData.current;
+  }
+
+  function setCurrent(data) {
+    var interactionData = getInteractionData();
+    interactionData.current = data;
+    setInteractionData(interactionData);
+  }
+
+  function stageCurrent() {
+    // Push existing current data to the staged list.  This allows
+    // us to get/clear the staged list without affecting the current data.
+    var interactionData = getInteractionData();
+
+    if (interactionData.current) {
+      var staged = interactionData.staged = interactionData.staged || [];
+      staged.unshift(interactionData.current);
+
+      delete interactionData.current;
+
+      setInteractionData(interactionData);
+    }
+  }
+
+  function getStaged() {
+    var interactionData = getInteractionData();
+    return interactionData.staged || [];
+  }
+
+  function clearStaged() {
+    var interactionData = getInteractionData();
+    delete interactionData.staged;
+    setInteractionData(interactionData);
+  }
+
+  // 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 publishStaged(oncomplete) {
+     var data = getStaged();
+
+    // XXX: should we even try to post data if it's larger than some reasonable
+    // threshold?
+    if (data && data.length !== 0) {
+      network.sendInteractionData(data, function() {
+        clearStaged();
+        complete(oncomplete, true);
+      }, function(status) {
+        // if the server returns a 413 error, (too much data posted), then
+        // let's clear our local storage and move on.  This does mean we
+        // loose some interaction data, but it shouldn't be statistically
+        // significant.
+        if (status && status.network && status.network.status === 413) {
+          clearStaged();
+        }
+        complete(oncomplete, false);
+      });
+    }
+    else {
+      complete(oncomplete, false);
+    }
+  }
+
+  return {
+    /**
+     * add a new interaction blob to localstorage, this will *push* any stored
+     * blobs to the 'staged' backlog, and happens when a new dialog interaction
+     * begins.
+     * @method push
+     * @param {object} data - an object to push onto the queue
+     * @returns nada
+     */
+    push: push,
+    /**
+     * read the interaction data blob associated with the current interaction
+     * @method getCurrent
+     * @returns a JSON object containing the latest interaction data blob
+     */
+    getCurrent: getCurrent,
+    /**
+     * overwrite the interaction data blob associated with the current interaction
+     * @method setCurrent
+     * @param {object} data - the object to overwrite current with
+     */
+    setCurrent: setCurrent,
+    /**
+     * Shift any "current" data into the staged list.  No data will be listed
+     * as current afterwards.
+     * @method stageCurrent
+     */
+    stageCurrent: stageCurrent,
+    /**
+     * get all past saved interaction data (returned as a JSON array), excluding
+     * the "current" data (that which is being collected now).
+     * @method getStaged
+     * @returns an array, possibly of length zero if no past interaction data is
+     * available
+     */
+    getStaged: getStaged,
+    /**
+     * publish staged data. Staged data will be cleared if successfully posted
+     * to server or if server returns 413 - too much data.
+     * @param {function} [oncomplete] - function to call when complete.  Called
+     * with true if data was successfully sent to server, false otw.
+     * @method publishStaged
+     */
+    publishStaged: publishStaged,
+    /**
+     * clear all interaction data, except the current, in-progress
+     * collection.
+     * @method clearStaged()
+     */
+    clearStaged: clearStaged
+  };
+
+}());
diff --git a/resources/static/shared/models/models.js b/resources/static/shared/models/models.js
new file mode 100644
index 0000000000000000000000000000000000000000..845cc04763349d29296935b5148f22f7f37f5032
--- /dev/null
+++ b/resources/static/shared/models/models.js
@@ -0,0 +1,7 @@
+/*globals 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/. */
+
+BrowserID.Models = {};
+
diff --git a/resources/static/shared/modules/interaction_data.js b/resources/static/shared/modules/interaction_data.js
index 97b981556365a017b70ee369142f1a68a690c73c..0ef57fd67381c5180d8e61a0734124032d6bf1bd 100644
--- a/resources/static/shared/modules/interaction_data.js
+++ b/resources/static/shared/modules/interaction_data.js
@@ -24,14 +24,13 @@
 
 BrowserID.Modules.InteractionData = (function() {
   var bid = BrowserID,
-      storage = bid.Storage.interactionData,
+      model = bid.Models.InteractionData,
       network = bid.Network,
       complete = bid.Helpers.complete,
       dom = bid.DOM,
       sc;
 
   function onSessionContext(msg, result) {
-    console.log("session context");
     var self=this;
 
     // defend against onSessionContext being called multiple times
@@ -43,106 +42,101 @@ BrowserID.Modules.InteractionData = (function() {
     // dialog session is allowed to sample data. This is because the original
     // dialog session has already decided whether to collect data.
 
-    // Continuation after publishing MUST be done
-    publishStored(function() {
-      // 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;
-      }
+    model.stageCurrent();
+    publishStored.call(self);
 
-      // if we're not going to sample, kick out early.
-      if (!self.samplingEnabled) {
-        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;
 
-      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 (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 (window.screen) {
-        currentData.screen_size = {
-          width: window.screen.width,
-          height: window.screen.height
-        };
-      }
+    // if we're not going to sample, kick out early.
+    if (!self.samplingEnabled) {
+      return;
+    }
 
-      // 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.
-      console.log("pushing currentData");
-      storage.push(currentData);
+    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
+      };
+    }
 
-      self.initialEventStream = null;
+    // 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.
+    model.push(currentData);
 
-      self.samplesBeingStored = true;
-    });
+    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)
+  // At every load, after session_context returns, try to publish the previous
+  // data.  We have to wait until session_context completes so that we have
+  // a csrf token to send.
   function publishStored(oncomplete) {
-    var data = storage.get();
-
-    // XXX: should we even try to post data if it's larger than some reasonable
-    // threshold?
-    console.log(data);
-    if (data && data.length !== 0) {
-      network.sendInteractionData(data, function() {
-        console.log("clear");
-        storage.clear();
-        complete(oncomplete, true);
-      }, function(status) {
-        // if the server returns a 413 error, (too much data posted), then
-        // let's clear our local storage and move on.  This does mean we
-        // loose some interaction data, but it shouldn't be statistically
-        // significant.
-        if (status && status.network && status.network.status === 413) {
-          storage.clear();
-        }
-        complete(oncomplete, false);
-      });
-    }
-    else {
-      complete(oncomplete, false);
-    }
+    var self=this;
+
+    model.publishStaged(function(status) {
+      var msg = status ? "interaction_data_send_complete" : "interaction_data_send_error";
+      self.publish(msg);
+      complete(oncomplete, status);
+    });
   }
 
 
   function addEvent(eventName) {
     var self=this;
-
     if (self.samplingEnabled === false) return;
 
     var eventData = [ eventName, new Date() - self.startTime ];
     if (self.samplesBeingStored) {
-      console.log("add stored event:" + eventName);
-      var d = storage.current() || {};
+      var d = model.getCurrent() || {};
       if (!d.event_stream) d.event_stream = [];
       d.event_stream.push(eventData);
-      storage.setCurrent(d);
+      model.setCurrent(d);
     } else {
-      console.log("add initial event:" + eventName);
       self.initialEventStream.push(eventData);
     }
   }
 
+  function getCurrent() {
+    var self=this;
+    if(self.samplingEnabled === false) return;
+
+    if (self.samplesBeingStored) {
+      return model.getCurrent();
+    }
+  }
+
+  function getCurrentEventStream() {
+    var self=this;
+    if(self.samplingEnabled === false) return;
+
+    if (self.samplesBeingStored) {
+      return model.getCurrent().event_stream;
+    }
+    else {
+      return self.initialEventStream;
+    }
+  }
+
   var Module = bid.Modules.PageModule.extend({
     start: function(options) {
       options = options || {};
@@ -155,29 +149,28 @@ BrowserID.Modules.InteractionData = (function() {
       // a continuation, samplingEnabled will be decided on the first "
       // context_info" event, which corresponds to the first time
       // 'session_context' returns from the server.
+      // samplingEnabled flag ignored for a continuation.
       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) {
+        // There will be no current data if the previous session was not
+        // allowed to save.
+        var previousData = model.getCurrent();
+        if (previousData) {
           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;
+          self.samplingEnabled = self.samplesBeingStored = true;
         }
         else {
-          // If there was no previous event stream, that means data collection
+          // If there was no previous data, 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.
+          self.samplingEnabled = false;
           return;
         }
       }
@@ -187,7 +180,7 @@ BrowserID.Modules.InteractionData = (function() {
         // 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.
+        // discarded or added to the data set that is saved to localmodel.
         self.initialEventStream = [];
         self.samplesBeingStored = false;
 
@@ -202,16 +195,8 @@ BrowserID.Modules.InteractionData = (function() {
     },
 
     addEvent: addEvent,
-
-    getCurrentStoredData: function() {
-      var und;
-      return this.samplesBeingStored ? storage.current() : und;
-    },
-
-    getEventStream: function() {
-      return this.samplesBeingStored ? storage.current().event_stream : this.initialEventStream || [];
-    },
-
+    getCurrent: getCurrent,
+    getCurrentEventStream: getCurrentEventStream,
     publishStored: publishStored
   });
 
diff --git a/resources/static/shared/storage.js b/resources/static/shared/storage.js
index 82ed0be6d3838d2d4e93695530005162997e38e6..1fa52161a9da6e0b65c7c0bad02e79dfe9682800 100644
--- a/resources/static/shared/storage.js
+++ b/resources/static/shared/storage.js
@@ -3,13 +3,8 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*globals BrowserID: true, console: true */
-
-BrowserID.Storage = (function() {
-  "use strict";
-
-  var jwcrypto,
-      ONE_DAY_IN_MS = (1000 * 60 * 60 * 24),
-      storage;
+BrowserID.getStorage = function() {
+  var storage;
 
   try {
     storage = localStorage;
@@ -27,6 +22,16 @@ BrowserID.Storage = (function() {
     };
   }
 
+  return storage;
+};
+
+BrowserID.Storage = (function() {
+  "use strict";
+
+  var jwcrypto,
+      ONE_DAY_IN_MS = (1000 * 60 * 60 * 24),
+      storage = BrowserID.getStorage();
+
   // temporary, replace with helpers.log if storage uses elog long term...
   function elog (msg) {
     if (window.console && console.error) console.error(msg);
@@ -401,56 +406,6 @@ BrowserID.Storage = (function() {
     storage.emailToUserID = JSON.stringify(allInfo);
   }
 
-  function pushInteractionData(data) {
-    var id;
-    try {
-      id = JSON.parse(storage.interactionData);
-      id.unshift(data);
-    } catch(e) {
-      id = [ data ];
-    }
-    storage.interactionData = JSON.stringify(id);
-  }
-
-  function currentInteractionData() {
-    try {
-      return storage.interactionData ? JSON.parse(storage.interactionData)[0] : {};
-    } catch(e) {
-      elog(e);
-      return {};
-    }
-  }
-
-  function setCurrentInteractionData(data) {
-    var id;
-    try {
-      id = JSON.parse(storage.interactionData);
-      id[0] = data;
-    } catch(e) {
-      elog(e);
-      id = [ data ];
-    }
-    storage.interactionData = JSON.stringify(id);
-  }
-
-  function getAllInteractionData() {
-    try {
-      return storage.interactionData ? JSON.parse(storage.interactionData) : [];
-    } catch(e) {
-      if (window.console && console.error) console.error(e);
-      return [];
-    }
-  }
-
-  function clearInteractionData() {
-    try {
-      storage.interactionData = JSON.stringify([]);
-    } catch(e) {
-      storage.removeItem("interactionData");
-      elog(e);
-    }
-  }
-
   return {
     /**
      * Add an email address and optional key pair.
@@ -533,44 +488,6 @@ BrowserID.Storage = (function() {
       remove: generic2KeyRemove.curry("main_site", "signInEmail")
     },
 
-    interactionData: {
-      /**
-       * add a new interaction blob to localstorage, this will *push* any stored
-       * blobs to the 'completed' backlog, and happens when a new dialog interaction
-       * begins.
-       * @param {object} data - an object to push onto the queue
-       * @method interactionData.push()
-       * @returns nada
-       */
-      push: pushInteractionData,
-      /**
-       * read the interaction data blob associated with the current interaction
-       * @method interactionData.current()
-       * @returns a JSON object containing the latest interaction data blob
-       */
-      current: currentInteractionData,
-      /**
-       * overwrite the interaction data blob associated with the current interaction
-       * @param {object} data - the object to overwrite current with
-       * @method interactionData.setCurrent()
-       */
-      setCurrent: setCurrentInteractionData,
-      /**
-       * get all past saved interaction data (returned as a JSON array), excluding
-       * the "current" data (that which is being collected now).
-       * @method interactionData.get()
-       * @returns an array, possibly of length zero if no past interaction data is
-       * available
-       */
-      get: getAllInteractionData,
-      /**
-       * clear all interaction data, except the current, in-progress
-       * collection.
-       * @method interactionData.clear()
-       */
-      clear: clearInteractionData
-    },
-
     usersComputer: {
       /**
        * Query whether the user has confirmed that this is their computer
diff --git a/resources/static/test/cases/shared/models/interaction_data.js b/resources/static/test/cases/shared/models/interaction_data.js
new file mode 100644
index 0000000000000000000000000000000000000000..5454ccfc6e88f1e0a8cd1e250e132521d50192ff
--- /dev/null
+++ b/resources/static/test/cases/shared/models/interaction_data.js
@@ -0,0 +1,108 @@
+
+/*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() {
+  var bid = BrowserID,
+      model = bid.Models.InteractionData,
+      testHelpers = bid.TestHelpers,
+      testObjectValuesEqual = testHelpers.testObjectValuesEqual,
+      xhr = bid.Mocks.xhr;
+
+  module("shared/models/interaction_data", {
+    setup: function() {
+      testHelpers.setup();
+      localStorage.removeItem("interaction_data");
+    },
+
+    teardown: function() {
+      testHelpers.teardown();
+    }
+  });
+
+  test("after push, most recently pushed data available through getCurrent, getStaged gets previous data sets", function() {
+    model.push({ foo: "bar" });
+    equal(model.getCurrent().foo, "bar",
+          "after pushing new interaction data, it's returned from .getCurrent()");
+
+    equal(model.getStaged().length, 0, "no data is yet staged");
+
+    model.push({ foo: "baz" });
+
+    equal(model.getCurrent().foo, "baz", "current points to new data set")
+    var staged = model.getStaged();
+
+    equal(staged.length, 1, "only one staged item");
+    testObjectValuesEqual(staged[0], { foo: "bar" });
+  });
+
+  test("setCurrent data overwrites current", function() {
+    model.clearStaged();
+    model.push({ foo: "bar" });
+    model.setCurrent({ foo: "baz" });
+    equal(model.getCurrent().foo, "baz",
+          "overwriting current interaction data works");
+  });
+
+  test("clearStaged clears staged interaction data but leaves current data unaffected", function() {
+    model.push({ foo: "bar" });
+    model.push({ foo: "baz" });
+    model.clearStaged();
+    equal(model.getStaged().length, 0,
+          "after clearStageding, interaction data is zero length");
+    equal(model.getCurrent().foo, "baz",
+          "after clearStageding, current data is unaffected");
+  });
+
+  test("stageCurrent - stage the current data, if any. no data is current afterwards", function() {
+    // There is no current data to stage.
+    model.stageCurrent();
+    equal(model.getStaged().length, 0, "no data to staged");
+
+    model.push({ foo: "bar" });
+    model.stageCurrent();
+
+    equal(model.getStaged().length, 1, "current data staged");
+    equal(typeof model.getCurrent(), "undefined", "current data removed after being staged");
+  });
+
+  asyncTest("publishStored - publish any staged data", function() {
+    // There is no currently staged data.
+    model.publishStaged(function(status) {
+      equal(status, false, "no data currently staged");
+
+      // Simulate a throttling
+      // desired result - data is purged from staging table
+
+      // The first pushed data will become staged.
+      model.push({ foo: "bar" });
+      model.stageCurrent();
+
+      xhr.useResult("throttle");
+      model.publishStaged(function(status) {
+        equal(false, status, "data throttling returns false status");
+        // the previously staged data should we wiped on a throttling response.
+
+        // When the interaction_data next completes, this will be the only data
+        // that is pushed.
+        model.push({ foo: "baz" });
+        model.stageCurrent();
+
+        xhr.useResult("valid");
+        model.publishStaged(function(status) {
+          equal(true, status, "data successfully posted");
+          var request = xhr.getLastRequest('/wsapi/interaction_data'),
+              previousSessionsData = JSON.parse(request.data).data;
+
+          equal(previousSessionsData.length, 1, "sending correct result sets");
+          equal(previousSessionsData[0].foo, "baz", "correct data sent");
+          start();
+        });
+      });
+
+    });
+
+  });
+}());
diff --git a/resources/static/test/cases/shared/modules/interaction_data.js b/resources/static/test/cases/shared/modules/interaction_data.js
index 951c506903538a898a6e0c80c8fa8913aeff3593..88610f6a9f3fdc5e08a64b0abf7198c15bfdfab3 100644
--- a/resources/static/test/cases/shared/modules/interaction_data.js
+++ b/resources/static/test/cases/shared/modules/interaction_data.js
@@ -9,11 +9,16 @@
   var bid = BrowserID,
       testHelpers = bid.TestHelpers,
       network = bid.Network,
-      storage = bid.Storage,
+      model = bid.Models.InteractionData,
+      xhr = bid.Mocks.xhr,
+      mediator = bid.Mediator,
       controller;
 
   module("shared/modules/interaction_data", {
-    setup: testHelpers.setup,
+    setup: function() {
+      testHelpers.setup();
+      localStorage.removeItem("interaction_data");
+    },
     teardown: function() {
       testHelpers.teardown();
 
@@ -36,25 +41,35 @@
   }
 
   asyncTest("samplingEnabled - ensure data collection working as expected", function() {
+    // Desired sequence:
+    // 1. When session_context completes, initialize this session's interaction
+    // data, sends previous session's data.
+    // 2. when network.sendInteractionData completes, previous session's data is
+    // erased, current session's data is unaffected.
+
+    // simulate data stored for last session
+    model.push({ timestamp: new Date().getTime() });
+
     createController();
 
     controller.addEvent("before_session_context");
 
-    var events = controller.getEventStream();
+    var events = controller.getCurrentEventStream();
     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();
+    // Add an XHR delay to simulate interaction_data completeing after
+    // session_context completes.
+    xhr.setDelay(5);
+
+    mediator.subscribe("interaction_data_send_complete", function() {
+      var data = controller.getCurrent();
 
       // 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();
-
+      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");
@@ -73,28 +88,7 @@
       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();
-        });
-      });
-    });
+    network.withContext();
   });
 
   asyncTest("samplingEnabled set to false - no data collection occurs", function() {
@@ -104,12 +98,9 @@
     // 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");
+      equal(typeof controller.getCurrent(), "undefined", "no stored data");
+      equal(typeof controller.getCurrentEventStream(), "undefined", "no data stored");
 
       controller.publishStored(function(status) {
         equal(status, false, "there was no data to publish");
@@ -135,7 +126,7 @@
       network.withContext(function() {
         controller.addEvent("session2_after_session_context");
 
-        var events = controller.getEventStream();
+        var events = controller.getCurrentEventStream();
 
         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");
@@ -166,12 +157,8 @@
       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");
+        equal(typeof controller.getCurrent(), "undefined", "no data collected");
+        equal(typeof controller.getCurrentEventStream(), "undefined", "no data collected");
 
         controller.publishStored(function(status) {
           equal(status, false, "there was no data to publish");
@@ -179,8 +166,35 @@
         });
       });
     });
-
   });
 
 
+  asyncTest("simulate failed starts - data not sent until second successful session_context", function() {
+    // simulate 3 dialog openings without session completing for any of them.
+    // On the forth attempt at opening the dialog, the data is successfully
+    // sent.
+
+    createController();
+    controller.addEvent("session1_before_session_context");
+
+    // simulate this as the first successful connection to backend to find out
+    // whether data collection is allowed.
+    createController();
+    controller.addEvent("session2_before_session_context");
+    network.withContext(function() {
+
+      createController();
+      controller.addEvent("session2_before_session_context");
+
+      // Data is sent on the second successful call to session_context
+      network.withContext(function() {
+        var request = xhr.getLastRequest('/wsapi/interaction_data'),
+            previousSessionsData = JSON.parse(request.data).data;
+
+        equal(previousSessionsData.length, 1, "sending correct result sets");
+        start();
+      });
+    });
+  });
+
 }());
diff --git a/resources/static/test/cases/shared/storage.js b/resources/static/test/cases/shared/storage.js
index b9b230b5e362aacd4174960d173810eabea5faa7..c9406c84a21ef077b4a150f956a3666a0eccc505 100644
--- a/resources/static/test/cases/shared/storage.js
+++ b/resources/static/test/cases/shared/storage.js
@@ -179,37 +179,5 @@
     equal(typeof storage.signInEmail.get(), "undefined", "after remove, signInEmail is empty");
   });
 
-  test("push interaction data and get current", function() {
-    storage.interactionData.push({ foo: "bar" });
-    equal(storage.interactionData.current().foo, "bar",
-          "after pushing new interaction data, it's returned from .current()");
-  });
-
-  test("set interaction data overwrites current", function() {
-    storage.interactionData.clear();
-    storage.interactionData.push({ foo: "bar" });
-    storage.interactionData.setCurrent({ foo: "baz" });
-    equal(storage.interactionData.current().foo, "baz",
-          "overwriting current interaction data works");
-    equal(storage.interactionData.get().length, 1,
-          "overwriting doesn't append");
-  });
-
-  test("clear interaction data", function() {
-    storage.interactionData.push({ foo: "bar" });
-    storage.interactionData.push({ foo: "bar" });
-    storage.interactionData.clear();
-    equal(storage.interactionData.get().length, 0,
-          "after clearing, interaction data is zero length");
-  });
-
-  test("get interaction data returns all data", function() {
-    storage.interactionData.push({ foo: "old2" });
-    storage.interactionData.clear();
-    storage.interactionData.push({ foo: "old1" });
-    var d = storage.interactionData.get();
-    equal(d.length, 1, "get() returns complete unpublished data blobs");
-    equal(d[0].foo, 'old1', "get() returns complete unpublished data blobs");
-  });
 }());
 
diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js
index 8b505f997d4680903d922e8de6e2a90d78f08cc1..5d0c5a226e92100d896359b187a5a624c245b411 100644
--- a/resources/static/test/mocks/xhr.js
+++ b/resources/static/test/mocks/xhr.js
@@ -21,13 +21,18 @@ BrowserID.Mocks.xhr = (function() {
   var random_cert = "eyJhbGciOiJSUzEyOCJ9.eyJpc3MiOiJpc3N1ZXIuY29tIiwiZXhwIjoxMzE2Njk1MzY3NzA3LCJwdWJsaWMta2V5Ijp7ImFsZ29yaXRobSI6IlJTIiwibiI6IjU2MDYzMDI4MDcwNDMyOTgyMzIyMDg3NDE4MTc2ODc2NzQ4MDcyMDM1NDgyODk4MzM0ODExMzY4NDA4NTI1NTk2MTk4MjUyNTE5MjY3MTA4MTMyNjA0MTk4MDA0NzkyODQ5MDc3ODY4OTUxOTA2MTcwODEyNTQwNzEzOTgyOTU0NjUzODEwNTM5OTQ5Mzg0NzEyNzczMzkwMjAwNzkxOTQ5NTY1OTAzNDM5NTIxNDI0OTA5NTc2ODMyNDE4ODkwODE5MjA0MzU0NzI5MjE3MjA3MzYwMTA1OTA2MDM5MDIzMjk5NTYxMzc0MDk4OTQyNzg5OTk2NzgwMTAyMDczMDcxNzYwODUyODQxMDY4OTg5ODYwNDAzNDMxNzM3NDgwMTgyNzI1ODUzODk5NzMzNzA2MDY5IiwiZSI6IjY1NTM3In0sInByaW5jaXBhbCI6eyJlbWFpbCI6InRlc3R1c2VyQHRlc3R1c2VyLmNvbSJ9fQ.aVIO470S_DkcaddQgFUXciGwq2F_MTdYOJtVnEYShni7I6mqBwK3fkdWShPEgLFWUSlVUtcy61FkDnq2G-6ikSx1fUZY7iBeSCOKYlh6Kj9v43JX-uhctRSB2pI17g09EUtvmb845EHUJuoowdBLmLa4DSTdZE-h4xUQ9MsY7Ik";
 
   /**
-   * This is the results table, the keys are the request type, url, and
+   * This is the responses table, the keys are the request type, url, and
    * a "selector" for testing.  The right is the expected return value, already
    * decoded.  If a result is "undefined", the request's error handler will be
    * called.
    */
   var xhr = {
-    results: {
+    // Keep track of the last request made to each wsapi call.  keyed only on
+    // url - for instince - instead of "get /wsapi/session_context
+    // valid", the key would only be "/wsapi/session_context"
+    requests: {},
+
+    responses: {
       "get /wsapi/session_context valid": contextInfo,
       // We are going to test for XHR failures for session_context using
       // the flag contextAjaxError.
@@ -85,7 +90,6 @@ BrowserID.Mocks.xhr = (function() {
       "post /wsapi/stage_email invalid": { success: false },
       "post /wsapi/stage_email throttle": 429,
       "post /wsapi/stage_email ajaxError": undefined,
-      "post /wsapi/cert_key ajaxError": undefined,
       "get /wsapi/email_addition_status?email=testuser%40testuser.com complete": { status: "complete" },
       "get /wsapi/email_addition_status?email=registered%40testuser.com pending": { status: "pending" },
       "get /wsapi/email_addition_status?email=registered%40testuser.com complete": { status: "complete" },
@@ -124,6 +128,7 @@ BrowserID.Mocks.xhr = (function() {
       "post /wsapi/prolong_session unauthenticated": 400,
       "post /wsapi/prolong_session ajaxError": undefined,
       "post /wsapi/interaction_data valid": { success: true },
+      "post /wsapi/interaction_data throttle": 413,
       "post /wsapi/interaction_data ajaxError": undefined
     },
 
@@ -136,61 +141,63 @@ BrowserID.Mocks.xhr = (function() {
     },
 
     useResult: function(result) {
-      xhr.resultType = result;
+      xhr.responseName = result;
     },
 
-    getLastRequest: function() {
-      return this.req;
+    getLastRequest: function(key) {
+      var req = this.request;
+      if (key) {
+        req = this.requests[key];
+      }
+
+      return req;
     },
 
-    ajax: function(obj) {
+    ajax: function(request) {
       //console.log("ajax request");
-      var type = obj.type ? obj.type.toLowerCase() : "get";
-
-      var req = this.req = {
-        type: type,
-        url: obj.url,
-        data: obj.data
-      };
+      var type = request.type ? request.type.toLowerCase() : "get";
 
+      this.request = request = _.extend(request, {
+        type: type
+      });
 
-      if(type === "post" && obj.data.indexOf("csrf") === -1) {
+      if (type === "post" && request.data.indexOf("csrf") === -1) {
         ok(false, "missing csrf token on POST request");
       }
 
-
-      var resultType = xhr.resultType;
+      var responseName = xhr.responseName;
 
       // Unless the contextAjaxError is specified, use the "valid" context info.
       // This makes it so we do not have to keep adding new items for
       // context_info for every possible result type.
-      if(req.url === "/wsapi/session_context" && resultType !== "contextAjaxError") {
-        resultType = "valid";
+      if (request.url === "/wsapi/session_context" && responseName !== "contextAjaxError") {
+        responseName = "valid";
       }
 
-      var resName = req.type + " " + req.url + " " + resultType;
+      var responseKey = request.type + " " + request.url + " " + responseName,
+          response = xhr.responses[responseKey],
+          typeofResponse = typeof response;
 
-      var result = xhr.results[resName];
+      this.requests[request.url] = request;
 
-      var type = typeof result;
-      if(type === "function") {
-        result(obj.success);
+      if (typeofResponse === "function") {
+        response(request.success);
       }
-      else if(!(type == "number" || type == "undefined")) {
-        if(obj.success) {
-          if(delay) {
+      else if (!(typeofResponse == "number" || typeofResponse == "undefined")) {
+        if (request.success) {
+          if (delay) {
             // simulate response delay
-            _.delay(obj.success, delay, result);
+            _.delay(request.success, delay, response);
           }
           else {
-            obj.success(result);
+            request.success(response);
           }
         }
       }
-      else if (obj.error) {
-        // Invalid result - either invalid URL, invalid GET/POST or
-        // invalid resultType
-        obj.error({ status: result || 400, responseText: "response text" }, "errorStatus", "errorThrown");
+      else if (request.error) {
+        // Invalid response - either invalid URL, invalid GET/POST or
+        // invalid responseName
+        request.error({ status: response || 400, responseText: "response text" }, "errorStatus", "errorThrown");
       }
     }
   };
diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js
index 287ba8393c039d0519f9c892fcc79495f62d051f..5f72fae9ff39fc56b9b95de99c2b142962d89816 100644
--- a/resources/static/test/testHelpers/helpers.js
+++ b/resources/static/test/testHelpers/helpers.js
@@ -181,7 +181,7 @@ BrowserID.TestHelpers = (function() {
         start();
       });
 
-      if(transport.resultType === "valid") {
+      if(transport.responseName === "valid") {
         transport.useResult("ajaxError");
       }
 
@@ -200,6 +200,11 @@ BrowserID.TestHelpers = (function() {
     },
 
     testKeysInObject: function(objToTest, expected, msg) {
+      if (!objToTest) {
+        ok(false, "Missing object to test against");
+        return;
+      }
+
       for(var i=0, key; key=expected[i]; ++i) {
         ok(key in objToTest, msg || ("object contains " + key));
       }
diff --git a/resources/views/test.ejs b/resources/views/test.ejs
index 30c4faf532c2d3c034eb70b9fe8a96402c0a3e40..5b1890c5630a9ea5cb134fe3c722dbf4c1e44fff 100644
--- a/resources/views/test.ejs
+++ b/resources/views/test.ejs
@@ -104,6 +104,9 @@
     <script src="/shared/history.js"></script>
     <script src="/shared/state_machine.js"></script>
 
+    <script src="/shared/models/models.js"></script>
+    <script src="/shared/models/interaction_data.js"></script>
+
     <script src="/shared/modules/page_module.js"></script>
     <script src="/shared/modules/xhr_delay.js"></script>
     <script src="/shared/modules/xhr_disable_form.js"></script>
@@ -155,6 +158,8 @@
     <script src="cases/shared/history.js"></script>
     <script src="cases/shared/state_machine.js"></script>
 
+    <script src="cases/shared/models/interaction_data.js"></script>
+
     <script src="cases/shared/modules/page_module.js"></script>
     <script src="cases/shared/modules/xhr_delay.js"></script>
     <script src="cases/shared/modules/xhr_disable_form.js"></script>