diff --git a/client/src/campaigns/Status.js b/client/src/campaigns/Status.js
index 494e825d..d42216a4 100644
--- a/client/src/campaigns/Status.js
+++ b/client/src/campaigns/Status.js
@@ -615,7 +615,7 @@ export default class Status extends Component {
{t('campaignStatus')}
{entity.name}
- {entity.delivered}
+ {entity.delivered}
{this.campaignStatusLabels[entity.status]}
{sendSettings}
diff --git a/mvis/client/webpack.config.js b/mvis/client/webpack.config.js
index 277f484c..f57e287d 100644
--- a/mvis/client/webpack.config.js
+++ b/mvis/client/webpack.config.js
@@ -1,12 +1,11 @@
-const webpack = require('webpack');
const path = require('path');
const webpackConf = require('../ivis-core/client/webpack.config');
webpackConf.resolve.modules = ['node_modules', '../ivis-core/client/node_modules'];
webpackConf.entry = {
- 'index-trusted': ['babel-polyfill', './src/root-trusted.js'],
- 'index-sandbox': ['babel-polyfill', '../ivis-core/client/src/root-sandbox.js']
+ 'index-trusted': ['@babel/polyfill', './src/root-trusted.js'],
+ 'index-sandbox': ['@babel/polyfill', '../ivis-core/client/src/root-sandbox.js']
};
webpackConf.output = {
filename: '[name].js',
diff --git a/mvis/ivis-core b/mvis/ivis-core
index 052fbff6..5ea4783f 160000
--- a/mvis/ivis-core
+++ b/mvis/ivis-core
@@ -1 +1 @@
-Subproject commit 052fbff6e4eaba52eee2f984f2b8d947ebfa7298
+Subproject commit 5ea4783f3ec5140ad68637c31e230a410e493170
diff --git a/mvis/server/config/default.yaml b/mvis/server/config/default.yaml
index d835ac30..3edcd45e 100644
--- a/mvis/server/config/default.yaml
+++ b/mvis/server/config/default.yaml
@@ -6,8 +6,7 @@ mysql:
mailtrain:
url: http://localhost:3000/
- namespaces:
- campaigns: 2
+ namespace: 1
userRole: mailtrainUser
www:
diff --git a/mvis/server/extensions-common.js b/mvis/server/extensions-common.js
index c917b517..cee6806d 100644
--- a/mvis/server/extensions-common.js
+++ b/mvis/server/extensions-common.js
@@ -5,6 +5,7 @@ const path = require('path');
em.set('config.extraDirs', [ path.join(__dirname, 'config') ]);
em.set('builder.exec', path.join(__dirname, 'builder.js'));
+em.set('task-handler.exec', path.join(__dirname, 'task-handler.js'));
em.set('indexer.elasticsearch.exec', path.join(__dirname, 'indexer-elasticsearch.js'));
em.set('app.title', 'Mailtrain IVIS');
diff --git a/mvis/server/index.js b/mvis/server/index.js
index bd9eb16d..c1a8d2eb 100644
--- a/mvis/server/index.js
+++ b/mvis/server/index.js
@@ -15,6 +15,9 @@ em.on('knex.migrate', async () => {
em.on('app.installAPIRoutes', app => {
const embedApi = require('./routes/api/embed');
app.use('/api', embedApi);
+
+ const eventsApi = require('./routes/api/events');
+ app.use('/api', eventsApi);
});
require('../ivis-core/server/index');
diff --git a/mvis/server/routes/api/events.js b/mvis/server/routes/api/events.js
new file mode 100644
index 00000000..0b9dce24
--- /dev/null
+++ b/mvis/server/routes/api/events.js
@@ -0,0 +1,123 @@
+'use strict';
+
+const config = require('../../../ivis-core/server/lib/config');
+const moment = require('moment');
+const knex = require('../../../ivis-core/server/lib/knex');
+const router = require('../../../ivis-core/server/lib/router-async').create();
+const log = require('../../../ivis-core/server/lib/log');
+const signalSets = require('../../../ivis-core/server/models/signal-sets');
+const { SignalType } = require('../../../ivis-core/shared/signals');
+const contextHelpers = require('../../../ivis-core/server/lib/context-helpers');
+const namespaces = require('../../../ivis-core/server/models/namespaces');
+
+/*
+async function ensureCampaignTracker() {
+ const schema = {
+ type: {
+ type: SignalType.INTEGER,
+ name: 'Type',
+ settings: {},
+ indexed: true,
+ weight_list: 0,
+ weight_edit: 0
+ },
+ timestamp: {
+ type: SignalType.DATE_TIME,
+ name: 'Timestamp',
+ settings: {},
+ indexed: true,
+ weight_list: 1,
+ weight_edit: 1
+ },
+ campaignId: {
+ type: SignalType.INTEGER,
+ name: 'Campaign ID',
+ settings: {},
+ indexed: true,
+ weight_list: 2,
+ weight_edit: 2
+ },
+ listId: {
+ type: SignalType.INTEGER,
+ name: 'List ID',
+ settings: {},
+ indexed: true,
+ weight_list: 3,
+ weight_edit: 3
+ },
+ subscriptionId: {
+ type: SignalType.INTEGER,
+ name: 'Subscription ID',
+ settings: {},
+ indexed: true,
+ weight_list: 4,
+ weight_edit: 4
+ },
+
+ };
+
+ return await signalSets.ensure(
+ req.context,
+ 'campaignTracker',
+ schema,
+ 'Campaign Tracker',
+ '',
+ config.mailtrain.namespace
+ );
+}
+
+async function ingestCampaignTrackerRecord(record) {
+ return {
+ id: TODO
+ };
+}
+
+const types = {
+ campaign_tracker: {
+ ensure: ensureCampaignTracker,
+ ingest: ingestCampaignTrackerRecord
+ }
+}
+
+router.postAsync('/events', async (req, res) => {
+ const batch = req.body;
+
+ const recordsByType = {};
+ const signalSetWithSignalMapByType = {};
+
+ for (const type in types) {
+ recordsByType[type] = [];
+ signalSetWithSignalMapByType[type] = await types[type].ensure();
+ }
+
+ for (const dataEntry of batch.data) {
+ const record = {
+ id: dataEntry[idField],
+ signals: {}
+ };
+
+ for (const fieldId in dataEntry) {
+ if (fieldId !== idField) {
+ if (!(fieldId in schema)) {
+ throw new Error(`Unknown data field "${fieldId}`);
+ }
+
+ let value = dataEntry[fieldId];
+ if (schema[fieldId].type === SignalType.DATE_TIME) {
+ value = moment(value);
+ }
+
+ record.signals[fieldId] = value;
+ }
+ }
+
+ records.push(record);
+ }
+
+ await signalSets.insertRecords(req.context, signalSetWithSignalMap, records);
+
+ return res.json();
+});
+*/
+
+module.exports = router;
diff --git a/mvis/server/task-handler.js b/mvis/server/task-handler.js
new file mode 100644
index 00000000..5b040ed5
--- /dev/null
+++ b/mvis/server/task-handler.js
@@ -0,0 +1,5 @@
+'use strict';
+
+require('./extensions-common');
+require('../ivis-core/server/services/task-handler');
+
diff --git a/server/lib/activity-log.js b/server/lib/activity-log.js
index de637599..6e532253 100644
--- a/server/lib/activity-log.js
+++ b/server/lib/activity-log.js
@@ -1,7 +1,37 @@
'use strict';
+const moment = require('moment');
+
+const activityQueueLenthThreshold = 100;
+const actitivyQueue = [];
+
+let processQueueIsRunning = false;
+
+async function processQueue() {
+ if (processQueueIsRunning) {
+ return;
+ }
+
+ processQueueIsRunning = true;
+
+ // XXX submit data to IVIS if configured in config
+
+ actitivyQueue.splice(0);
+
+ processQueueIsRunning = false;
+}
+
async function _logActivity(typeId, data) {
- // TODO
+ actitivyQueue.push({
+ typeId,
+ data,
+ timestamp: moment.utc().toISOString()
+ });
+
+ if (actitivyQueue.length >= activityQueueLenthThreshold) {
+ // noinspection ES6MissingAwait
+ processQueue();
+ }
}
/*
diff --git a/server/services/sender-worker.js b/server/services/sender-worker.js
index f0667ee6..60a1c6c6 100644
--- a/server/services/sender-worker.js
+++ b/server/services/sender-worker.js
@@ -4,8 +4,12 @@ const config = require('../lib/config');
const log = require('../lib/log');
const mailers = require('../lib/mailers');
const messageSender = require('../lib/message-sender');
+const {CampaignTrackerActivityType} = require('../../shared/activity-log');
+const activityLog = require('../lib/activity-log');
require('../lib/fork');
+const MessageType = messageSender.MessageType;
+
const workerId = Number.parseInt(process.argv[2]);
let running = false;
@@ -26,6 +30,8 @@ async function processCampaignMessages(campaignId, messages) {
try {
await cs.sendRegularCampaignMessage(campaignMessage);
+ await activityLog.logCampaignTrackerActivity(CampaignTrackerActivityType.SENT, campaignId, campaignMessage.list, campaignMessage.subscription);
+
log.verbose('Senders', 'Message sent and status updated for %s:%s', campaignMessage.list, campaignMessage.subscription);
} catch (err) {
@@ -58,6 +64,8 @@ async function processQueuedMessages(sendConfigurationId, messages) {
for (const queuedMessage of messages) {
+ const messageType = queuedMessage.type;
+
const msgData = queuedMessage.data;
let target = '';
if (msgData.listId && msgData.subscriptionId) {
@@ -74,6 +82,11 @@ async function processQueuedMessages(sendConfigurationId, messages) {
try {
await messageSender.sendQueuedMessage(queuedMessage);
+
+ if ((messageType === MessageType.TRIGGERED || messageType === MessageType.TEST) && msgData.campaignId && msgData.listId && msgData.subscriptionId) {
+ await activityLog.logCampaignTrackerActivity(CampaignTrackerActivityType.SENT, msgData.campaignId, msgData.listId, msgData.subscriptionId);
+ }
+
log.verbose('Senders', `Message sent and status updated for ${target}`);
} catch (err) {
if (err instanceof mailers.SendConfigurationError) {
diff --git a/shared/activity-log.js b/shared/activity-log.js
index 55ea6c63..4e82387e 100644
--- a/shared/activity-log.js
+++ b/shared/activity-log.js
@@ -30,10 +30,12 @@ const ListActivityType = {
};
const CampaignTrackerActivityType = {
- DELIVERED: 1,
+ SENT: 1,
BOUNCED: 2,
- OPENED: 3,
- CLICKED: 4
+ UNSUBSCRIBED: 3,
+ COMPLAINED: 4,
+ OPENED: 5,
+ CLICKED: 6
};
const BlacklistActivityType = {
@@ -45,4 +47,5 @@ const BlacklistActivityType = {
module.exports.EntityActivityType = EntityActivityType;
module.exports.BlacklistActivityType = BlacklistActivityType;
module.exports.CampaignActivityType = CampaignActivityType;
-module.exports.ListActivityType = ListActivityType;
\ No newline at end of file
+module.exports.ListActivityType = ListActivityType;
+module.exports.CampaignTrackerActivityType = CampaignTrackerActivityType;
\ No newline at end of file