ghost/stats-service/lib/ReferrersStatsService.js
const moment = require('moment');
class ReferrersStatsService {
/**
* @param {object} deps
* @param {import('knex').Knex} deps.knex
**/
constructor({knex}) {
this.knex = knex;
}
/**
* Return a list of all the attribution sources for a given post, with their signup and conversion counts
* @param {string} postId
* @returns {Promise<AttributionCountStat[]>}
*/
async getForPost(postId) {
const knex = this.knex;
const signupRows = await knex('members_created_events')
.select('referrer_source')
.select(knex.raw('COUNT(id) AS total'))
.where('attribution_id', postId)
.where('attribution_type', 'post')
.groupBy('referrer_source');
const conversionRows = await knex('members_subscription_created_events')
.select('referrer_source')
.select(knex.raw('COUNT(id) AS total'))
.where('attribution_id', postId)
.where('attribution_type', 'post')
.groupBy('referrer_source');
// Stitch them toghether, grouping them by source
const map = new Map();
for (const row of signupRows) {
map.set(row.referrer_source, {
source: row.referrer_source,
signups: row.total,
paid_conversions: 0
});
}
for (const row of conversionRows) {
const existing = map.get(row.referrer_source) ?? {
source: row.referrer_source,
signups: 0,
paid_conversions: 0
};
existing.paid_conversions = row.total;
map.set(row.referrer_source, existing);
}
return [...map.values()].sort((a, b) => b.paid_conversions - a.paid_conversions);
}
/**
* Return a list of all the attribution sources, with their signup and conversion counts on each date
* @returns {Promise<{data: AttributionCountStat[], meta: {}}>}
*/
async getReferrersHistory() {
const paidConversionEntries = await this.fetchAllPaidConversionSources();
const signupEntries = await this.fetchAllSignupSources();
const allEntries = signupEntries.map((entry) => {
return {
...entry,
paid_conversions: 0,
date: moment(entry.date).format('YYYY-MM-DD')
};
});
paidConversionEntries.forEach((entry) => {
const entryDate = moment(entry.date).format('YYYY-MM-DD');
const existingEntry = allEntries.find(e => e.source === entry.source && e.date === entryDate);
if (existingEntry) {
existingEntry.paid_conversions = entry.paid_conversions;
} else {
allEntries.push({
...entry,
signups: 0,
date: entryDate
});
}
});
// sort allEntries in date ascending format
allEntries.sort((a, b) => {
return moment(a.date).diff(moment(b.date));
});
return {
data: allEntries,
meta: {}
};
}
/**
* @returns {Promise<PaidConversionsCountStatDate[]>}
**/
async fetchAllPaidConversionSources() {
const knex = this.knex;
const ninetyDaysAgo = moment.utc().subtract(90, 'days').startOf('day').utc().format('YYYY-MM-DD HH:mm:ss');
const rows = await knex('members_subscription_created_events')
.select(knex.raw(`DATE(created_at) as date`))
.select(knex.raw(`COUNT(*) as paid_conversions`))
.select(knex.raw(`referrer_source as source`))
.where('created_at', '>=', ninetyDaysAgo)
.groupBy('date', 'referrer_source')
.orderBy('date');
return rows;
}
/**
* @returns {Promise<SignupCountStatDate[]>}
**/
async fetchAllSignupSources() {
const knex = this.knex;
const ninetyDaysAgo = moment.utc().subtract(90, 'days').startOf('day').utc().format('YYYY-MM-DD HH:mm:ss');
const rows = await knex('members_created_events')
.select(knex.raw(`DATE(created_at) as date`))
.select(knex.raw(`COUNT(*) as signups`))
.select(knex.raw(`referrer_source as source`))
.where('created_at', '>=', ninetyDaysAgo)
.groupBy('date', 'referrer_source')
.orderBy('date');
return rows;
}
}
module.exports = ReferrersStatsService;
/**
* @typedef AttributionCountStat
* @type {Object}
* @property {string} source Attribution Source
* @property {number} signups Total free members signed up for this source
* @property {number} paid_conversions Total paid conversions for this source
*/
/**
* @typedef AttributionCountStatDate
* @type {AttributionCountStat}
* @property {string} date The date (YYYY-MM-DD) on which these counts were recorded
*/
/**
* @typedef {object} SignupCountStatDate
* @type {Object}
* @property {string} source Attribution Source
* @property {number} signups Total free members signed up for this source
* @property {string} date The date (YYYY-MM-DD) on which these counts were recorded
**/
/**
* @typedef {object} PaidConversionsCountStatDate
* @type {Object}
* @property {string} source Attribution Source
* @property {number} paid_conversions Total paid conversions for this source
* @property {string} date The date (YYYY-MM-DD) on which these counts were recorded
**/