diff --git a/lib/models/campaigns.js b/lib/models/campaigns.js
index b71d1bbf..2afe367c 100644
--- a/lib/models/campaigns.js
+++ b/lib/models/campaigns.js
@@ -193,7 +193,43 @@ module.exports.filterClickedSubscribers = (campaign, linkId, request, columns, c
});
});
});
+};
+module.exports.statsClickedSubscribersByColumn = (campaign, linkId, request, column, limit, callback) => {
+ db.getConnection((err, connection) => {
+ if (err) {
+ return callback(err);
+ }
+
+ let values = [campaign.list, linkId];
+ let query = 'SELECT SQL_CALC_FOUND_ROWS ' + column + ' AS data, COUNT(*) AS cnt FROM `subscription__' + campaign.list + '` JOIN `campaign_tracker__' + campaign.id + '` ON `campaign_tracker__' + campaign.id + '`.`list`=? AND `campaign_tracker__' + campaign.id + '`.`subscriber`=`subscription__' + campaign.list + '`.`id` AND `campaign_tracker__' + campaign.id + '`.`link`=? GROUP BY ' + column + ' ORDER BY COUNT(' + column + ') DESC,' + column;
+
+ connection.query(query, values, (err, rows) => {
+ connection.release();
+ if (err) {
+ return callback(err);
+ }
+
+ let data = {};
+ let dataPercent = [];
+ let total = 0;
+
+ rows.forEach(function (row, index) {
+ if (index < limit) {
+ data[row.data] = row.cnt;
+ } else {
+ data.other = (data.other ? data.other : 0) + row.cnt;
+ }
+ total += row.cnt;
+ });
+ Object.keys(data).forEach(function (key) {
+ let name = key + ': ' + data[key];
+ let value = parseInt(data[key] * 100 / total);
+ dataPercent.push([name, value]);
+ });
+ return callback(null, dataPercent, total);
+ });
+ });
};
module.exports.filterStatusSubscribers = (campaign, status, request, columns, callback) => {
diff --git a/lib/models/links.js b/lib/models/links.js
index 6c5a0bae..3a254788 100644
--- a/lib/models/links.js
+++ b/lib/models/links.js
@@ -13,6 +13,7 @@ let lists = require('./lists');
let log = require('npmlog');
let urllib = require('url');
let he = require('he');
+let ua_parser = require('device');
module.exports.resolve = (linkCid, callback) => {
db.getConnection((err, connection) => {
@@ -35,7 +36,7 @@ module.exports.resolve = (linkCid, callback) => {
});
};
-module.exports.countClick = (remoteIp, campaignCid, listCid, subscriptionCid, linkId, callback) => {
+module.exports.countClick = (remoteIp, useragent, campaignCid, listCid, subscriptionCid, linkId, callback) => {
getSubscriptionData(campaignCid, listCid, subscriptionCid, (err, data) => {
if (err) {
return callback(err);
@@ -57,9 +58,9 @@ module.exports.countClick = (remoteIp, campaignCid, listCid, subscriptionCid, li
}
let country = geoip.lookupCountry(remoteIp) || null;
-
- let query = 'INSERT INTO `campaign_tracker__' + data.campaign.id + '` (`list`, `subscriber`, `link`, `ip`, `country`) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE `count`=`count`+1';
- connection.query(query, [data.list.id, data.subscription.id, linkId, remoteIp, country], (err, result) => {
+ let device = ua_parser(useragent, { unknownUserAgentDeviceType: 'desktop', emptyUserAgentDeviceType: 'desktop' });
+ let query = 'INSERT INTO `campaign_tracker__' + data.campaign.id + '` (`list`, `subscriber`, `link`, `ip`, `device_type`, `country`) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE `count`=`count`+1';
+ connection.query(query, [data.list.id, data.subscription.id, linkId, remoteIp, device.type, country], (err, result) => {
if (err && err.code !== 'ER_DUP_ENTRY') {
return connection.rollback(() => {
connection.release();
@@ -98,8 +99,8 @@ module.exports.countClick = (remoteIp, campaignCid, listCid, subscriptionCid, li
});
}
- let query = 'INSERT INTO `campaign_tracker__' + data.campaign.id + '` (`list`, `subscriber`, `link`, `ip`, `country`) VALUES (?,?,?,?,?)';
- connection.query(query, [data.list.id, data.subscription.id, 0, remoteIp, country], err => {
+ let query = 'INSERT INTO `campaign_tracker__' + data.campaign.id + '` (`list`, `subscriber`, `link`, `ip`, `device_type`, `country`) VALUES (?,?,?,?,?,?)';
+ connection.query(query, [data.list.id, data.subscription.id, 0, remoteIp, device.type, country], err => {
if (err && err.code !== 'ER_DUP_ENTRY') {
return connection.rollback(() => {
connection.release();
@@ -141,7 +142,7 @@ module.exports.countClick = (remoteIp, campaignCid, listCid, subscriptionCid, li
});
// also count clicks as open events in case beacon image was blocked
- module.exports.countOpen(remoteIp, campaignCid, listCid, subscriptionCid, () => false);
+ module.exports.countOpen(remoteIp, useragent, campaignCid, listCid, subscriptionCid, () => false);
});
});
});
@@ -151,7 +152,7 @@ module.exports.countClick = (remoteIp, campaignCid, listCid, subscriptionCid, li
});
};
-module.exports.countOpen = (remoteIp, campaignCid, listCid, subscriptionCid, callback) => {
+module.exports.countOpen = (remoteIp, useragent, campaignCid, listCid, subscriptionCid, callback) => {
getSubscriptionData(campaignCid, listCid, subscriptionCid, (err, data) => {
if (err) {
return callback(err);
@@ -173,9 +174,9 @@ module.exports.countOpen = (remoteIp, campaignCid, listCid, subscriptionCid, cal
}
let country = geoip.lookupCountry(remoteIp) || null;
-
- let query = 'INSERT INTO `campaign_tracker__' + data.campaign.id + '` (`list`, `subscriber`, `link`, `ip`, `country`) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE `count`=`count`+1';
- connection.query(query, [data.list.id, data.subscription.id, -1, remoteIp, country], (err, result) => {
+ let device = ua_parser(useragent, { unknownUserAgentDeviceType: 'desktop', emptyUserAgentDeviceType: 'desktop' });
+ let query = 'INSERT INTO `campaign_tracker__' + data.campaign.id + '` (`list`, `subscriber`, `link`, `ip`, `device_type`, `country`) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE `count`=`count`+1';
+ connection.query(query, [data.list.id, data.subscription.id, -1, remoteIp, device.type, country], (err, result) => {
if (err && err.code !== 'ER_DUP_ENTRY') {
return connection.rollback(() => {
connection.release();
diff --git a/meta.json b/meta.json
index 731dc8bf..7bb9c8c4 100644
--- a/meta.json
+++ b/meta.json
@@ -1,3 +1,3 @@
{
- "schemaVersion": 21
+ "schemaVersion": 22
}
diff --git a/package.json b/package.json
index dab0933f..04a898a6 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"csurf": "^1.9.0",
"csv-generate": "^1.0.0",
"csv-parse": "^1.2.0",
+ "device": "^0.3.8",
"escape-html": "^1.0.3",
"express": "^4.15.2",
"express-session": "^1.15.1",
diff --git a/public/javascript/tables.js b/public/javascript/tables.js
index be592106..7faad36b 100644
--- a/public/javascript/tables.js
+++ b/public/javascript/tables.js
@@ -72,6 +72,31 @@ $('.data-table-ajax').each(function () {
});
});
+$('.data-stats-pie-chart').each(function () {
+ var column = $(this).data('column') || 'country';
+ var limit = $(this).data('limit') || 20;
+ var topicId = $(this).data('topicId');
+ var topicUrl = $(this).data('topicUrl') || '/campaigns/clicked';
+ var ajaxUrl = topicUrl + '/ajax/' + topicId + '/stats';
+ var self = $(this);
+
+ $.post(ajaxUrl, {column: column, limit: limit}, function(data) {
+ google.charts.load('current', {'packages':['corechart']});
+ google.charts.setOnLoadCallback(drawChart);
+
+ function drawChart() {
+ var gTable = new google.visualization.DataTable();
+ gTable.addColumn('string', 'Column');
+ gTable.addColumn('number', 'Value');
+ gTable.addRows(data.data);
+
+ var options = {'width':500, 'height':400};
+ var chart = new google.visualization.PieChart(self[0]);
+ chart.draw(gTable, options);
+ }
+ });
+});
+
$('.datestring').each(function () {
$(this).html(moment($(this).data('date')).fromNow());
});
diff --git a/routes/campaigns.js b/routes/campaigns.js
index 4ee1958a..81dcd2ba 100644
--- a/routes/campaigns.js
+++ b/routes/campaigns.js
@@ -581,6 +581,45 @@ router.post('/clicked/ajax/:id/:linkId', (req, res) => {
});
});
+router.post('/clicked/ajax/:id/:linkId/stats', (req, res) => {
+ let linkId = Number(req.params.linkId) || 0;
+ campaigns.get(req.params.id, true, (err, campaign) => {
+ if (err || !campaign) {
+ return res.json({
+ error: err && err.message || err || _('Campaign not found'),
+ data: []
+ });
+ }
+ lists.get(campaign.list, (err, list) => {
+ if (err) {
+ return res.json({
+ error: err && err.message || err,
+ data: []
+ });
+ }
+
+ let column = req.body.column;
+ let limit = req.body.limit;
+
+ campaigns.statsClickedSubscribersByColumn(campaign, linkId, req.body, column, limit, (err, data, total) => {
+ if (err) {
+ return res.json({
+ error: err.message || err,
+ data: []
+ });
+ }
+
+ res.json({
+ draw: req.body.draw,
+ total: total,
+ data: data
+ });
+ });
+ });
+ });
+});
+
+
router.post('/status/ajax/:id/:status', (req, res) => {
let status = Number(req.params.status) || 0;
diff --git a/routes/links.js b/routes/links.js
index 5da3ef43..f65bc57b 100644
--- a/routes/links.js
+++ b/routes/links.js
@@ -18,8 +18,7 @@ router.get('/:campaign/:list/:subscription', (req, res) => {
'Content-Type': 'image/gif',
'Content-Length': trackImg.length
});
-
- links.countOpen(req.ip, req.params.campaign, req.params.list, req.params.subscription, (err, opened) => {
+ links.countOpen(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, (err, opened) => {
if (err) {
log.error('Redirect', err.stack || err);
}
@@ -53,7 +52,7 @@ router.get('/:campaign/:list/:subscription/:link', (req, res) => {
log.error('Redirect', 'Unresolved URL: <%s>', req.url);
return notFound();
}
- links.countClick(req.ip, req.params.campaign, req.params.list, req.params.subscription, linkId, (err, status) => {
+ links.countClick(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, linkId, (err, status) => {
if (err) {
log.error('Redirect', err.stack || err);
}
diff --git a/setup/sql/upgrade-00022.sql b/setup/sql/upgrade-00022.sql
new file mode 100644
index 00000000..3867edd4
--- /dev/null
+++ b/setup/sql/upgrade-00022.sql
@@ -0,0 +1,11 @@
+# Header section
+# Define incrementing schema version number
+SET @schema_version = '22';
+
+# Add field device_type to campaign_tracker
+ALTER TABLE `campaign_tracker` ADD COLUMN `device_type` varchar(50) DEFAULT NULL AFTER `ip`;
+
+# Footer section
+LOCK TABLES `settings` WRITE;
+INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
+UNLOCK TABLES;
diff --git a/views/campaigns/clicked.hbs b/views/campaigns/clicked.hbs
index 9f71d470..d93f2ac4 100644
--- a/views/campaigns/clicked.hbs
+++ b/views/campaigns/clicked.hbs
@@ -69,6 +69,31 @@
{{#if aggregated}}{{#translate}}Subscribers who clicked on a link:{{/translate}}{{else}}{{#translate}}Subscribers who clicked on this link:{{/translate}}{{/if}}
+ {{#if aggregated}}{{else}}
+
+
+
+
+
+
+ {{#translate}}Stats by country{{/translate}}
+ |
+
+ {{#translate}}Stats by device type{{/translate}}
+ |
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+ {{/if}}
diff --git a/views/campaigns/opened.hbs b/views/campaigns/opened.hbs
index 20665d53..b4b3b823 100644
--- a/views/campaigns/opened.hbs
+++ b/views/campaigns/opened.hbs
@@ -16,12 +16,36 @@
{{{description}}}
{{/if}}
+
{{#translate}}Subscribers who opened this message:{{/translate}}
+
+
+
+
+
+
+ {{#translate}}Stats by country{{/translate}}
+ |
+
+ {{#translate}}Stats by device type{{/translate}}
+ |
+
+
+
+
+ |
+
+
+ |
+
+
+
+