Skip to content

Commit 7b0b5b5

Browse files
committed
Moved new endpoint into PostsStatsService and updated endpoint
1 parent bb1cc72 commit 7b0b5b5

File tree

6 files changed

+119
-100
lines changed

6 files changed

+119
-100
lines changed

ghost/core/core/server/api/endpoints/stats.js

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -89,30 +89,6 @@ const controller = {
8989
return await statsService.api.getPostReferrers(frame.data.id);
9090
}
9191
},
92-
postReferrersAlpha: {
93-
headers: {
94-
cacheInvalidate: false
95-
},
96-
data: [
97-
'id'
98-
],
99-
permissions: {
100-
docName: 'posts',
101-
method: 'browse'
102-
},
103-
cache: statsService.cache,
104-
generateCacheKeyData(frame) {
105-
return {
106-
method: 'postReferrersAlpha',
107-
data: {
108-
id: frame.data.id
109-
}
110-
};
111-
},
112-
async query(frame) {
113-
return await statsService.api.getPostReferrersAlpha(frame.data.id);
114-
}
115-
},
11692
referrersHistory: {
11793
headers: {
11894
cacheInvalidate: false
@@ -191,7 +167,31 @@ const controller = {
191167
async query(frame) {
192168
return await statsService.api.getTopPosts(frame.options);
193169
}
194-
}
170+
},
171+
postReferrersAlpha: {
172+
headers: {
173+
cacheInvalidate: false
174+
},
175+
data: [
176+
'id'
177+
],
178+
permissions: {
179+
docName: 'posts',
180+
method: 'browse'
181+
},
182+
cache: statsService.cache,
183+
generateCacheKeyData(frame) {
184+
return {
185+
method: 'postReferrersAlpha',
186+
data: {
187+
id: frame.data.id
188+
}
189+
};
190+
},
191+
async query(frame) {
192+
return await statsService.api.getPostReferrersAlpha(frame.data.id, frame.options);
193+
}
194+
},
195195
};
196196

197197
module.exports = controller;

ghost/core/core/server/services/stats/PostsStatsService.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,82 @@ class PostsStatsService {
8686
}
8787
}
8888

89+
/**
90+
* Get referrers for a post
91+
* @param {string} postId
92+
* @param {TopPostsOptions} options
93+
* @returns {Promise<{data: TopPostResult[]}>} The referrers for the post
94+
*/
95+
async getForPostAlpha(postId, options) {
96+
const knex = this.knex;
97+
const freeMembers = await knex('members_created_events as mce')
98+
.select('mce.referrer_source as source')
99+
.countDistinct('mce.member_id as free_members')
100+
.leftJoin('members_subscription_created_events as msce', function () {
101+
this.on('mce.member_id', '=', 'msce.member_id')
102+
.andOn('mce.attribution_id', '=', 'msce.attribution_id')
103+
.andOnVal('msce.attribution_type', '=', 'post');
104+
})
105+
.where('mce.attribution_id', postId)
106+
.where('mce.attribution_type', 'post')
107+
.whereNull('msce.id')
108+
.groupBy('mce.referrer_source');
109+
110+
const paidMembers = await knex('members_subscription_created_events as msce')
111+
.select('msce.referrer_source as source')
112+
.countDistinct('msce.member_id as paid_members')
113+
.where('msce.attribution_id', postId)
114+
.where('msce.attribution_type', 'post')
115+
.groupBy('msce.referrer_source');
116+
117+
const mrr = await knex('members_subscription_created_events as msce')
118+
.select('msce.referrer_source as source')
119+
.sum('mpse.mrr_delta as mrr')
120+
.join('members_paid_subscription_events as mpse', function () {
121+
this.on('mpse.subscription_id', '=', 'msce.subscription_id')
122+
.andOn('mpse.member_id', '=', 'msce.member_id');
123+
})
124+
.where('msce.attribution_id', postId)
125+
.where('msce.attribution_type', 'post')
126+
.groupBy('msce.referrer_source');
127+
128+
const map = new Map();
129+
for (const row of freeMembers) {
130+
map.set(row.source, {
131+
source: row.source,
132+
free_members: row.free_members,
133+
paid_members: 0,
134+
mrr: 0
135+
});
136+
}
137+
138+
for (const row of paidMembers) {
139+
const existing = map.get(row.source) ?? {
140+
source: row.source,
141+
free_members: 0,
142+
paid_members: 0,
143+
mrr: 0
144+
};
145+
existing.paid_members = row.paid_members;
146+
map.set(row.source, existing);
147+
}
148+
149+
for (const row of mrr) {
150+
const existing = map.get(row.source) ?? {
151+
source: row.source,
152+
free_members: 0,
153+
paid_members: 0,
154+
mrr: 0
155+
};
156+
existing.mrr = row.mrr;
157+
map.set(row.source, existing);
158+
}
159+
160+
const results = [...map.values()].sort((a, b) => b.mrr - a.mrr);
161+
162+
return {data: results};
163+
}
164+
89165
/**
90166
* Build a subquery/CTE for free_members count
91167
* (Signed up on Post, Paid Elsewhere/Never)

ghost/core/core/server/services/stats/ReferrersStatsService.js

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -54,76 +54,6 @@ class ReferrersStatsService {
5454
return [...map.values()].sort((a, b) => b.paid_conversions - a.paid_conversions);
5555
}
5656

57-
async getForPostAlpha(postId) {
58-
const knex = this.knex;
59-
const freeMembers = await knex('members_created_events as mce')
60-
.select('mce.referrer_source as source')
61-
.countDistinct('mce.member_id as free_members')
62-
.leftJoin('members_subscription_created_events as msce', function () {
63-
this.on('mce.member_id', '=', 'msce.member_id')
64-
.andOn('mce.attribution_id', '=', 'msce.attribution_id')
65-
.andOnVal('msce.attribution_type', '=', 'post');
66-
})
67-
.where('mce.attribution_id', postId)
68-
.where('mce.attribution_type', 'post')
69-
.whereNull('msce.id')
70-
.groupBy('mce.referrer_source');
71-
72-
const paidMembers = await knex('members_subscription_created_events as msce')
73-
.select('msce.referrer_source as source')
74-
.countDistinct('msce.member_id as paid_members')
75-
.where('msce.attribution_id', postId)
76-
.where('msce.attribution_type', 'post')
77-
.groupBy('msce.referrer_source');
78-
79-
const mrr = await knex('members_subscription_created_events as msce')
80-
.select('msce.referrer_source as source')
81-
.sum('mpse.mrr_delta as mrr')
82-
.join('members_paid_subscription_events as mpse', function () {
83-
this.on('mpse.subscription_id', '=', 'msce.subscription_id')
84-
.andOn('mpse.member_id', '=', 'msce.member_id');
85-
})
86-
.where('msce.attribution_id', postId)
87-
.where('msce.attribution_type', 'post')
88-
.groupBy('msce.referrer_source');
89-
90-
const map = new Map();
91-
for (const row of freeMembers) {
92-
map.set(row.source, {
93-
source: row.source,
94-
free_members: row.free_members,
95-
paid_members: 0,
96-
mrr: 0
97-
});
98-
}
99-
100-
for (const row of paidMembers) {
101-
const existing = map.get(row.source) ?? {
102-
source: row.source,
103-
free_members: 0,
104-
paid_members: 0,
105-
mrr: 0
106-
};
107-
existing.paid_members = row.paid_members;
108-
map.set(row.source, existing);
109-
}
110-
111-
for (const row of mrr) {
112-
const existing = map.get(row.source) ?? {
113-
source: row.source,
114-
free_members: 0,
115-
paid_members: 0,
116-
mrr: 0
117-
};
118-
existing.mrr = row.mrr;
119-
map.set(row.source, existing);
120-
}
121-
122-
const result = [...map.values()].sort((a, b) => b.mrr - a.mrr);
123-
124-
return result;
125-
}
126-
12757
/**
12858
* Return a list of all the attribution sources, with their signup and conversion counts on each date
12959
* @returns {Promise<{data: AttributionCountStat[], meta: {}}>}

ghost/core/core/server/services/stats/StatsService.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ class StatsService {
6666
/**
6767
* @param {string} postId
6868
*/
69-
async getPostReferrersAlpha(postId) {
69+
async getPostReferrersAlpha(postId, options) {
7070
return {
71-
data: await this.referrers.getForPostAlpha(postId),
71+
data: await this.posts.getForPostAlpha(postId, options),
7272
meta: {}
7373
};
7474
}

ghost/core/core/server/web/api/endpoints/admin/routes.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const api = require('../../../../api').endpoints;
33
const {http} = require('@tryghost/api-framework');
44
const apiMw = require('../../middleware');
55
const mw = require('./middleware');
6+
const labs = require('../../../../../shared/labs');
67

78
const shared = require('../../../shared');
89

@@ -153,10 +154,12 @@ module.exports = function apiRoutes() {
153154
router.get('/stats/mrr', mw.authAdminApi, http(api.stats.mrr));
154155
router.get('/stats/subscriptions', mw.authAdminApi, http(api.stats.subscriptions));
155156
router.get('/stats/referrers/posts/:id', mw.authAdminApi, http(api.stats.postReferrers));
156-
router.get('/stats/referrers/posts/:id/alpha', mw.authAdminApi, http(api.stats.postReferrersAlpha));
157157
router.get('/stats/referrers', mw.authAdminApi, http(api.stats.referrersHistory));
158-
router.get('/stats/top-content', mw.authAdminApi, http(api.stats.topContent));
159-
router.get('/stats/top-posts', mw.authAdminApi, http(api.stats.topPosts));
158+
if (labs.isSet('trafficAnalytics')) {
159+
router.get('/stats/top-posts', mw.authAdminApi, http(api.stats.topPosts));
160+
router.get('/stats/top-content', mw.authAdminApi, http(api.stats.topContent));
161+
router.get('/stats/referrers/posts/:id/alpha', mw.authAdminApi, http(api.stats.postReferrersAlpha));
162+
}
160163

161164
// ## Labels
162165
router.get('/labels', mw.authAdminApi, http(api.labels.browse));

ghost/core/test/unit/server/services/stats/posts.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ describe('PostsStatsService', function () {
122122
table.string('attribution_id').index();
123123
table.string('attribution_type');
124124
table.dateTime('created_at');
125+
table.string('referrer_source');
125126
});
126127

127128
await db.schema.createTable('members_subscription_created_events', function (table) {
@@ -131,6 +132,7 @@ describe('PostsStatsService', function () {
131132
table.string('attribution_id').index();
132133
table.string('attribution_type');
133134
table.dateTime('created_at');
135+
table.string('referrer_source');
134136
});
135137

136138
await db.schema.createTable('members_paid_subscription_events', function (table) {
@@ -320,4 +322,12 @@ describe('PostsStatsService', function () {
320322
assert.equal(result.data[1].post_id, 'post2');
321323
});
322324
});
325+
326+
describe('getForPostAlpha', function () {
327+
it('returns empty array when no events exist', async function () {
328+
const result = await service.getForPostAlpha('post1');
329+
assert.ok(result.data, 'Result should have a data property');
330+
assert.equal(result.data.length, 0, 'Should return empty array when no events exist');
331+
});
332+
});
323333
});

0 commit comments

Comments
 (0)