Bug 1048785 Part 1: Update Loop FeedbackAPIClient to allow attaching more metadata. r=Standard8

This commit is contained in:
Nicolas Perriault 2014-08-08 18:10:51 +01:00
parent caa4d0fafa
commit bc72f4efa3
7 changed files with 132 additions and 53 deletions

View File

@ -248,11 +248,18 @@ loop.conversation = (function(OT, mozL10n) {
feedback: function() {
document.title = mozL10n.get("call_has_ended");
var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
"feedback.baseUrl");
var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
product: navigator.mozLoop.getLoopCharPref("feedback.product"),
platform: navigator.platform
// XXX add "channel" and "version" options as per bug 1048785; these
// could be provided by the mozLoop API.
});
this.loadReactComponent(sharedViews.FeedbackView({
feedbackApiClient: new loop.FeedbackAPIClient({
baseUrl: navigator.mozLoop.getLoopCharPref("feedback.baseUrl"),
product: navigator.mozLoop.getLoopCharPref("feedback.product")
})
feedbackApiClient: feedbackClient
}));
}
});

View File

@ -248,11 +248,18 @@ loop.conversation = (function(OT, mozL10n) {
feedback: function() {
document.title = mozL10n.get("call_has_ended");
var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
"feedback.baseUrl");
var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
product: navigator.mozLoop.getLoopCharPref("feedback.product"),
platform: navigator.platform
// XXX add "channel" and "version" options as per bug 1048785; these
// could be provided by the mozLoop API.
});
this.loadReactComponent(sharedViews.FeedbackView({
feedbackApiClient: new loop.FeedbackAPIClient({
baseUrl: navigator.mozLoop.getLoopCharPref("feedback.baseUrl"),
product: navigator.mozLoop.getLoopCharPref("feedback.product")
})
feedbackApiClient: feedbackClient
}));
}
});

View File

@ -5,58 +5,82 @@
/* global loop:true */
var loop = loop || {};
loop.FeedbackAPIClient = (function($) {
loop.FeedbackAPIClient = (function($, _) {
"use strict";
/**
* Feedback API client. Sends feedback data to an input.mozilla.com compatible
* API.
*
* Available settings:
* - {String} baseUrl Base API url (required)
* @param {String} baseUrl Base API url (required)
* @param {Object} defaults Defaults field values for that client.
*
* Required defaults:
* - {String} product Product name (required)
*
* @param {Object} settings Settings.
* Optional defaults:
* - {String} platform Platform name, eg. "Windows 8", "Android", "Linux"
* - {String} version Product version, eg. "22b2", "1.1"
* - {String} channel Product channel, eg. "stable", "beta"
* - {String} user_agent eg. Mozilla/5.0 (Mobile; rv:18.0) Gecko/18.0 Firefox/18.0
*
* @link http://fjord.readthedocs.org/en/latest/api.html
*/
function FeedbackAPIClient(settings) {
settings = settings || {};
if (!settings.hasOwnProperty("baseUrl")) {
throw new Error("Missing required baseUrl setting.");
function FeedbackAPIClient(baseUrl, defaults) {
this.baseUrl = baseUrl;
if (!this.baseUrl) {
throw new Error("Missing required 'baseUrl' argument.");
}
this._baseUrl = settings.baseUrl;
if (!settings.hasOwnProperty("product")) {
throw new Error("Missing required product setting.");
this.defaults = defaults || {};
// required defaults checks
if (!this.defaults.hasOwnProperty("product")) {
throw new Error("Missing required 'product' default.");
}
this._product = settings.product;
}
FeedbackAPIClient.prototype = {
/**
* Formats Feedback data to match the API spec.
*
* @param {Object} fields Feedback form data.
* @return {Object} Formatted data.
* Supported field names by the feedback API.
* @type {Array}
*/
_formatData: function(fields) {
var formatted = {};
_supportedFields: ["happy",
"category",
"description",
"product",
"platform",
"version",
"channel",
"user_agent"],
/**
* Creates a formatted payload object compliant with the Feedback API spec
* against validated field data.
*
* @param {Object} fields Feedback initial values.
* @return {Object} Formatted payload object.
* @throws {Error} If provided values are invalid
*/
_createPayload: function(fields) {
if (typeof fields !== "object") {
throw new Error("Invalid feedback data provided.");
}
formatted.product = this._product;
formatted.happy = fields.happy;
formatted.category = fields.category;
Object.keys(fields).forEach(function(name) {
if (this._supportedFields.indexOf(name) === -1) {
throw new Error("Unsupported field " + name);
}
}, this);
// Payload is basically defaults + fields merged in
var payload = _.extend({}, this.defaults, fields);
// Default description field value
if (!fields.description) {
formatted.description = (fields.happy ? "Happy" : "Sad") + " User";
} else {
formatted.description = fields.description;
payload.description = (fields.happy ? "Happy" : "Sad") + " User";
}
return formatted;
return payload;
},
/**
@ -67,11 +91,11 @@ loop.FeedbackAPIClient = (function($) {
*/
send: function(fields, cb) {
var req = $.ajax({
url: this._baseUrl,
url: this.baseUrl,
method: "POST",
contentType: "application/json",
dataType: "json",
data: JSON.stringify(this._formatData(fields))
data: JSON.stringify(this._createPayload(fields))
});
req.done(function(result) {
@ -89,4 +113,4 @@ loop.FeedbackAPIClient = (function($) {
};
return FeedbackAPIClient;
})(jQuery);
})(jQuery, _);

View File

@ -31,13 +31,13 @@ describe("loop.FeedbackAPIClient", function() {
it("should require a baseUrl setting", function() {
expect(function() {
return new loop.FeedbackAPIClient();
}).to.Throw(/required baseUrl/);
}).to.Throw(/required 'baseUrl'/);
});
it("should require a product setting", function() {
expect(function() {
return new loop.FeedbackAPIClient({baseUrl: "http://fake"});
}).to.Throw(/required product/);
return new loop.FeedbackAPIClient("http://fake", {});
}).to.Throw(/required 'product'/);
});
});
@ -45,9 +45,9 @@ describe("loop.FeedbackAPIClient", function() {
var client;
beforeEach(function() {
client = new loop.FeedbackAPIClient({
baseUrl: "http://fake/feedback",
product: "Hello"
client = new loop.FeedbackAPIClient("http://fake/feedback", {
product: "Hello",
version: "42b1"
});
});
@ -103,16 +103,57 @@ describe("loop.FeedbackAPIClient", function() {
expect(parsed.description).eql("it's far too awesome!");
});
it("should send product information", function() {
client.send({product: "Hello"}, function(){});
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.product).eql("Hello");
});
it("should send platform information when provided", function() {
client.send({platform: "Windows 8"}, function(){});
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.platform).eql("Windows 8");
});
it("should send channel information when provided", function() {
client.send({channel: "beta"}, function(){});
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.channel).eql("beta");
});
it("should send version information when provided", function() {
client.send({version: "42b1"}, function(){});
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.version).eql("42b1");
});
it("should send user_agent information when provided", function() {
client.send({user_agent: "MOZAGENT"}, function(){});
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.user_agent).eql("MOZAGENT");
});
it("should throw on invalid feedback data", function() {
expect(function() {
client.send("invalid data", function(){});
}).to.Throw(/Invalid/);
});
it("should throw on unsupported field name", function() {
expect(function() {
client.send({bleh: "bah"}, function(){});
}).to.Throw(/Unsupported/);
});
it("should call passed callback on success", function() {
var cb = sandbox.spy();
var fakeResponseData = {description: "confusing"};
client.send({reason: "confusing"}, cb);
client.send({category: "confusing"}, cb);
requests[0].respond(200, {"Content-Type": "application/json"},
JSON.stringify(fakeResponseData));
@ -124,7 +165,7 @@ describe("loop.FeedbackAPIClient", function() {
it("should call passed callback on error", function() {
var cb = sandbox.spy();
var fakeErrorData = {error: true};
client.send({reason: "confusing"}, cb);
client.send({category: "confusing"}, cb);
requests[0].respond(400, {"Content-Type": "application/json"},
JSON.stringify(fakeErrorData));

View File

@ -15,7 +15,7 @@
<div id="main"></div>
<script src="fake-mozLoop.js"></script>
<script src="fake-l10n.js"></script>
<script src="../content/libs/sdk.js"></script>
<script src="../content/shared/libs/sdk.js"></script>
<script src="../content/shared/libs/react-0.11.1.js"></script>
<script src="../content/shared/libs/jquery-2.1.0.js"></script>
<script src="../content/shared/libs/lodash-2.4.1.js"></script>

View File

@ -35,10 +35,10 @@
// Feedback API client configured to send data to the stage input server,
// which is available at https://input.allizom.org
var stageFeedbackApiClient = new loop.FeedbackAPIClient({
baseUrl: "https://input.allizom.org/api/v1/feedback",
product: "Loop"
});
var stageFeedbackApiClient = new loop.FeedbackAPIClient(
"https://input.allizom.org/api/v1/feedback", {
product: "Loop"
});
var Example = React.createClass({displayName: 'Example',
render: function() {

View File

@ -35,10 +35,10 @@
// Feedback API client configured to send data to the stage input server,
// which is available at https://input.allizom.org
var stageFeedbackApiClient = new loop.FeedbackAPIClient({
baseUrl: "https://input.allizom.org/api/v1/feedback",
product: "Loop"
});
var stageFeedbackApiClient = new loop.FeedbackAPIClient(
"https://input.allizom.org/api/v1/feedback", {
product: "Loop"
});
var Example = React.createClass({
render: function() {