diff --git a/resources/static/shared/modules/interaction_data.js b/resources/static/shared/modules/interaction_data.js index 16550f949582907ef983c4394a04d02108d11bd4..b08ef54163ce99f2f8760790a89da287e71e44f6 100644 --- a/resources/static/shared/modules/interaction_data.js +++ b/resources/static/shared/modules/interaction_data.js @@ -27,38 +27,45 @@ 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, - currentData = self.currentData; + var self=this; // defend against onSessionContext being called multiple times - if (self.sample !== undefined) return; + if (self.sessionContextHandled) return; + self.sessionContextHandled = true; + + // Publish any outstanding data. 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; - // now that we've got sample rate, let's smash it into a boolean - // probalistically - self.sample = self.forceSample || (Math.random() <= sampleRate); + 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.sample) { - self.currentData = undefined; + if (!self.samplingEnabled) { return; } - currentData.sample_rate = sampleRate; - - // set current time - currentData.timestamp = result.server_time; - - // language - currentData.lang = dom.getAttr('html', 'lang'); + var currentData = { + event_stream: self.initialEventStream, + sample_rate: sampleRate, + timestamp: result.server_time, + lang: dom.getAttr('html', 'lang') || null, + }; if (window.screen) { currentData.screen_size = { @@ -67,19 +74,15 @@ BrowserID.Modules.InteractionData = (function() { }; } - // XXX: implement me! - currentData.user_agent = { - os: null, - browser: null, - version: null - }; - - // cool. now let's persist this data + // 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); - currentData = undefined; - // finally, let's try to publish any old data - setTimeout(publish, 10); + self.initialEventStream = null; + + self.samplesBeingStored = true; } // At every load, after session_context returns, we'll try to publish @@ -90,34 +93,34 @@ BrowserID.Modules.InteractionData = (function() { // 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 publish() { + function publishStored(oncomplete) { var data = storage.get(); - if (data.length === 0) return; - - network.sendInteractionData(data, complete, function() { - storage.clear(); - }); + 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.sample === false) return; + if (self.samplingEnabled === false) return; var eventData = [ eventName, new Date() - self.startTime ]; - if (self.currentData) { - self.currentData.event_stream.push(eventData); - } else { - // @lloyd, how does this bit work? When can sampling be enabled but - // there not be currentData? It looks like currentData is created as - // soon as this module is run, and cleared only when session context - // comes in. - var d = storage.current(); + 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); } } @@ -127,21 +130,21 @@ BrowserID.Modules.InteractionData = (function() { var self = this; - self.forceSample = options.forceSample; - self.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 - self.sample = undefined; + // If samplingEnabled is not specified in the options, it will be decided + // on the first "context_info" event, which corresponds to the first time + // we get 'session_context' from the server. + // + // options.samplingEnabled is used for testing purposes. + self.samplingEnabled = options.samplingEnabled; - self.currentData = { - event_stream: [ - ] - }; + // 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 @@ -154,19 +157,16 @@ BrowserID.Modules.InteractionData = (function() { addEvent: addEvent, - isSampling: function() { - return this.sample; - }, - - getData: function() { - return this.currentData; + getCurrentStoredData: function() { + var und; + return this.samplesBeingStored ? storage.current() : und; }, - getStream: function() { - return this.currentData && this.currentData.event_stream; + getEventStream: function() { + return this.samplesBeingStored ? storage.current().event_stream : this.initialEventStream; }, - publish: publish + publishStored: publishStored }); sc = Module.sc; diff --git a/resources/static/shared/storage.js b/resources/static/shared/storage.js index 9f0ef262417581ef9c821859930d628578a3c886..dd5c8ce0f17105f804b70af5a1a03c3d89e851a9 100644 --- a/resources/static/shared/storage.js +++ b/resources/static/shared/storage.js @@ -413,8 +413,7 @@ 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); return []; @@ -423,11 +422,10 @@ BrowserID.Storage = (function() { 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 index 66e47319a1dc725559bbbe1640b4a02b3dfc0e7b..818ca815363b33e2395e38b88e007aa24fcd0a8d 100644 --- a/resources/static/test/cases/shared/modules/interaction_data.js +++ b/resources/static/test/cases/shared/modules/interaction_data.js @@ -9,6 +9,7 @@ var bid = BrowserID, testHelpers = bid.TestHelpers, network = bid.Network, + storage = bid.Storage, controller; module("shared/modules/interaction_data", { @@ -21,42 +22,99 @@ }); function createController(config) { - config = _.extend({ forceSample: true }, config); + config = _.extend({ samplingEnabled: true }, config); controller = BrowserID.Modules.InteractionData.create(); controller.start(config); } - asyncTest("forceSample - data collection starts on context_info", function() { + 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() { - equal(controller.isSampling(), true, "sampling has started!"); - var data = controller.getData(); + 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]; - testHelpers.testObjectHasOwnProperties(data, ["sample_rate", "timestamp", "lang", "user_agent"]); + 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("addEvent to stream, getEventStream", function() { + asyncTest("publish data", function() { createController(); - network.withContext(function() { - controller.addEvent("something_special"); + // force saved data to be cleared. + storage.interactionData.clear(); + controller.publishStored(function(status) { + equal(status, false, "no data to publish"); - var stream = controller.getStream(), - lastItem = stream[stream.length - 1]; + // session context is required start saving events to localStorage. + network.withContext(function() { - equal(lastItem[0], "something_special", "name stored"); - equal(typeof lastItem[1], "number", "time stored"); + // Add an event which should allow us to publish + controller.addEvent("something_special"); + controller.publishStored(function(status) { + equal(status, true, "data correctly published"); - start(); + start(); + }); + }); }); }); - asyncTest("publish - publish any outstanding data", function() { + 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"); + + 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 71ede1adaf652a5dda123fc61092e936d30b5b04..d9048e7bfd80a82e99b6b6f8525c8c0e1c2f7630 100644 --- a/resources/static/test/mocks/xhr.js +++ b/resources/static/test/mocks/xhr.js @@ -119,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 808f628baff4291c40d9e52b88ea9d568c33fa6c..910db36bd27f94bb2265896c3c2e103077c7a510 100644 --- a/resources/static/test/testHelpers/helpers.js +++ b/resources/static/test/testHelpers/helpers.js @@ -199,9 +199,9 @@ BrowserID.TestHelpers = (function() { return str; }, - testObjectHasOwnProperties: function(objToTest, expected, msg) { + testKeysInObject: function(objToTest, expected, msg) { for(var i=0, key; key=expected[i]; ++i) { - ok(objToTest.hasOwnProperty(key), msg || ("object contains " + key)); + ok(key in objToTest, msg || ("object contains " + key)); } }