Merge pull request #254 from tellform/fixSlowAnalyticsBug

Fix Slow Form Analytics Bug
This commit is contained in:
David Baldwynn 2017-11-13 15:00:18 -08:00 committed by GitHub
commit 1f9a0ed1e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 261 additions and 228 deletions

View File

@ -133,7 +133,7 @@ exports.listSubmissions = function(req, res) {
FormSubmission.find({ form: _form._id }).sort('created').lean().exec(function(err, _submissions) {
if (err) {
console.error(err);
res.status(500).send({
return res.status(500).send({
message: errorHandler.getErrorMessage(err)
});
}
@ -141,6 +141,135 @@ exports.listSubmissions = function(req, res) {
});
};
/**
* Get Visitor Analytics Data for a given Form
*/
exports.getVisitorData = function(req, res) {
Form.aggregate([
{
$match: {
_id: mongoose.Types.ObjectId(req.params.formIdNoMiddleware),
admin: mongoose.Types.ObjectId(req.user.id)
}
},
{
$facet: {
"deviceStatistics": [
{
$unwind: '$analytics.visitors'
},
{
$project: {
_id: 0,
deviceType: '$analytics.visitors.deviceType',
SubmittedTimeElapsed: {
$cond: [
{
$eq: ['$analytics.visitors.isSubmitted', true]
},
'$analytics.visitors.timeElapsed',
0
]
},
SubmittedResponses: {
$cond: [
{
$eq: ['$analytics.visitors.isSubmitted', true]
},
1,
0
]
}
}
},
{
$group: {
_id: "$deviceType",
total_time: { $sum: "$SubmittedTimeElapsed" },
responses: { $sum: "$SubmittedResponses" },
visits: { $sum: 1 }
}
},
{
$project: {
total_time: "$total_time",
responses: "$responses",
visits: "$visits",
average_time: {
$divide : ["$total_time", "$responses"]
},
conversion_rate: {
$divide : ["$responses", "$visits"]
}
}
}
],
"globalStatistics": [
{
$unwind: '$analytics.visitors'
},
{
$project: {
_id: 0,
deviceType: '$analytics.visitors.deviceType',
SubmittedTimeElapsed: {
$cond: [
{
$eq: ['$analytics.visitors.isSubmitted', true]
},
'$analytics.visitors.timeElapsed',
0
]
},
SubmittedResponses: {
$cond: [
{
$eq: ['$analytics.visitors.isSubmitted', true]
},
1,
0
]
}
}
},
{
$group: {
_id: null,
total_time: { $sum: "$SubmittedTimeElapsed" },
responses: { $sum: "$SubmittedResponses" },
visits: { $sum: 1 }
}
},
{
$project: {
_id: 0,
total_time: "$total_time",
responses: "$responses",
visits: "$visits",
average_time: {
$divide : ["$total_time", "$responses"]
},
conversion_rate: {
$divide : ["$responses", "$visits"]
}
}
}
],
}
}
], function(err, results){
if (err) {
console.error(err);
return res.status(500).send({
message: errorHandler.getErrorMessage(err)
});
}
return res.json(results);
});
};
/**
* Create a new form
*/
@ -181,7 +310,7 @@ exports.read = function(req, res) {
var newForm = req.form.toJSON();
if(newForm.admin._id === req.user._id){
if(newForm.admin === req.user._id){
return res.json(newForm);
}
@ -323,6 +452,7 @@ exports.formByID = function(req, res, next, id) {
}
Form.findById(id)
.select('admin title language form_fields startPage endPage hideFooter isLive design analytics.gaCode')
.populate('admin')
.exec(function(err, form) {
if (err) {

View File

@ -8,13 +8,8 @@ var mongoose = require('mongoose'),
_ = require('lodash'),
timeStampPlugin = require('../libs/timestamp.server.plugin'),
async = require('async'),
Random = require('random-js'),
mt = Random.engines.mt19937(),
constants = require('../libs/constants');
mt.autoSeed();
//Mongoose Models
var FieldSchema = require('./form_field.server.model.js');
@ -48,8 +43,8 @@ var VisitorDataSchema = new Schema({
referrer: {
type: String
},
lastActiveField: {
type: Schema.Types.ObjectId
filledOutFields: {
type: [Schema.Types.ObjectId]
},
timeElapsed: {
type: Number
@ -112,7 +107,7 @@ var FormSchema = new Schema({
type: Schema.Types.ObjectId,
ref: 'FormSubmission'
}],
dfeault: []
default: []
},
admin: {
type: Schema.Types.ObjectId,
@ -239,94 +234,6 @@ var FormSchema = new Schema({
}
}, formSchemaOptions);
/*
** In-Form Analytics Virtual Attributes
*/
FormSchema.virtual('analytics.views').get(function () {
if(this.analytics && this.analytics.visitors && this.analytics.visitors.length > 0){
return this.analytics.visitors.length;
} else {
return 0;
}
});
FormSchema.virtual('analytics.conversionRate').get(function () {
if(this.analytics && this.analytics.visitors && this.analytics.visitors.length > 0){
return this.submissions.length/this.analytics.visitors.length*100;
} else {
return 0;
}
});
FormSchema.virtual('analytics.fields').get(function () {
var fieldDropoffs = [];
var visitors = this.analytics.visitors;
var that = this;
if(!this.form_fields || this.form_fields.length === 0) {
return null;
}
for(var i=0; i<this.form_fields.length; i++){
var field = this.form_fields[i];
if(field && !field.deletePreserved){
var dropoffViews = _.reduce(visitors, function(sum, visitorObj){
if(visitorObj.lastActiveField+'' === field._id+'' && !visitorObj.isSubmitted){
return sum + 1;
}
return sum;
}, 0);
var continueViews, nextIndex;
if(i !== this.form_fields.length-1){
continueViews = _.reduce(visitors, function(sum, visitorObj){
nextIndex = that.form_fields.indexOf(_.find(that.form_fields, function(o) {
return o._id+'' === visitorObj.lastActiveField+'';
}));
if(nextIndex > i){
return sum + 1;
}
return sum;
}, 0);
} else {
continueViews = _.reduce(visitors, function(sum, visitorObj){
if(visitorObj.lastActiveField+'' === field._id+'' && visitorObj.isSubmitted){
return sum + 1;
}
return sum;
}, 0);
}
var totalViews = dropoffViews+continueViews;
var continueRate = 0;
var dropoffRate = 0;
if(totalViews > 0){
continueRate = (continueViews/totalViews*100).toFixed(0);
dropoffRate = (dropoffViews/totalViews*100).toFixed(0);
}
fieldDropoffs[i] = {
dropoffViews: dropoffViews,
responses: continueViews,
totalViews: totalViews,
continueRate: continueRate,
dropoffRate: dropoffRate,
field: field
};
}
}
return fieldDropoffs;
});
FormSchema.plugin(timeStampPlugin, {
createdPath: 'created',
modifiedPath: 'lastModified',

View File

@ -47,6 +47,8 @@ module.exports = function(app) {
.get(auth.isAuthenticatedOrApiKey, forms.hasAuthorization, forms.listSubmissions)
.delete(auth.isAuthenticatedOrApiKey, forms.hasAuthorization, forms.deleteSubmissions);
app.route('/forms/:formIdNoMiddleware([a-zA-Z0-9]+)/visitors')
.get(auth.isAuthenticatedOrApiKey, forms.getVisitorData);
// Slower formId middleware
app.param('formId', forms.formByID);

View File

@ -22,7 +22,6 @@ module.exports = function (io, socket) {
var newVisitor = {
socketId: data.socketId,
referrer: data.referrer,
lastActiveField: data.lastActiveField,
timeElapsed: data.timeElapsed,
isSubmitted: data.isSubmitted,
language: data.language,

147
package-lock.json generated
View File

@ -2710,6 +2710,11 @@
"resolved": "https://registry.npmjs.org/bson/-/bson-0.4.23.tgz",
"integrity": "sha1-5louPHUH/63kEJvHV1p25Q+NqRU="
},
"buffer-shims": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz",
"integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E="
},
"builtin-modules": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
@ -7035,9 +7040,9 @@
"integrity": "sha1-D1kbGzRL3LPfWXc/Yvu6+Fv0Aos="
},
"hooks-fixed": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-1.1.0.tgz",
"integrity": "sha1-DowVM2cI5mERhf45C0RofdUjDbs="
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.2.tgz",
"integrity": "sha512-YurCM4gQSetcrhwEtpQHhQ4M7Zo7poNGqY4kQGeBS6eZtOcT3tnNs01ThFa0jYBByAiYt1MjMjP/YApG0EnAvQ=="
},
"hosted-git-info": {
"version": "2.5.0",
@ -8002,9 +8007,9 @@
}
},
"kareem": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-1.0.1.tgz",
"integrity": "sha1-eAXSFbtTIU7Dr5aaHQsfF+PnuVw="
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-1.5.0.tgz",
"integrity": "sha1-4+QQHZ3P3imXadr0tNtk2JXRdEg="
},
"karma": {
"version": "0.13.22",
@ -8690,6 +8695,11 @@
}
}
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@ -9365,70 +9375,79 @@
}
},
"mongoose": {
"version": "4.4.20",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.4.20.tgz",
"integrity": "sha1-6XT/tq6MUPQJgBqEl6mOnztR8t0=",
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.13.0.tgz",
"integrity": "sha512-PVUEQ4eS1Bh0Q4IqWRph+li8VMwBxHetdJ1O/P/vE8DktOtBOM1G1G0QOrtQSW1FDrLFSVYkzK4IfI7vJeihQg==",
"requires": {
"async": "1.5.2",
"bson": "0.4.23",
"hooks-fixed": "1.1.0",
"kareem": "1.0.1",
"mongodb": "2.1.18",
"mpath": "0.2.1",
"async": "2.1.4",
"bson": "1.0.4",
"hooks-fixed": "2.0.2",
"kareem": "1.5.0",
"lodash.get": "4.4.2",
"mongodb": "2.2.33",
"mpath": "0.3.0",
"mpromise": "0.5.5",
"mquery": "1.11.0",
"ms": "0.7.1",
"muri": "1.1.0",
"mquery": "2.3.2",
"ms": "2.0.0",
"muri": "1.3.0",
"regexp-clone": "0.0.1",
"sliced": "1.0.1"
},
"dependencies": {
"async": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz",
"integrity": "sha1-LSFgx3iAMuTdbL4lAvH5osj2zeQ=",
"requires": {
"lodash": "4.17.4"
}
},
"bluebird": {
"version": "2.10.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.10.2.tgz",
"integrity": "sha1-AkpVFylTCIV/FPkfEQb8O1VfRGs="
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA=="
},
"bson": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz",
"integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw="
},
"debug": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
"integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "0.7.1"
"ms": "2.0.0"
}
},
"es6-promise": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz",
"integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y="
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz",
"integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q="
},
"mongodb": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.1.18.tgz",
"integrity": "sha1-KNQLUVsr5NWmn/3UxTXw30MuQJc=",
"version": "2.2.33",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.33.tgz",
"integrity": "sha1-tTfEcdNKZlG0jzb9vyl1A0Dgi1A=",
"requires": {
"es6-promise": "3.0.2",
"mongodb-core": "1.3.18",
"readable-stream": "1.0.31"
"es6-promise": "3.2.1",
"mongodb-core": "2.1.17",
"readable-stream": "2.2.7"
}
},
"mongodb-core": {
"version": "1.3.18",
"resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-1.3.18.tgz",
"integrity": "sha1-kGhLO3xzVtZa41Y5HTCw8kiATHo=",
"version": "2.1.17",
"resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.17.tgz",
"integrity": "sha1-pBizN6FKFJkPtRC5I97mqBMXPfg=",
"requires": {
"bson": "0.4.23",
"bson": "1.0.4",
"require_optional": "1.0.1"
}
},
"mpath": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.2.1.tgz",
"integrity": "sha1-Ok6Ck1mAHeljCcJ6ay4QLon56W4="
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.3.0.tgz",
"integrity": "sha1-elj3iem1/TyUUgY0FXlg8mvV70Q="
},
"mpromise": {
"version": "0.5.5",
@ -9436,12 +9455,12 @@
"integrity": "sha1-9bJCWddjrMIlewoMjG2Gb9UXMuY="
},
"mquery": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-1.11.0.tgz",
"integrity": "sha1-4MZd7bEDftv2z7iCYud3/uI1Udk=",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.2.tgz",
"integrity": "sha512-KXWMypZSvhCuqRtza+HMQZdYw7PfFBjBTFvP31NNAq0OX0/NTIgpcDpkWQ2uTxk6vGQtwQ2elhwhs+ZvCA8OaA==",
"requires": {
"bluebird": "2.10.2",
"debug": "2.2.0",
"bluebird": "3.5.1",
"debug": "2.6.9",
"regexp-clone": "0.0.1",
"sliced": "0.0.5"
},
@ -9453,31 +9472,29 @@
}
}
},
"ms": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
"integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg="
"muri": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/muri/-/muri-1.3.0.tgz",
"integrity": "sha512-FiaFwKl864onHFFUV/a2szAl7X0fxVlSKNdhTf+BM8i8goEgYut8u5P9MqQqIYwvaMxjzVESsoEm/2kfkFH1rg=="
},
"readable-stream": {
"version": "1.0.31",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz",
"integrity": "sha1-jyUC4LyeOw2huUUgqrtOJgPsr64=",
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz",
"integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=",
"requires": {
"buffer-shims": "1.0.0",
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "0.0.1",
"string_decoder": "0.10.31"
"isarray": "1.0.0",
"process-nextick-args": "1.0.7",
"string_decoder": "1.0.3",
"util-deprecate": "1.0.2"
}
},
"sliced": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
}
}
},

View File

@ -21,8 +21,8 @@
"generate": "all-contributors generate",
"start": "grunt",
"test": "grunt test",
"postinstall": "bower install --config.interactive=false",
"travis": "grunt test:travis",
"postinstall": "bower install --config.interactive=false; grunt build;",
"init": "node scripts/setup.js"
},
"dependencies": {
@ -58,7 +58,7 @@
"main-bower-files": "~2.9.0",
"method-override": "~2.3.0",
"mkdirp": "^0.5.1",
"mongoose": "~4.4.19",
"mongoose": "^4.13.0",
"morgan": "~1.8.1",
"nodemailer": "~4.0.0",
"passport": "~0.3.0",
@ -86,7 +86,6 @@
"grunt-closure-compiler": "0.0.21",
"grunt-contrib-concat": "^1.0.1",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-uglify": "^0.11.1",
"grunt-contrib-watch": "~0.6.1",
"grunt-execute": "^0.2.2",
"grunt-karma": "~0.12.1",

View File

@ -14,6 +14,10 @@ angular.module('forms').directive('editSubmissionsFormDirective', ['$rootScope',
masterChecker: false,
rows: []
};
$scope.analyticsData = {
deviceStatistics: [],
globalStatistics: []
};
$scope.deletionInProgress = false;
$scope.waitingForDeletion = false;
@ -73,11 +77,11 @@ angular.module('forms').directive('editSubmissionsFormDirective', ['$rootScope',
method: 'GET',
url: '/forms/'+$scope.myform._id+'/visitors'
}).then(function successCallback(response) {
var defaultFormFields = _.cloneDeep($scope.myform.form_fields);
var data = response.data || [];
var visitors = response.data || [];
$scope.visitors = visitors;
$scope.analyticsData = data[0];
$scope.analyticsData.globalStatistics = $scope.analyticsData.globalStatistics[0];
$scope.analyticsData.deviceStatistics = formatDeviceStatistics($scope.analyticsData.deviceStatistics);
});
};
@ -102,26 +106,12 @@ angular.module('forms').directive('editSubmissionsFormDirective', ['$rootScope',
/*
** Analytics Functions
*/
$scope.AverageTimeElapsed = (function(){
var totalTime = 0;
var numSubmissions = $scope.table.rows.length;
for(var i=0; i<$scope.table.rows.length; i++){
totalTime += $scope.table.rows[i].timeElapsed;
}
if(numSubmissions === 0) {
return 0;
}
return (totalTime/numSubmissions).toFixed(0);
})();
$scope.DeviceStatistics = (function(){
var formatDeviceStatistics = function(deviceStatData){
var newStatItem = function(){
return {
visits: 0,
responses: 0,
completion: 0,
conversion_rate: 0,
average_time: 0,
total_time: 0
};
@ -134,30 +124,16 @@ angular.module('forms').directive('editSubmissionsFormDirective', ['$rootScope',
other: newStatItem()
};
if($scope.myform.analytics && $scope.myform.analytics.visitors) {
var visitors = $scope.myform.analytics.visitors;
for (var i = 0; i < visitors.length; i++) {
var visitor = visitors[i];
var deviceType = visitor.deviceType;
stats[deviceType].visits++;
if (visitor.isSubmitted) {
stats[deviceType].total_time = stats[deviceType].total_time + visitor.timeElapsed;
stats[deviceType].responses++;
}
if(stats[deviceType].visits) {
stats[deviceType].completion = 100*(stats[deviceType].responses / stats[deviceType].visits).toFixed(2);
}
if(stats[deviceType].responses){
stats[deviceType].average_time = (stats[deviceType].total_time / stats[deviceType].responses).toFixed(0);
if(deviceStatData.length){
for(var i=0; i<deviceStatData.length; i++){
var currDevice = deviceStatData[i];
if(stats[currDevice._id]){
stats[currDevice._id] = currDevice;
}
}
}
return stats;
})();
};
/*
** Table Functions

View File

@ -19,19 +19,19 @@
</div>
<div class="col-xs-12 header-numbers">
<div class="col-xs-3">
{{myform.analytics.visitors.length}}
{{analyticsData.globalStatistics.visits | number:0}}
</div>
<div class="col-xs-3">
{{myform.analytics.submissions}}
{{analyticsData.globalStatistics.responses | number:0}}
</div>
<div class="col-xs-3">
{{myform.analytics.conversionRate | number:0}}%
{{analyticsData.globalStatistics.conversion_rate | number:2}}
</div>
<div class="col-xs-3">
{{ AverageTimeElapsed | secondsToDateTime | date:'mm:ss'}}
{{analyticsData.globalStatistics.average_time | secondsToDateTime | date:'mm:ss'}}
</div>
</div>
<div class="col-xs-12 detailed-title">
@ -58,7 +58,7 @@
{{ 'UNIQUE_VISITS' | translate }}
</div>
<div class="row">
{{DeviceStatistics.desktop.visits}}
{{analyticsData.deviceStatistics.desktop.visits | number:0}}
</div>
</div>
@ -67,7 +67,7 @@
{{ 'UNIQUE_VISITS' | translate }}
</div>
<div class="row">
{{DeviceStatistics.tablet.visits}}
{{analyticsData.deviceStatistics.tablet.visits | number:0}}
</div>
</div>
@ -76,7 +76,7 @@
{{ 'UNIQUE_VISITS' | translate }}
</div>
<div class="row">
{{DeviceStatistics.tablet.visits}}
{{analyticsData.deviceStatistics.tablet.visits | number:0}}
</div>
</div>
@ -85,7 +85,7 @@
{{ 'UNIQUE_VISITS' | translate }}
</div>
<div class="row">
{{DeviceStatistics.other.visits}}
{{analyticsData.deviceStatistics.other.visits | number:0}}
</div>
</div>
</div>
@ -96,7 +96,7 @@
{{ 'RESPONSES' | translate }}
</div>
<div class="row">
{{DeviceStatistics.desktop.responses}}
{{analyticsData.deviceStatistics.desktop.responses | number:0}}
</div>
</div>
@ -105,7 +105,7 @@
{{ 'RESPONSES' | translate }}
</div>
<div class="row">
{{DeviceStatistics.tablet.responses}}
{{analyticsData.deviceStatistics.tablet.responses | number:0}}
</div>
</div>
@ -114,7 +114,7 @@
{{ 'RESPONSES' | translate }}
</div>
<div class="row">
{{DeviceStatistics.phone.responses}}
{{analyticsData.deviceStatistics.phone.responses | number:0}}
</div>
</div>
@ -123,7 +123,7 @@
{{ 'RESPONSES' | translate }}
</div>
<div class="row">
{{DeviceStatistics.other.responses}}
{{analyticsData.deviceStatistics.other.responses | number:0}}
</div>
</div>
</div>
@ -134,7 +134,7 @@
{{ 'COMPLETION_RATE' | translate }}
</div>
<div class="row">
{{DeviceStatistics.desktop.completion}}%
{{analyticsData.deviceStatistics.desktop.conversion_rate | number:2}}%
</div>
</div>
@ -143,7 +143,7 @@
{{ 'COMPLETION_RATE' | translate }}
</div>
<div class="row">
{{DeviceStatistics.tablet.completion}}%
{{analyticsData.deviceStatistics.tablet.conversion_rate | number:2}}%
</div>
</div>
@ -152,7 +152,7 @@
{{ 'COMPLETION_RATE' | translate }}
</div>
<div class="row">
{{DeviceStatistics.phone.completion}}%
{{analyticsData.deviceStatistics.phone.conversion_rate | number:2}}%
</div>
</div>
@ -161,7 +161,7 @@
{{ 'COMPLETION_RATE' | translate }}
</div>
<div class="row">
{{DeviceStatistics.other.completion}}%
{{analyticsData.deviceStatistics.other.conversion_rate | number:2}}%
</div>
</div>
</div>
@ -172,7 +172,7 @@
{{ 'AVERAGE_TIME_TO_COMPLETE' | translate }}
</div>
<div class="row">
{{DeviceStatistics.desktop.average_time | secondsToDateTime | date:'mm:ss'}}
{{analyticsData.deviceStatistics.desktop.average_time | secondsToDateTime | date:'mm:ss'}}
</div>
</div>
@ -181,7 +181,7 @@
{{ 'AVERAGE_TIME_TO_COMPLETE' | translate }}
</div>
<div class="row">
{{DeviceStatistics.tablet.average_time | secondsToDateTime | date:'mm:ss'}}
{{analyticsData.deviceStatistics.tablet.average_time | secondsToDateTime | date:'mm:ss'}}
</div>
</div>
@ -190,7 +190,7 @@
{{ 'AVERAGE_TIME_TO_COMPLETE' | translate }}
</div>
<div class="row">
{{DeviceStatistics.phone.average_time | secondsToDateTime | date:'mm:ss'}}
{{analyticsData.deviceStatistics.phone.average_time | secondsToDateTime | date:'mm:ss'}}
</div>
</div>
@ -199,11 +199,12 @@
{{ 'AVERAGE_TIME_TO_COMPLETE' | translate }}
</div>
<div class="row">
{{DeviceStatistics.other.average_time | secondsToDateTime | date:'mm:ss'}}
{{analyticsData.deviceStatistics.other.average_time | secondsToDateTime | date:'mm:ss'}}
</div>
</div>
</div>
<!--
<div class="col-xs-12 field-title-row">
<div class="col-xs-3">
@ -222,7 +223,6 @@
</div>
<div class="col-xs-12 field-detailed-row" ng-repeat="fieldStats in myform.analytics.fields">
<div class="col-xs-3">
{{fieldStats.field.title}}
</div>
@ -236,7 +236,10 @@
{{fieldStats.continueRate}}%
</div>
</div>
-->
</div>
<br>
<div class="row table-tools">
<div class="col-xs-2">