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}} +
+
+
+
+
+