Panels with campaign statistics and some fixes in computation of clicks.

This commit is contained in:
Tomas Bures 2018-12-16 13:47:08 +01:00
parent ba996d845d
commit d103a2cc79
18 changed files with 811 additions and 96 deletions

View file

@ -10,13 +10,14 @@ const shares = require('./shares');
const namespaceHelpers = require('../lib/namespace-helpers');
const files = require('./files');
const templates = require('./templates');
const { CampaignStatus, CampaignSource, CampaignType, getSendConfigurationPermissionRequiredForSend} = require('../../shared/campaigns');
const { CampaignStatus, CampaignSource, CampaignType, getSendConfigurationPermissionRequiredForSend } = require('../../shared/campaigns');
const sendConfigurations = require('./send-configurations');
const triggers = require('./triggers');
const {SubscriptionStatus} = require('../../shared/lists');
const subscriptions = require('./subscriptions');
const segments = require('./segments');
const senders = require('../lib/senders');
const {LinkId} = require('./links');
const allowedKeysCommon = ['name', 'description', 'segment', 'namespace',
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
@ -158,7 +159,8 @@ async function listTestUsersDTAjax(context, campaignId, params) {
subsQry = knex.raw(subsUnionSql, subsUnionBindings);
}
return await dtHelpers.ajaxListWithPermissions(
return await dtHelpers.ajaxListWithPermissionsTx(
tx,
context,
[{ entityTypeId: 'list', requiredOperations: ['viewSubscriptions'], column: 'subs.list_id' }],
params,
@ -191,6 +193,115 @@ async function listTestUsersDTAjax(context, campaignId, params) {
});
}
async function _listSubscriberResultsDTAjax(context, campaignId, getSubsQrys, columns, params) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'view');
const subsQrys = [];
const cpgLists = await tx('campaign_lists').where('campaign', campaignId);
for (const cpgList of cpgLists) {
const subsTable = subscriptions.getSubscriptionTableName(cpgList.list);
subsQrys.push(getSubsQrys(subsTable, cpgList));
}
if (subsQrys.length > 0) {
let subsSql, subsBindings;
if (subsQrys.length === 1) {
subsSql = '(' + subsQrys[0].sql + ') as `subs`'
subsBindings = subsQrys[0].bindings;
} else {
subsSql = '(' +
subsQrys.map(qry => '(' + qry.sql + ')').join(' UNION ALL ') +
') as `subs`';
subsBindings = Array.prototype.concat(...subsQrys.map(qry => qry.bindings));
}
return await dtHelpers.ajaxListWithPermissionsTx(
tx,
context,
[{ entityTypeId: 'list', requiredOperations: ['viewSubscriptions'], column: 'lists.id' }],
params,
(builder, tx) => builder.from(knex.raw(subsSql, subsBindings))
.innerJoin('lists', 'subs.list', 'lists.id')
.innerJoin('namespaces', 'lists.namespace', 'namespaces.id')
,
columns
);
} else {
const result = {
draw: params.draw,
recordsTotal: 0,
recordsFiltered: 0,
data: []
};
return result;
}
});
}
async function listSentByStatusDTAjax(context, campaignId, status, params) {
return await _listSubscriberResultsDTAjax(
context,
campaignId,
(subsTable, cpgList) => knex.from(subsTable)
.innerJoin(
function () {
return this.from('campaign_messages')
.where('campaign_messages.campaign', campaignId)
.where('campaign_messages.list', cpgList.list)
.where('campaign_messages.status', status)
.as('related_campaign_messages');
},
'related_campaign_messages.subscription', subsTable + '.id')
.select([subsTable + '.email', subsTable + '.cid', knex.raw('? AS list', [cpgList.list])])
.toSQL().toNative(),
[ 'subs.email', 'subs.cid', 'lists.cid', 'lists.name', 'namespaces.name' ],
params
);
}
async function listOpensDTAjax(context, campaignId, params) {
return await _listSubscriberResultsDTAjax(
context,
campaignId,
(subsTable, cpgList) => knex.from(subsTable)
.innerJoin(
function () {
return this.from('campaign_links')
.where('campaign_links.campaign', campaignId)
.where('campaign_links.list', cpgList.list)
.where('campaign_links.link', LinkId.OPEN)
.as('related_campaign_links');
},
'related_campaign_links.subscription', subsTable + '.id')
.select([subsTable + '.email', subsTable + '.cid', knex.raw('? AS list', [cpgList.list]), 'related_campaign_links.count'])
.toSQL().toNative(),
[ 'subs.email', 'subs.cid', 'lists.cid', 'lists.name', 'namespaces.name', 'subs.count' ],
params
);
}
async function listLinkClicksDTAjax(context, campaignId, params) {
return await knex.transaction(async (tx) => {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'viewStats');
return await dtHelpers.ajaxListTx(
tx,
params,
builder => builder.from('links')
.where('links.campaign', campaignId),
[ 'links.url', 'links.visits', 'links.hits' ]
);
});
}
async function getTrackingSettingsByCidTx(tx, cid) {
const entity = await tx('campaigns').where('campaigns.cid', cid)
.select([
@ -511,6 +622,8 @@ async function _removeTx(tx, context, id, existing = null) {
await tx('campaign_messages').where('campaign', id).del();
await tx('campaign_links').where('campaign', id).del();
await tx('links').where('campaign', id).del();
await triggers.removeAllByCampaignIdTx(tx, context, id);
await tx('template_dep_campaigns')
@ -779,11 +892,19 @@ async function reset(context, campaignId) {
}
await tx('campaigns').where('id', campaignId).update({
status: CampaignStatus.IDLE
status: CampaignStatus.IDLE,
delivered: 0,
unsubscribed: 0,
bounced: 0,
complained: 0,
blacklisted: 0,
opened: 0,
clicks: 0
});
await tx('campaign_messages').where('campaign', campaignId).del();
await tx('campaign_links').where('campaign', campaignId).del();
await tx('links').where('campaign', campaignId).del();
});
}
@ -796,14 +917,51 @@ async function disable(context, campaignId) {
}
async function getStatisticsOverview(context, id) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'viewStats');
const stats = await tx('campaigns').where('id', id).select(['delivered', 'unsubscribed', 'bounced', 'complained', 'blacklisted', 'opened', 'clicks']).first();
const totalQryGen = await getSubscribersQueryGeneratorTx(tx, id, false);
if (totalQryGen) {
const res = await totalQryGen(tx).count('* AS subscriptionsTotal').first();
stats.total = res.subscriptionsTotal;
} else {
stats.total = 0;
}
return stats;
});
}
async function getStatisticsOpened(context, id) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'viewStats');
const devices = await tx('campaign_links').where('campaign', id).groupBy('device_type').select('device_type AS key').count('* as count');
const countries = await tx('campaign_links').where('campaign', id).groupBy('country').select('country AS key').count('* as count');
return {
devices,
countries
};
});
}
module.exports.Content = Content;
module.exports.hash = hash;
module.exports.listDTAjax = listDTAjax;
module.exports.listChildrenDTAjax = listChildrenDTAjax;
module.exports.listWithContentDTAjax = listWithContentDTAjax;
module.exports.listOthersWhoseListsAreIncludedDTAjax = listOthersWhoseListsAreIncludedDTAjax;
module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
module.exports.listSentByStatusDTAjax = listSentByStatusDTAjax;
module.exports.listOpensDTAjax = listOpensDTAjax;
module.exports.listLinkClicksDTAjax = listLinkClicksDTAjax;
module.exports.getByIdTx = getByIdTx;
module.exports.getById = getById;
module.exports.create = create;
@ -829,4 +987,6 @@ module.exports.enable = enable;
module.exports.disable = disable;
module.exports.rawGetByTx = rawGetByTx;
module.exports.getTrackingSettingsByCidTx = getTrackingSettingsByCidTx;
module.exports.getTrackingSettingsByCidTx = getTrackingSettingsByCidTx;
module.exports.getStatisticsOverview = getStatisticsOverview;
module.exports.getStatisticsOpened = getStatisticsOpened;

View file

@ -14,6 +14,7 @@ const he = require('he');
const { enforce } = require('../lib/helpers');
const { getPublicUrl } = require('../lib/urls');
const tools = require('../lib/tools');
const shortid = require('shortid');
const LinkId = {
OPEN: -1,
@ -41,7 +42,7 @@ async function countLink(remoteIp, userAgent, campaignCid, listCid, subscription
campaign: campaign.id,
list: list.id,
subscription: subscription.id,
link: linkId,
link: clickLinkId,
ip: remoteIp,
device_type: device.type,
country
@ -49,7 +50,7 @@ async function countLink(remoteIp, userAgent, campaignCid, listCid, subscription
const campaignLinksQryResult = await tx.raw(campaignLinksQry.sql + (incrementOnDup ? ' ON DUPLICATE KEY UPDATE `count`=`count`+1' : ''), campaignLinksQry.bindings);
if (campaignLinksQryResult.affectedRows > 1) { // When using DUPLICATE KEY UPDATE, this means that the entry was already there
if (campaignLinksQryResult[0].affectedRows > 1) { // When using DUPLICATE KEY UPDATE, this means that the entry was already there
return false;
}
@ -84,7 +85,10 @@ async function countLink(remoteIp, userAgent, campaignCid, listCid, subscription
// Update clicks
if (linkId > LinkId.GENERAL_CLICK && !campaign.click_tracking_disabled) {
await tx('links').increment('hits').where('id', linkId);
if (await _countLink(linkId, true)) {
await tx('links').increment('visits').where('id', linkId);
if (await _countLink(LinkId.GENERAL_CLICK, false)) {
await tx('campaigns').increment('clicks').where('id', campaign.id);
}
@ -103,7 +107,7 @@ async function countLink(remoteIp, userAgent, campaignCid, listCid, subscription
async function addOrGet(campaignId, url) {
return await knex.transaction(async tx => {
const link = tx('links').select(['id', 'cid']).where({
const link = await tx('links').select(['id', 'cid']).where({
campaign: campaignId,
url
}).first();
@ -111,7 +115,7 @@ async function addOrGet(campaignId, url) {
if (!link) {
let cid = shortid.generate();
const ids = tx('links').insert({
const ids = await tx('links').insert({
campaign: campaignId,
cid,
url

View file

@ -5,7 +5,7 @@ const { filterObject } = require('../lib/helpers');
const hasher = require('node-object-hash')();
const shares = require('./shares');
const allowedKeys = new Set(['adminEmail', 'uaCode', 'shoutout', 'pgpPassphrase', 'pgpPrivateKey', 'defaultHomepage']);
const allowedKeys = new Set(['adminEmail', 'uaCode', 'mapsApiKey', 'shoutout', 'pgpPassphrase', 'pgpPrivateKey', 'defaultHomepage']);
// defaultHomepage is used as a default to list.homepage - if the list.homepage is not filled in
function hash(entity) {