b04452fdb3
ref: https://linear.app/tryghost/issue/SLO-184/add-90-days-filter-to-top-sources-admin-dashboard-query
165 lines
5.4 KiB
JavaScript
165 lines
5.4 KiB
JavaScript
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
|
|
**/
|