added analytics

This commit is contained in:
David Baldwynn 2016-06-06 17:37:09 -07:00
parent 709c2c68d2
commit 20634f8f19
19 changed files with 484 additions and 46 deletions

View file

@ -19,12 +19,12 @@ var mongoose = require('mongoose'),
*/
exports.uploadPDF = function(req, res, next) {
console.log('inside uploadPDF');
//console.log('inside uploadPDF');
// console.log('\n\nProperty Descriptor\n-----------');
// console.log(Object.getOwnPropertyDescriptor(req.files.file, 'path'));
console.log(req.file);
//console.log(req.file);
if(req.file){
var pdfFile = req.file;
@ -159,9 +159,20 @@ exports.deleteSubmissions = function(req, res) {
res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
return;
}
res.status(200).send('Form submissions successfully deleted');
form.analytics.visitors = [];
form.save(function(err){
if(err){
res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
return;
}
res.status(200).send('Form submissions successfully deleted');
});
});
};
@ -171,8 +182,6 @@ exports.deleteSubmissions = function(req, res) {
exports.createSubmission = function(req, res) {
var form = req.form;
// console.log('in createSubmission()');
// console.log(req.body);
var submission = new FormSubmission({
admin: req.form.admin._id,
@ -212,7 +221,7 @@ exports.createSubmission = function(req, res) {
submission.save(function(err, submission){
if(err){
console.log(err.message);
res.status(400).send({
res.status(500).send({
message: errorHandler.getErrorMessage(err)
});
}
@ -280,8 +289,9 @@ exports.create = function(req, res) {
exports.read = function(req, res) {
var validUpdateTypes= Form.schema.path('plugins.oscarhost.settings.updateType').enumValues;
var newForm = JSON.parse(JSON.stringify(req.form));
var newForm = req.form.toJSON({virtuals : true});
newForm.plugins.oscarhost.settings.validUpdateTypes = validUpdateTypes;
res.json(newForm);
};
@ -376,11 +386,12 @@ exports.formByID = function(req, res, next, id) {
}
else {
//Remove sensitive information from User object
form.admin.password = undefined;
form.admin.salt = undefined;
form.provider = undefined;
var _form = form;
_form.admin.password = undefined;
_form.admin.salt = undefined;
_form.provider = undefined;
req.form = form;
req.form = _form;
return next();
}
});

View file

@ -46,6 +46,21 @@ var ButtonSchema = new Schema({
}
});
var VisitorDataSchema = new Schema({
referrer: {
type: String
},
lastActiveField: {
type: Schema.Types.ObjectId
},
timeElapsed: {
type: Number
},
isSubmitted: {
type: Boolean
}
});
/**
* Form Schema
*/
@ -66,6 +81,13 @@ var FormSchema = new Schema({
default: ''
},
analytics:{
gaCode: {
type: String
},
visitors: [VisitorDataSchema]
},
form_fields: [FieldSchema],
submissions: [{
type: Schema.Types.ObjectId,
@ -193,16 +215,91 @@ var FormSchema = new Schema({
},
auth: {
user: {
type: String,
type: String
},
pass: {
type: String,
type: String
}
}
}
}
});
/*
** In-Form Analytics Virtual Attributes
*/
FormSchema.virtual('analytics.views').get(function () {
return this.analytics.visitors.length;
});
FormSchema.virtual('analytics.submissions').get(function () {
return this.submissions.length;
});
FormSchema.virtual('analytics.conversionRate').get(function () {
return this.submissions.length/this.analytics.visitors.length*100;
});
FormSchema.virtual('analytics.fields').get(function () {
var fieldDropoffs = [];
var visitors = this.analytics.visitors;
var that = this;
for(var i=0; i<this.form_fields.length; i++){
var field = this.form_fields[i];
if(!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 = continueViews/totalViews*100;
var dropoffRate = dropoffViews/totalViews*100;
fieldDropoffs[i] = {
dropoffViews: dropoffViews,
continueViews: continueViews,
totalViews: totalViews,
continueRate: continueRate,
dropoffRate: dropoffRate,
field: field
};
}
}
return fieldDropoffs;
});
FormSchema.plugin(mUtilities.timestamp, {
createdPath: 'created',
modifiedPath: 'lastModified',

View file

@ -193,15 +193,11 @@ FormFieldSchema.pre('validate', function(next) {
//Submission fieldValue correction
FormFieldSchema.pre('save', function(next) {
console.log('pre save');
console.log(this.isSubmission);
console.log(this._id);
console.log(this.submissionId);
console.log(this.fieldType);
if(this.isSubmission && this.fieldType === 'dropdown'){
if(this.fieldType === 'dropdown' && this.isSubmission){
//console.log(this);
this.fieldValue = this.fieldValue.option_value;
//console.log(this.fieldValue);
}
return next();

View file

@ -51,6 +51,7 @@ function formFieldsSetter(form_fields){
form_fields[i].submissionId = form_fields[i]._id;
form_fields[i]._id = new mongoose.mongo.ObjectID();
}
//console.log(form_fields)
return form_fields;
}
@ -127,7 +128,6 @@ var FormSubmissionSchema = new Schema({
default: false
}
}
});
FormSubmissionSchema.path('form_fields').set(formFieldsSetter);
@ -267,7 +267,6 @@ FormSubmissionSchema.pre('save', function (next) {
self = this,
_form = this.form;
if(this.pdf && this.pdf.path){
dest_filename = self.title.replace(/ /g,'')+'_submission_'+Date.now()+'.pdf';
var __path = this.pdf.path.split('/').slice(0,this.pdf.path.split('/').length-1).join('/');

View file

@ -0,0 +1,80 @@
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
config = require('../../config/config'),
errorHandler = require('../controllers/errors.server.controller'),
Form = mongoose.model('Form');
// Create the chat configuration
module.exports = function (io, socket) {
var visitorsData = {};
var saveVisitorData = function (data, cb){
Form.findById(data.formId, function(err, form) {
if (err) {
console.log(err);
throw new Error(errorHandler.getErrorMessage(err));
}
var newVisitor = {
referrer: data.referrer,
lastActiveField: data.lastActiveField,
timeElapsed: data.timeElapsed,
isSubmitted: data.isSubmitted
};
form.analytics.visitors.push(newVisitor);
form.save(function (err) {
if (err) {
console.log(err);
throw new Error(errorHandler.getErrorMessage(err));
}
console.log('\n\nVisitor data successfully added!');
delete visitorsData[socket.id];
if(cb) cb();
});
});
socket.disconnect(0);
};
io.on('connection', function(socket) {
// a user has visited our page - add them to the visitorsData object
socket.on('form-visitor-data', function(data) {
console.log('\n\nuser has visited our page');
visitorsData[socket.id] = data;
console.log(data);
if (data.isSubmitted) {
saveVisitorData(data, function () {
console.log('\n\n user submitted form');
socket.disconnect(0);
});
}
});
socket.on('disconnect', function() {
var data = visitorsData[socket.id];
if(data){
if(!data.isSubmitted) {
saveVisitorData(data);
}
}
});
});
};

View file

@ -80,8 +80,12 @@
<!--Embedding The signupDisabled Boolean-->
<script type="text/javascript">
var signupDisabled = {{signupDisabled | safe}};
var socketPort = {{socketPort | safe}};
</script>
<!--Socket.io Client Dependency-->
<script src="https://cdn.socket.io/socket.io-1.4.5.js"></script>
<!--Bower JS dependencies-->
{% for bowerJSFile in bowerJSFiles %}
<script type="text/javascript" src="{{bowerJSFile}}"></script>
@ -103,7 +107,7 @@
<script src="https://cdn.ravenjs.com/2.3.0/angular/raven.min.js"></script>
<script>
Raven.config('http://825fefd6b4ed4a4da199c1b832ca845c@sentry.tellform.com/2').install();
Raven.config('https://825fefd6b4ed4a4da199c1b832ca845c@sentry.tellform.com/2').install();
</script>
<!-- [if lt IE 9]>

8
config/env/all.js vendored
View file

@ -8,6 +8,8 @@ module.exports = {
keywords: process.env.APP_KEYWORDS || 'typeform, pdfs, forms, opensource, formbuilder, google forms, nodejs'
},
port: process.env.PORT || 3000,
socketPort: process.env.SOCKET_PORT || 35729,
templateEngine: 'swig',
reCAPTCHA_Key: process.env.reCAPTCHA_KEY || '',
@ -81,15 +83,15 @@ module.exports = {
views: [
'public/modules/*/views/*.html',
'public/modules/*/views/*/*.html',
'public/modules/*/views/*/*/*.html',
'public/modules/*/views/*/*/*.html'
],
unit_tests: [
'public/lib/angular-mocks/angular-mocks.js',
'public/modules/*/tests/unit/*.js',
'public/modules/*/tests/unit/**/*.js',
'public/modules/*/tests/unit/**/*.js'
],
e2e_tests: [
'public/modules/*/tests/e2e/**.js',
'public/modules/*/tests/e2e/**.js'
]
}
};

View file

@ -26,7 +26,16 @@ var fs = require('fs-extra'),
device = require('express-device'),
client = new raven.Client(config.DSN);
/**
* Configure Socket.io
*/
var configureSocketIO = function (app, db) {
// Load the Socket.io configuration
var server = require('./socket.io')(app, db);
// Return server object
return server;
};
module.exports = function(db) {
// Initialize express app
@ -43,7 +52,8 @@ module.exports = function(db) {
app.locals.signupDisabled = config.signupDisabled;
app.locals.description = config.app.description;
app.locals.keywords = config.app.keywords;
app.locals.socketPort = config.socketPort;
app.locals.bowerJSFiles = config.getBowerJSAssets();
app.locals.bowerCssFiles = config.getBowerCSSAssets();
app.locals.bowerOtherFiles = config.getBowerOtherAssets();
@ -146,11 +156,11 @@ module.exports = function(db) {
// Add headers for Sentry
/*
app.use(function (req, res, next) {
// Website you wish to allow to connect
res.setHeader('Access-Control-Allow-Origin', 'http://sentry.polydaic.com');
res.setHeader('Access-Control-Allow-Origin', 'https://sentry.polydaic.com');
// Request methods you wish to allow
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
@ -165,7 +175,7 @@ module.exports = function(db) {
// Pass to next layer of middleware
next();
});
*/
// Sentry (Raven) middleware
// app.use(raven.middleware.express.requestHandler(config.DSN));
@ -211,6 +221,8 @@ module.exports = function(db) {
return httpsServer;
}
app = configureSocketIO(app, db);
// Return Express server instance
return app;
};

25
config/socket.io.js Normal file
View file

@ -0,0 +1,25 @@
'use strict';
// Load the module dependencies
var config = require('./config'),
path = require('path'),
http = require('http'),
socketio = require('socket.io'),
session = require('express-session');
// Define the Socket.io configuration method
module.exports = function (app, db) {
var server = http.createServer(app);
// Create a new Socket.io server
var io = socketio.listen(server);
// Add an event listener to the 'connection' event
io.on('connection', function (socket) {
config.getGlobbedFiles('./app/sockets/**.js').forEach(function (socketConfiguration) {
require(path.resolve(socketConfiguration))(io, socket);
});
});
return server;
};

View file

@ -0,0 +1,12 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
AnonymousStrategy = require('passport-anonymous').Strategy;
module.exports = function() {
// Use local strategy
passport.use(new AnonymousStrategy());
};

View file

@ -70,6 +70,7 @@
"node-freegeoip": "0.0.1",
"nodemailer": "~1.10.0",
"passport": "~0.3.0",
"passport-anonymous": "^1.0.1",
"passport-facebook": "~2.0.0",
"passport-github": "~1.0.0",
"passport-google-oauth": "~0.2.0",
@ -82,6 +83,7 @@
"random-js": "^1.0.8",
"raven": "^0.9.0",
"soap": "^0.11.0",
"socket.io": "^1.4.6",
"swig": "~1.4.1"
},
"devDependencies": {

View file

@ -0,0 +1,55 @@
(function () {
'use strict';
// Create the Socket.io wrapper service
angular
.module('core')
.factory('Socket', Socket);
Socket.$inject = ['$timeout', '$window'];
function Socket($timeout, $window) {
var service = {
connect: connect,
emit: emit,
on: on,
removeListener: removeListener,
socket: null
};
console.log('https://'+window.location.hostname+':'+$window.socketPort);
connect('https://'+window.location.hostname+':'+$window.socketPort);
return service;
// Connect to Socket.io server
function connect(url) {
service.socket = io();
}
// Wrap the Socket.io 'emit' method
function emit(eventName, data) {
if (service.socket) {
service.socket.emit(eventName, data);
}
}
// Wrap the Socket.io 'on' method
function on(eventName, callback) {
if (service.socket) {
service.socket.on(eventName, function (data) {
$timeout(function () {
callback(data);
});
});
}
}
// Wrap the Socket.io 'removeListener' method
function removeListener(eventName) {
if (service.socket) {
service.socket.removeListener(eventName);
}
}
}
}());

View file

@ -0,0 +1,44 @@
(function () {
'use strict';
// Create the SendVisitorData service
angular
.module('forms')
.factory('SendVisitorData', SendVisitorData);
SendVisitorData.$inject = ['Socket', '$state'];
function SendVisitorData(Socket, $state) {
// Create a controller method for sending visitor data
function send(form, lastActiveIndex, timeElapsed) {
console.log(lastActiveIndex);
// Create a new message object
var visitorData = {
referrer: document.referrer,
isSubmitted: form.submitted,
formId: form._id,
lastActiveField: form.form_fields[lastActiveIndex]._id,
timeElapsed: timeElapsed
};
Socket.emit('form-visitor-data', visitorData);
}
function init(){
// Make sure the Socket is connected
if (!Socket.socket) {
Socket.connect();
}
}
var service = {
send: send
};
init();
return service;
}
}());

View file

@ -1,7 +1,7 @@
'use strict';
angular.module('forms').directive('submitFormDirective', ['$http', 'TimeCounter', '$filter', '$rootScope', 'Auth',
function ($http, TimeCounter, $filter, $rootScope, Auth) {
angular.module('forms').directive('submitFormDirective', ['$http', 'TimeCounter', '$filter', '$rootScope', 'Auth', 'SendVisitorData',
function ($http, TimeCounter, $filter, $rootScope, Auth, SendVisitorData) {
return {
templateUrl: 'modules/forms/views/directiveViews/form/submit-form.client.view.html',
restrict: 'E',
@ -41,6 +41,7 @@ angular.module('forms').directive('submitFormDirective', ['$http', 'TimeCounter'
TimeCounter.restartClock();
};
//Fire event when window is scrolled
$window.onscroll = function(){
$scope.scrollPos = document.body.scrollTop || document.documentElement.scrollTop || 0;
var elemBox = document.getElementsByClassName('activeField')[0].getBoundingClientRect();
@ -80,13 +81,22 @@ angular.module('forms').directive('submitFormDirective', ['$http', 'TimeCounter'
}
};
$rootScope.setDropdownOption = function(){
console.log('setDropdownOption index: ');
};
/*
** Field Controls
*/
var getActiveField = function(){
if($scope.selected === null){
console.error('current active field is null');
throw new Error('current active field is null');
}
if($scope.selected._id === 'submit_field') {
return $scope.myform.form_fields.length - 1;
} else {
return $scope.selected.index;
}
};
$scope.setActiveField = $rootScope.setActiveField = function(field_id, field_index, animateScroll) {
if($scope.selected === null || $scope.selected._id === field_id){
//console.log('not scrolling');
@ -129,6 +139,8 @@ angular.module('forms').directive('submitFormDirective', ['$http', 'TimeCounter'
}
});
}
SendVisitorData.send($scope.myform, getActiveField(), TimeCounter.getTimeElapsed());
};
$rootScope.nextField = $scope.nextField = function(){
@ -171,22 +183,30 @@ angular.module('forms').directive('submitFormDirective', ['$http', 'TimeCounter'
};
$rootScope.submitForm = $scope.submitForm = function() {
var _timeElapsed = TimeCounter.stopClock();
$scope.loading = true;
var form = _.cloneDeep($scope.myform);
form.timeElapsed = _timeElapsed;
form.percentageComplete = $filter('formValidity')($scope.myform) / $scope.myform.visible_form_fields.length * 100;
delete form.visible_form_fields;
for(var i=0; i < $scope.myform.form_fields.length; i++){
if($scope.myform.form_fields[i].fieldType === 'dropdown' && !$scope.myform.form_fields[i].deletePreserved){
$scope.myform.form_fields[i].fieldValue = $scope.myform.form_fields[i].fieldValue.option_value;
}
}
setTimeout(function () {
$scope.submitPromise = $http.post('/forms/' + $scope.myform._id, form)
.success(function (data, status, headers) {
console.log($scope.myform.form_fields[0]);
$scope.myform.submitted = true;
$scope.loading = false;
SendVisitorData.send($scope.myform, getActiveField(), _timeElapsed);
})
.error(function (error) {
$scope.loading = false;

View file

@ -2,22 +2,29 @@
angular.module('forms').service('TimeCounter', [
function(){
var _startTime, _endTime, that=this;
var _startTime, _endTime = null, that=this;
this.timeSpent = 0;
this.restartClock = function(){
_startTime = Date.now();
_endTime = _startTime;
_endTime = null;
// console.log('Clock Started');
};
this.getTimeElapsed = function(){
if(_startTime) {
return Math.abs(Date.now().valueOf() - _startTime.valueOf()) / 1000;
}
};
this.stopClock = function(){
if(_startTime){
if(_startTime && _endTime === null){
_endTime = Date.now();
that.timeSpent = Math.abs(_endTime.valueOf() - _startTime.valueOf())/1000;
// console.log('Clock Ended');
return that.timeSpent;
this.timeSpent = Math.abs(_endTime.valueOf() - _startTime.valueOf())/1000;
this._startTime = this._endTime = null;
return this.timeSpent;
}else{
return new Error('Clock has not been started');
}
@ -28,4 +35,4 @@ angular.module('forms').service('TimeCounter', [
};
}
]);
]);

View file

@ -271,6 +271,22 @@
<!-- <span class="required-error" ng-show="field.required && !field.fieldValue">* required</span> -->
</div>
</div>
<div class="row field">
<div class="field-title col-sm-4">
<h5>Google Analytics Tracking Code</h5>
</div>
<div class="col-sm-8">
<input type="text"
ng-model="myform.analytics.gaCode"
value="{{myform.analytics.gaCode}}"
style="width: 100%;"
ng-minlength="4"
placeholder="UA-XXXXX-Y"
ng-pattern="/\bUA-\d{4,10}-\d{1,4}\b/">
</div>
</div>
<div class="row field">
<div class="col-xs-6 field-title">Language</div>

View file

@ -1,5 +1,47 @@
<div class="submissions-table row container" ng-init="initFormSubmissions()">
<div class="row">
<div class="col-xs-4">
Total Views: {{myform.analytics.views}}
</div>
<div class="col-xs-4">
Submissions: {{myform.analytics.submissions}}
</div>
<div class="col-xs-4">
Conversion Rate: {{myform.analytics.conversionRate}}%
</div>
</div>
<br>
<div class="row">
<div class="col-xs-12">
<div class="col-xs-2">
<strong>Field Title</strong>
</div>
<div class="col-xs-2">
<strong>Field Views</strong>
</div>
<div class="col-xs-4">
<strong>User dropoff rate at this field</strong>
</div>
</div>
<div class="col-xs-12" ng-repeat="fieldStats in myform.analytics.fields">
<div class="col-xs-2">
{{fieldStats.field.title}}
</div>
<div class="col-xs-2">
{{fieldStats.totalViews}}
</div>
<div class="col-xs-4">
{{fieldStats.dropoffRate}}%
</div>
</div>
</div>
<br>
<div class="row">
<div class="col-xs-2">
<button class="btn btn-danger" ng-click="deleteSelectedSubmissions()" ng-disabled="!isAtLeastOneChecked();">

View file

@ -2,7 +2,7 @@
<!-- Start Page View -->
<div ng-show="!myform.submitted && myform.startPage.showStart"
class="form-submitted"
class="form-submitted"
style="padding-top: 35vh;">
<div class="row">
<div class="col-xs-12 text-center" style="overflow-wrap: break-word;">
@ -53,6 +53,7 @@ ng-style="{'color':button.color}">
data-id="{{field._id}}"
ng-class="{activeField: selected._id == field._id }"
class="row field-directive">
<field-directive field="field" design="myform.design" index="$index" forms="forms">
</field-directive>
</div>

View file

@ -1,4 +1,17 @@
<section class="public-form" ng-style="{ 'background-color': myform.design.colors.backgroundColor }">
<submit-form-directive myform="myform"></submit-form-directive>
</section>
<!-- User's Google Analytics -->
<script ng-if="myform.analytics.gaCode">
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '{{myform.analytics.gaCode}}', 'auto');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->