0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Added /stats/subscriptions API (#14547)

refs https://github.com/TryGhost/Team/issues/1505
refs https://github.com/TryGhost/Team/issues/1466

Exposes an API for historical counts broken down by tier and cadence.

Counts backwards from the current stats like MRR to minimize inaccruate
data due to missing/superfluous events.
This commit is contained in:
Fabien 'egg' O'Carroll 2022-04-27 14:53:32 +01:00 committed by GitHub
parent 756f86dbdc
commit d94859f2e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 151 additions and 8 deletions

View file

@ -19,5 +19,14 @@ module.exports = {
async query() {
return await statsService.getMRRHistory();
}
},
subscriptions: {
permissions: {
docName: 'members',
method: 'browse'
},
async query() {
return await statsService.getSubscriptionCountHistory();
}
}
};

View file

@ -140,6 +140,7 @@ module.exports = function apiRoutes() {
// ## Stats
router.get('/stats/member_count', mw.authAdminApi, http(api.stats.memberCountHistory));
router.get('/stats/mrr', mw.authAdminApi, http(api.stats.mrr));
router.get('/stats/subscriptions', mw.authAdminApi, http(api.stats.subscriptions));
// ## Labels
router.get('/labels', mw.authAdminApi, http(api.labels.browse));

View file

@ -105,7 +105,7 @@
"@tryghost/session-service": "0.1.39",
"@tryghost/settings-path-manager": "0.1.5",
"@tryghost/social-urls": "0.1.29",
"@tryghost/stats-service": "0.1.0",
"@tryghost/stats-service": "0.2.1",
"@tryghost/string": "0.1.23",
"@tryghost/tpl": "0.1.16",
"@tryghost/update-check-service": "0.3.2",

View file

@ -11,6 +11,11 @@ Object {
],
},
"stats": Array [
Object {
"currency": "usd",
"date": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"mrr": 0,
},
Object {
"currency": "usd",
"date": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
@ -24,7 +29,7 @@ exports[`Stats API Can fetch MRR history 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "111",
"content-length": "158",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -65,3 +70,54 @@ Object {
"x-powered-by": "Express",
}
`;
exports[`Stats API Can fetch subscriptions history 1: [body] 1`] = `
Object {
"meta": Object {
"cadences": Array [
"year",
"month",
],
"tiers": Array [
StringMatching /\\[a-f0-9\\]\\{24\\}/,
],
"totals": Array [
Object {
"cadence": "month",
"count": 1,
"tier": StringMatching /\\[a-f0-9\\]\\{24\\}/,
},
],
},
"stats": Array [
Object {
"cadence": "month",
"count": 1,
"date": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"negative_delta": 0,
"positive_delta": 1,
"tier": StringMatching /\\[a-f0-9\\]\\{24\\}/,
},
Object {
"cadence": "year",
"count": 0,
"date": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"negative_delta": 0,
"positive_delta": 0,
"tier": StringMatching /\\[a-f0-9\\]\\{24\\}/,
},
],
}
`;
exports[`Stats API Can fetch subscriptions history 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "403",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;

View file

@ -1,5 +1,5 @@
const {agentProvider, fixtureManager, matchers} = require('../../utils/e2e-framework');
const {anyEtag, anyISODate} = matchers;
const {anyEtag, anyISODate, anyObjectId} = matchers;
let agent;
@ -31,10 +31,36 @@ describe('Stats API', function () {
.matchBodySnapshot({
stats: [{
date: anyISODate
}, {
date: anyISODate
}]
})
.matchHeaderSnapshot({
etag: anyEtag
});
});
it('Can fetch subscriptions history', async function () {
await agent
.get(`/stats/subscriptions`)
.expectStatus(200)
.matchBodySnapshot({
stats: [{
date: anyISODate,
tier: anyObjectId
}, {
date: anyISODate,
tier: anyObjectId
}],
meta: {
tiers: [anyObjectId],
totals: [{
tier: anyObjectId
}]
}
})
.matchHeaderSnapshot({
etag: anyEtag
});
});
});

View file

@ -562,6 +562,10 @@ const fixtures = {
})}, {id: member.id});
}
}
}).then(async function () {
for (const event of DataGenerator.forKnex.members_paid_subscription_events) {
await models.MemberPaidSubscriptionEvent.add(event);
}
});
},

View file

@ -492,6 +492,42 @@ DataGenerator.Content = {
}
],
members_paid_subscription_events: [
{
id: ObjectId().toHexString(),
type: 'created',
mrr_delta: 1000,
currency: 'usd',
source: 'stripe',
created_at: null,
subscription_id: null,
member_id: null,
from_plan: null,
to_plan: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730bb8'
}, {
id: ObjectId().toHexString(),
type: 'created',
mrr_delta: 0,
currency: 'usd',
source: 'stripe',
created_at: null,
subscription_id: null,
member_id: null,
from_plan: null,
to_plan: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730bb9'
}, {
id: ObjectId().toHexString(),
type: 'created',
mrr_delta: 0,
currency: 'usd',
source: 'stripe',
created_at: null,
subscription_id: null,
member_id: null,
from_plan: null,
to_plan: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730ba0'
}
],
members_stripe_customers_subscriptions: [
{
id: ObjectId().toHexString(),
@ -780,6 +816,9 @@ DataGenerator.Content.members_stripe_customers[1].member_id = DataGenerator.Cont
DataGenerator.Content.members_stripe_customers[2].member_id = DataGenerator.Content.members[4].id;
DataGenerator.Content.members_stripe_customers[3].member_id = DataGenerator.Content.members[6].id;
DataGenerator.Content.members_stripe_customers[4].member_id = DataGenerator.Content.members[7].id;
DataGenerator.Content.members_paid_subscription_events[0].member_id = DataGenerator.Content.members[2].id;
DataGenerator.Content.members_paid_subscription_events[1].member_id = DataGenerator.Content.members[3].id;
DataGenerator.Content.members_paid_subscription_events[2].member_id = DataGenerator.Content.members[4].id;
DataGenerator.forKnex = (function () {
function createBasic(overrides) {
@ -1473,6 +1512,12 @@ DataGenerator.forKnex = (function () {
createBasic(DataGenerator.Content.members_stripe_customers_subscriptions[2])
];
const members_paid_subscription_events = [
createBasic(DataGenerator.Content.members_paid_subscription_events[0]),
createBasic(DataGenerator.Content.members_paid_subscription_events[1]),
createBasic(DataGenerator.Content.members_paid_subscription_events[2])
];
const snippets = [
createBasic(DataGenerator.Content.snippets[0])
];
@ -1537,7 +1582,9 @@ DataGenerator.forKnex = (function () {
stripe_prices,
stripe_products,
snippets,
custom_theme_settings
custom_theme_settings,
members_paid_subscription_events
};
}());

View file

@ -2447,10 +2447,10 @@
resolved "https://registry.yarnpkg.com/@tryghost/social-urls/-/social-urls-0.1.29.tgz#09177858959e9521244d0192bf219237f0fb6094"
integrity sha512-PUgSQj/Y8x4Sbp0/Lq/Pq6ZN8ICSoAIHpVouJwblW3LvY/C0eeDrDLQMrUmR2SSBx59mS+Blavwy5mFlt23lwg==
"@tryghost/stats-service@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@tryghost/stats-service/-/stats-service-0.1.0.tgz#5638ac07b6de6de40ea4f7a9963e832530eef2e2"
integrity sha512-YJRdomtJ8y3f0UDvYjSnK4/RMu5xvZoMvtFAZfxiDC1d0yKA4YostxFQHAKh32t2y0FNO6IJzO7yJ5HnUXjueg==
"@tryghost/stats-service@0.2.1":
version "0.2.1"
resolved "https://registry.yarnpkg.com/@tryghost/stats-service/-/stats-service-0.2.1.tgz#d9b0488d705b9a697dc9ebff9ec8e64b33902649"
integrity sha512-2w2C8f9BR7QjcyvgPLQmzNW+cDHT6eceCMa8Ihp3PoQtcikE8TKAOx1cwlEDxs+0Blf5BQ8HILA2bJYsfsylAg==
dependencies:
moment "^2.29.3"