Merge branch 'clicks-by-device-type' of https://github.com/larrabee/mailtrain into larrabee-clicks-by-device-type

This commit is contained in:
Andris Reinman 2017-03-21 10:29:00 +02:00
commit 1e9c9f3275
10 changed files with 203 additions and 15 deletions

View file

@ -13,6 +13,7 @@ let log = require('npmlog');
let mailer = require('../mailer'); let mailer = require('../mailer');
let humanize = require('humanize'); let humanize = require('humanize');
let _ = require('../translate')._; let _ = require('../translate')._;
let util = require('util');
let allowedKeys = ['description', 'from', 'address', 'reply_to', 'subject', 'editor_name', 'editor_data', 'template', 'source_url', 'list', 'segment', 'html', 'text', 'tracking_disabled']; let allowedKeys = ['description', 'from', 'address', 'reply_to', 'subject', 'editor_name', 'editor_data', 'template', 'source_url', 'list', 'segment', 'html', 'text', 'tracking_disabled'];
@ -193,7 +194,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 query_template = 'SELECT %s AS data, COUNT(*) AS cnt FROM `subscription__%d` JOIN `campaign_tracker__%d` ON `campaign_tracker__%d`.`list`=%d AND `campaign_tracker__%d`.`subscriber`=`subscription__%d`.`id` AND `campaign_tracker__%d`.`link`=%d GROUP BY `%s` ORDER BY COUNT(`%s`) DESC, `%s`';
let query = util.format(query_template, column, campaign.list, campaign.id, campaign.id, campaign.list, campaign.id, campaign.list, campaign.id, linkId, column, column, column);
connection.query(query, (err, rows) => {
connection.release();
if (err) {
return callback(err);
}
let data = {};
let dataPercent = [];
let total = 0;
rows.forEach((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(key => {
let name = key + ': ' + data[key];
let value = parseInt(data[key] * 100 / total, 10);
dataPercent.push([name, value]);
});
return callback(null, dataPercent, total);
});
});
}; };
module.exports.filterStatusSubscribers = (campaign, status, request, columns, callback) => { module.exports.filterStatusSubscribers = (campaign, status, request, columns, callback) => {

View file

@ -13,6 +13,7 @@ let lists = require('./lists');
let log = require('npmlog'); let log = require('npmlog');
let urllib = require('url'); let urllib = require('url');
let he = require('he'); let he = require('he');
let ua_parser = require('device');
module.exports.resolve = (linkCid, callback) => { module.exports.resolve = (linkCid, callback) => {
db.getConnection((err, connection) => { 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) => { getSubscriptionData(campaignCid, listCid, subscriptionCid, (err, data) => {
if (err) { if (err) {
return callback(err); return callback(err);
@ -57,9 +58,9 @@ module.exports.countClick = (remoteIp, campaignCid, listCid, subscriptionCid, li
} }
let country = geoip.lookupCountry(remoteIp) || null; let country = geoip.lookupCountry(remoteIp) || null;
let device = ua_parser(useragent, { unknownUserAgentDeviceType: 'desktop', emptyUserAgentDeviceType: 'desktop' });
let query = 'INSERT INTO `campaign_tracker__' + data.campaign.id + '` (`list`, `subscriber`, `link`, `ip`, `country`) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE `count`=`count`+1'; 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, country], (err, result) => { connection.query(query, [data.list.id, data.subscription.id, linkId, remoteIp, device.type, country], (err, result) => {
if (err && err.code !== 'ER_DUP_ENTRY') { if (err && err.code !== 'ER_DUP_ENTRY') {
return connection.rollback(() => { return connection.rollback(() => {
connection.release(); 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 (?,?,?,?,?)'; 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, country], err => { connection.query(query, [data.list.id, data.subscription.id, 0, remoteIp, device.type, country], err => {
if (err && err.code !== 'ER_DUP_ENTRY') { if (err && err.code !== 'ER_DUP_ENTRY') {
return connection.rollback(() => { return connection.rollback(() => {
connection.release(); 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 // 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) => { getSubscriptionData(campaignCid, listCid, subscriptionCid, (err, data) => {
if (err) { if (err) {
return callback(err); return callback(err);
@ -173,9 +174,9 @@ module.exports.countOpen = (remoteIp, campaignCid, listCid, subscriptionCid, cal
} }
let country = geoip.lookupCountry(remoteIp) || null; let country = geoip.lookupCountry(remoteIp) || null;
let device = ua_parser(useragent, { unknownUserAgentDeviceType: 'desktop', emptyUserAgentDeviceType: 'desktop' });
let query = 'INSERT INTO `campaign_tracker__' + data.campaign.id + '` (`list`, `subscriber`, `link`, `ip`, `country`) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE `count`=`count`+1'; 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, country], (err, result) => { connection.query(query, [data.list.id, data.subscription.id, -1, remoteIp, device.type, country], (err, result) => {
if (err && err.code !== 'ER_DUP_ENTRY') { if (err && err.code !== 'ER_DUP_ENTRY') {
return connection.rollback(() => { return connection.rollback(() => {
connection.release(); connection.release();

View file

@ -1,3 +1,3 @@
{ {
"schemaVersion": 22 "schemaVersion": 23
} }

View file

@ -47,6 +47,7 @@
"csurf": "^1.9.0", "csurf": "^1.9.0",
"csv-generate": "^1.0.0", "csv-generate": "^1.0.0",
"csv-parse": "^1.2.0", "csv-parse": "^1.2.0",
"device": "^0.3.8",
"dompurify": "^0.8.5", "dompurify": "^0.8.5",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"express": "^4.15.2", "express": "^4.15.2",

View file

@ -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 () { $('.datestring').each(function () {
$(this).html(moment($(this).data('date')).fromNow()); $(this).html(moment($(this).data('date')).fromNow());
}); });

View file

@ -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) => { router.post('/status/ajax/:id/:status', (req, res) => {
let status = Number(req.params.status) || 0; let status = Number(req.params.status) || 0;

View file

@ -18,8 +18,7 @@ router.get('/:campaign/:list/:subscription', (req, res) => {
'Content-Type': 'image/gif', 'Content-Type': 'image/gif',
'Content-Length': trackImg.length 'Content-Length': trackImg.length
}); });
links.countOpen(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, (err, opened) => {
links.countOpen(req.ip, req.params.campaign, req.params.list, req.params.subscription, (err, opened) => {
if (err) { if (err) {
log.error('Redirect', err.stack || 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); log.error('Redirect', 'Unresolved URL: <%s>', req.url);
return notFound(); 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) { if (err) {
log.error('Redirect', err.stack || err); log.error('Redirect', err.stack || err);
} }

View file

@ -0,0 +1,37 @@
# Header section
# Define incrementing schema version number
SET @schema_version = '23';
# Add field device_type to campaign_tracker
# Create ALTER TABLE PROCEDURE
DROP PROCEDURE IF EXISTS `alterbyregexp`;
CREATE PROCEDURE `alterbyregexp` (`table_regexp` VARCHAR(255), `altertext` VARCHAR(255))
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE tbl VARCHAR(255);
DECLARE curs CURSOR FOR SELECT table_name FROM information_schema.tables WHERE table_schema = (SELECT DATABASE()) and table_name like table_regexp;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN curs;
read_loop: LOOP
FETCH curs INTO tbl;
IF done THEN
LEAVE read_loop;
END IF;
SET @query = CONCAT('ALTER TABLE `', tbl, '`' , altertext);
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END LOOP;
CLOSE curs;
END;
# Add field device_type to campaign_tracker
CALL alterbyregexp('campaign\_tracker%', 'ADD COLUMN `device_type` varchar(50) DEFAULT NULL AFTER `ip`');
DROP PROCEDURE IF EXISTS `alterbyregexp`;
# 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;

View file

@ -69,6 +69,31 @@
<!-- Default panel contents --> <!-- Default panel contents -->
<div class="panel-heading">{{#if aggregated}}{{#translate}}Subscribers who clicked on a link:{{/translate}}{{else}}{{#translate}}Subscribers who clicked on this link:{{/translate}}{{/if}}</div> <div class="panel-heading">{{#if aggregated}}{{#translate}}Subscribers who clicked on a link:{{/translate}}{{else}}{{#translate}}Subscribers who clicked on this link:{{/translate}}{{/if}}</div>
<div class="panel-body"> <div class="panel-body">
{{#unless aggregated}}
<div class="table-responsive">
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<table class="table table-bordered table-hover data-piechart-ajax display nowrap">
<thead>
<tr>
<th>
{{#translate}}Stats by country{{/translate}}
</th>
<th>
{{#translate}}Stats by device type{{/translate}}
</th>
</tr>
<tr>
<th>
<div class="data-stats-pie-chart" data-topic-url="/campaigns/clicked" data-column="country" data-topic-id="{{id}}/{{link.id}}"></div>
</th>
<th>
<div class="data-stats-pie-chart" data-topic-url="/campaigns/clicked" data-column="device_type" data-topic-id="{{id}}/{{link.id}}"></div>
</th>
</tr>
</thead>
</table>
</div>
{{/unless}}
<div class="table-responsive"> <div class="table-responsive">
<table data-topic-url="/campaigns/clicked" data-topic-id="{{id}}/{{link.id}}" data-sort-column="1" data-sort-order="asc" class="table table-bordered table-hover data-table-ajax display nowrap" width="100%" data-row-sort="0,1,1,1,1,1,0"> <table data-topic-url="/campaigns/clicked" data-topic-id="{{id}}/{{link.id}}" data-sort-column="1" data-sort-order="asc" class="table table-bordered table-hover data-table-ajax display nowrap" width="100%" data-row-sort="0,1,1,1,1,1,0">
<thead> <thead>

View file

@ -16,12 +16,36 @@
<div class="well well-sm">{{{description}}}</div> <div class="well well-sm">{{{description}}}</div>
{{/if}} {{/if}}
<div class="table-responsive"> <div class="table-responsive">
<div class="panel panel-info"> <div class="panel panel-info">
<!-- Default panel contents --> <!-- Default panel contents -->
<div class="panel-heading">{{#translate}}Subscribers who opened this message:{{/translate}}</div> <div class="panel-heading">{{#translate}}Subscribers who opened this message:{{/translate}}</div>
<div class="panel-body"> <div class="panel-body">
<div class="table-responsive">
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<table class="table table-bordered table-hover data-piechart-ajax display nowrap">
<thead>
<tr>
<th>
{{#translate}}Stats by country{{/translate}}
</th>
<th>
{{#translate}}Stats by device type{{/translate}}
</th>
</tr>
<tr>
<th>
<div class="data-stats-pie-chart" data-topic-url="/campaigns/clicked" data-column="country" data-topic-id="{{id}}/-1"></div>
</th>
<th>
<div class="data-stats-pie-chart" data-topic-url="/campaigns/clicked" data-column="device_type" data-topic-id="{{id}}/-1"></div>
</th>
</tr>
</thead>
</table>
</div>
<div class="table-responsive"> <div class="table-responsive">
<table data-topic-url="/campaigns/clicked" data-topic-id="{{id}}/-1" data-sort-column="1" data-sort-order="asc" class="table table-bordered table-hover data-table-ajax display nowrap" width="100%" data-row-sort="0,1,1,1,1,1,0"> <table data-topic-url="/campaigns/clicked" data-topic-id="{{id}}/-1" data-sort-column="1" data-sort-order="asc" class="table table-bordered table-hover data-table-ajax display nowrap" width="100%" data-row-sort="0,1,1,1,1,1,0">
<thead> <thead>