Merge branch 'clicks-by-device-type' of https://github.com/larrabee/mailtrain into larrabee-clicks-by-device-type
This commit is contained in:
commit
1e9c9f3275
10 changed files with 203 additions and 15 deletions
|
@ -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) => {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"schemaVersion": 22
|
"schemaVersion": 23
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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());
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
37
setup/sql/upgrade-00023.sql
Normal file
37
setup/sql/upgrade-00023.sql
Normal 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;
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue