ghost 5.110.4 → 5.112.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tryghost-adapter-cache-memory-ttl-5.112.0.tgz +0 -0
- package/components/tryghost-adapter-cache-redis-5.112.0.tgz +0 -0
- package/components/{tryghost-adapter-manager-5.110.4.tgz → tryghost-adapter-manager-5.112.0.tgz} +0 -0
- package/components/tryghost-announcement-bar-settings-5.112.0.tgz +0 -0
- package/components/{tryghost-api-framework-5.110.4.tgz → tryghost-api-framework-5.112.0.tgz} +0 -0
- package/components/tryghost-api-version-compatibility-service-5.112.0.tgz +0 -0
- package/components/{tryghost-audience-feedback-5.110.4.tgz → tryghost-audience-feedback-5.112.0.tgz} +0 -0
- package/components/tryghost-bookshelf-repository-5.112.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.112.0.tgz +0 -0
- package/components/tryghost-captcha-service-5.112.0.tgz +0 -0
- package/components/tryghost-constants-5.112.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.112.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.110.4.tgz → tryghost-custom-theme-settings-service-5.112.0.tgz} +0 -0
- package/components/{tryghost-data-generator-5.110.4.tgz → tryghost-data-generator-5.112.0.tgz} +0 -0
- package/components/{tryghost-domain-events-5.110.4.tgz → tryghost-domain-events-5.112.0.tgz} +0 -0
- package/components/tryghost-donations-5.112.0.tgz +0 -0
- package/components/tryghost-email-addresses-5.112.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.112.0.tgz +0 -0
- package/components/{tryghost-email-analytics-service-5.110.4.tgz → tryghost-email-analytics-service-5.112.0.tgz} +0 -0
- package/components/tryghost-email-content-generator-5.112.0.tgz +0 -0
- package/components/tryghost-email-events-5.112.0.tgz +0 -0
- package/components/tryghost-email-service-5.112.0.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.112.0.tgz +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.110.4.tgz → tryghost-express-dynamic-redirects-5.112.0.tgz} +0 -0
- package/components/{tryghost-external-media-inliner-5.110.4.tgz → tryghost-external-media-inliner-5.112.0.tgz} +0 -0
- package/components/tryghost-extract-api-key-5.112.0.tgz +0 -0
- package/components/tryghost-ghost-5.112.0.tgz +0 -0
- package/components/{tryghost-html-to-plaintext-5.110.4.tgz → tryghost-html-to-plaintext-5.112.0.tgz} +0 -0
- package/components/tryghost-i18n-5.112.0.tgz +0 -0
- package/components/tryghost-identity-token-service-5.112.0.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.112.0.tgz +0 -0
- package/components/{tryghost-importer-revue-5.110.4.tgz → tryghost-importer-revue-5.112.0.tgz} +0 -0
- package/components/tryghost-in-memory-repository-5.112.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.110.4.tgz → tryghost-job-manager-5.112.0.tgz} +0 -0
- package/components/{tryghost-link-redirects-5.110.4.tgz → tryghost-link-redirects-5.112.0.tgz} +0 -0
- package/components/tryghost-link-replacer-5.112.0.tgz +0 -0
- package/components/{tryghost-magic-link-5.110.4.tgz → tryghost-magic-link-5.112.0.tgz} +0 -0
- package/components/tryghost-mail-events-5.112.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.112.0.tgz +0 -0
- package/components/{tryghost-member-attribution-5.110.4.tgz → tryghost-member-attribution-5.112.0.tgz} +0 -0
- package/components/{tryghost-member-events-5.110.4.tgz → tryghost-member-events-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-api-5.110.4.tgz → tryghost-members-api-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-csv-5.110.4.tgz → tryghost-members-csv-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-importer-5.110.4.tgz → tryghost-members-importer-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-offers-5.110.4.tgz → tryghost-members-offers-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-payments-5.110.4.tgz → tryghost-members-payments-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-ssr-5.110.4.tgz → tryghost-members-ssr-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-stripe-service-5.110.4.tgz → tryghost-members-stripe-service-5.112.0.tgz} +0 -0
- package/components/{tryghost-milestones-5.110.4.tgz → tryghost-milestones-5.112.0.tgz} +0 -0
- package/components/tryghost-minifier-5.112.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.112.0.tgz +0 -0
- package/components/{tryghost-mw-cache-control-5.110.4.tgz → tryghost-mw-cache-control-5.112.0.tgz} +0 -0
- package/components/tryghost-mw-error-handler-5.112.0.tgz +0 -0
- package/components/{tryghost-mw-session-from-token-5.110.4.tgz → tryghost-mw-session-from-token-5.112.0.tgz} +0 -0
- package/components/{tryghost-mw-update-user-last-seen-5.110.4.tgz → tryghost-mw-update-user-last-seen-5.112.0.tgz} +0 -0
- package/components/tryghost-mw-version-match-5.112.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.112.0.tgz +0 -0
- package/components/tryghost-package-json-5.112.0.tgz +0 -0
- package/components/{tryghost-post-events-5.110.4.tgz → tryghost-post-events-5.112.0.tgz} +0 -0
- package/components/{tryghost-post-revisions-5.110.4.tgz → tryghost-post-revisions-5.112.0.tgz} +0 -0
- package/components/{tryghost-posts-service-5.110.4.tgz → tryghost-posts-service-5.112.0.tgz} +0 -0
- package/components/tryghost-prometheus-metrics-5.112.0.tgz +0 -0
- package/components/tryghost-recommendations-5.112.0.tgz +0 -0
- package/components/tryghost-referrers-5.112.0.tgz +0 -0
- package/components/{tryghost-security-5.110.4.tgz → tryghost-security-5.112.0.tgz} +0 -0
- package/components/tryghost-session-service-5.112.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.112.0.tgz +0 -0
- package/components/{tryghost-slack-notifications-5.110.4.tgz → tryghost-slack-notifications-5.112.0.tgz} +0 -0
- package/components/tryghost-tiers-5.112.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.112.0.tgz +0 -0
- package/components/tryghost-webmentions-5.112.0.tgz +0 -0
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +12799 -10270
- package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +2 -2
- package/core/built/admin/assets/admin-x-demo/{index-82e381fb.mjs → index-0040480a.mjs} +3252 -2891
- package/core/built/admin/assets/admin-x-demo/{modals-b20a9ede.mjs → modals-fb35c86c.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-ea62c29b.mjs → CodeEditorView-ad8698fe.mjs} +624 -618
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-af8cf9cf.mjs → index-2713e469.mjs} +6892 -6469
- package/core/built/admin/assets/admin-x-settings/{index-4b25c788.mjs → index-463cec50.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-cb2dc7b7.mjs → modals-033e8fc4.mjs} +7888 -7669
- package/core/built/admin/assets/{chunk.524.3096e68df5b51dacf872.js → chunk.524.db49da6fd8ae155205a4.js} +6 -6
- package/core/built/admin/assets/{chunk.582.e225422f90639ff30544.js → chunk.582.0bf715eb6807f7641706.js} +8 -8
- package/core/built/admin/assets/{ghost-98d002d50a5e01d2100b2c387a849249.js → ghost-62bd4d4c837d453e1038808dc1cd1e4c.js} +43 -42
- package/core/built/admin/assets/img/ap-nodes-01ee317529e6353a1c34a062c388f1e7.png +0 -0
- package/core/built/admin/assets/koenig-lexical/index.css +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +18314 -17680
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +229 -200
- package/core/built/admin/assets/posts/posts.js +24137 -24156
- package/core/built/admin/index.html +3 -3
- package/core/frontend/helpers/get.js +2 -3
- package/core/frontend/services/sitemap/SiteMapManager.js +1 -1
- package/core/frontend/src/cards/css/cta.css +40 -30
- package/core/frontend/src/cards/css/video.css +1 -0
- package/core/server/api/endpoints/settings-public.js +3 -2
- package/core/server/api/endpoints/utils/serializers/input/settings.js +3 -1
- package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.js +2 -1
- package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.js +2 -1
- package/core/server/data/migrations/versions/5.111/2025-03-05-16-36-39-add-captcha-setting.js +8 -0
- package/core/server/data/migrations/versions/5.112/2025-03-10-10-01-01-add-require-mfa-setting.js +8 -0
- package/core/server/data/schema/default-settings/default-settings.json +14 -0
- package/core/server/models/invite.js +4 -5
- package/core/server/models/post.js +3 -9
- package/core/server/models/relations/authors.js +2 -4
- package/core/server/models/role-utils.js +38 -0
- package/core/server/models/role.js +5 -3
- package/core/server/models/user.js +5 -3
- package/core/server/services/activitypub/ActivityPubService.js +116 -0
- package/core/server/services/activitypub/ActivityPubService.ts +139 -0
- package/core/server/services/activitypub/ActivityPubServiceWrapper.js +1 -1
- package/core/server/services/link-tracking/ClickEvent.js +25 -0
- package/core/server/services/link-tracking/FullPostLink.js +36 -0
- package/core/server/services/link-tracking/LinkClickRepository.js +1 -1
- package/core/server/services/link-tracking/LinkClickTrackingService.js +237 -0
- package/core/server/services/link-tracking/PostLink.js +29 -0
- package/core/server/services/link-tracking/PostLinkRepository.js +2 -2
- package/core/server/services/link-tracking/index.js +1 -1
- package/core/server/services/members-events/EventStorage.js +61 -0
- package/core/server/services/members-events/LastSeenAtCache.js +96 -0
- package/core/server/services/members-events/LastSeenAtUpdater.js +192 -0
- package/core/server/services/members-events/index.js +3 -1
- package/core/server/services/mentions-email-report/MentionEmailReportJob.js +117 -0
- package/core/server/services/mentions-email-report/service.js +3 -3
- package/core/server/services/staff/StaffService.js +179 -0
- package/core/server/services/staff/StaffServiceEmails.js +527 -0
- package/core/server/services/staff/email-templates/donation.hbs +119 -0
- package/core/server/services/staff/email-templates/donation.txt.js +15 -0
- package/core/server/services/staff/email-templates/mention-report.hbs +136 -0
- package/core/server/services/staff/email-templates/mention-report.txt.js +19 -0
- package/core/server/services/staff/email-templates/new-free-signup.hbs +118 -0
- package/core/server/services/staff/email-templates/new-free-signup.txt.js +13 -0
- package/core/server/services/staff/email-templates/new-milestone-received.hbs +142 -0
- package/core/server/services/staff/email-templates/new-milestone-received.txt.js +13 -0
- package/core/server/services/staff/email-templates/new-paid-cancellation.hbs +125 -0
- package/core/server/services/staff/email-templates/new-paid-cancellation.txt.js +13 -0
- package/core/server/services/staff/email-templates/new-paid-started.hbs +124 -0
- package/core/server/services/staff/email-templates/new-paid-started.txt.js +13 -0
- package/core/server/services/staff/email-templates/partials/preview.hbs +6 -0
- package/core/server/services/staff/email-templates/partials/styles.hbs +114 -0
- package/core/server/services/staff/email-templates/recommendation-received.hbs +154 -0
- package/core/server/services/staff/email-templates/recommendation-received.txt.js +13 -0
- package/core/server/services/staff/index.js +1 -1
- package/core/server/services/staff/milestone-email-config.js +207 -0
- package/core/server/services/stats/MembersStatsService.js +167 -0
- package/core/server/services/stats/MrrStatsService.js +161 -0
- package/core/server/services/stats/ReferrersStatsService.js +164 -0
- package/core/server/services/stats/StatsService.js +63 -0
- package/core/server/services/stats/SubscriptionStatsService.js +180 -0
- package/core/server/services/stats/service.js +1 -1
- package/core/server/services/url/Resources.js +2 -2
- package/core/shared/config/defaults.json +2 -1
- package/core/shared/events/URLResourceUpdatedEvent.js +33 -0
- package/core/shared/settings-cache/public.js +1 -0
- package/package.json +155 -158
- package/tsconfig.json +105 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/yarn.lock +347 -136
- package/components/tryghost-activitypub-5.110.4.tgz +0 -0
- package/components/tryghost-adapter-cache-memory-ttl-5.110.4.tgz +0 -0
- package/components/tryghost-adapter-cache-redis-5.110.4.tgz +0 -0
- package/components/tryghost-announcement-bar-settings-5.110.4.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.110.4.tgz +0 -0
- package/components/tryghost-bookshelf-repository-5.110.4.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.110.4.tgz +0 -0
- package/components/tryghost-captcha-service-5.110.4.tgz +0 -0
- package/components/tryghost-constants-5.110.4.tgz +0 -0
- package/components/tryghost-custom-fonts-5.110.4.tgz +0 -0
- package/components/tryghost-donations-5.110.4.tgz +0 -0
- package/components/tryghost-dynamic-routing-events-5.110.4.tgz +0 -0
- package/components/tryghost-email-addresses-5.110.4.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.110.4.tgz +0 -0
- package/components/tryghost-email-content-generator-5.110.4.tgz +0 -0
- package/components/tryghost-email-events-5.110.4.tgz +0 -0
- package/components/tryghost-email-service-5.110.4.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.110.4.tgz +0 -0
- package/components/tryghost-extract-api-key-5.110.4.tgz +0 -0
- package/components/tryghost-ghost-5.110.4.tgz +0 -0
- package/components/tryghost-i18n-5.110.4.tgz +0 -0
- package/components/tryghost-identity-token-service-5.110.4.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.110.4.tgz +0 -0
- package/components/tryghost-in-memory-repository-5.110.4.tgz +0 -0
- package/components/tryghost-link-replacer-5.110.4.tgz +0 -0
- package/components/tryghost-link-tracking-5.110.4.tgz +0 -0
- package/components/tryghost-mail-events-5.110.4.tgz +0 -0
- package/components/tryghost-mailgun-client-5.110.4.tgz +0 -0
- package/components/tryghost-members-events-service-5.110.4.tgz +0 -0
- package/components/tryghost-mentions-email-report-5.110.4.tgz +0 -0
- package/components/tryghost-minifier-5.110.4.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.110.4.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.110.4.tgz +0 -0
- package/components/tryghost-mw-version-match-5.110.4.tgz +0 -0
- package/components/tryghost-mw-vhost-5.110.4.tgz +0 -0
- package/components/tryghost-package-json-5.110.4.tgz +0 -0
- package/components/tryghost-prometheus-metrics-5.110.4.tgz +0 -0
- package/components/tryghost-recommendations-5.110.4.tgz +0 -0
- package/components/tryghost-referrers-5.110.4.tgz +0 -0
- package/components/tryghost-session-service-5.110.4.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.110.4.tgz +0 -0
- package/components/tryghost-staff-service-5.110.4.tgz +0 -0
- package/components/tryghost-stats-service-5.110.4.tgz +0 -0
- package/components/tryghost-tiers-5.110.4.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.110.4.tgz +0 -0
- package/components/tryghost-webmentions-5.110.4.tgz +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const moment = require('moment');
|
|
2
|
+
|
|
3
|
+
class ReferrersStatsService {
|
|
4
|
+
/**
|
|
5
|
+
* @param {object} deps
|
|
6
|
+
* @param {import('knex').Knex} deps.knex
|
|
7
|
+
**/
|
|
8
|
+
constructor({knex}) {
|
|
9
|
+
this.knex = knex;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Return a list of all the attribution sources for a given post, with their signup and conversion counts
|
|
14
|
+
* @param {string} postId
|
|
15
|
+
* @returns {Promise<AttributionCountStat[]>}
|
|
16
|
+
*/
|
|
17
|
+
async getForPost(postId) {
|
|
18
|
+
const knex = this.knex;
|
|
19
|
+
const signupRows = await knex('members_created_events')
|
|
20
|
+
.select('referrer_source')
|
|
21
|
+
.select(knex.raw('COUNT(id) AS total'))
|
|
22
|
+
.where('attribution_id', postId)
|
|
23
|
+
.where('attribution_type', 'post')
|
|
24
|
+
.groupBy('referrer_source');
|
|
25
|
+
|
|
26
|
+
const conversionRows = await knex('members_subscription_created_events')
|
|
27
|
+
.select('referrer_source')
|
|
28
|
+
.select(knex.raw('COUNT(id) AS total'))
|
|
29
|
+
.where('attribution_id', postId)
|
|
30
|
+
.where('attribution_type', 'post')
|
|
31
|
+
.groupBy('referrer_source');
|
|
32
|
+
|
|
33
|
+
// Stitch them toghether, grouping them by source
|
|
34
|
+
|
|
35
|
+
const map = new Map();
|
|
36
|
+
for (const row of signupRows) {
|
|
37
|
+
map.set(row.referrer_source, {
|
|
38
|
+
source: row.referrer_source,
|
|
39
|
+
signups: row.total,
|
|
40
|
+
paid_conversions: 0
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const row of conversionRows) {
|
|
45
|
+
const existing = map.get(row.referrer_source) ?? {
|
|
46
|
+
source: row.referrer_source,
|
|
47
|
+
signups: 0,
|
|
48
|
+
paid_conversions: 0
|
|
49
|
+
};
|
|
50
|
+
existing.paid_conversions = row.total;
|
|
51
|
+
map.set(row.referrer_source, existing);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return [...map.values()].sort((a, b) => b.paid_conversions - a.paid_conversions);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Return a list of all the attribution sources, with their signup and conversion counts on each date
|
|
59
|
+
* @returns {Promise<{data: AttributionCountStat[], meta: {}}>}
|
|
60
|
+
*/
|
|
61
|
+
async getReferrersHistory() {
|
|
62
|
+
const paidConversionEntries = await this.fetchAllPaidConversionSources();
|
|
63
|
+
const signupEntries = await this.fetchAllSignupSources();
|
|
64
|
+
|
|
65
|
+
const allEntries = signupEntries.map((entry) => {
|
|
66
|
+
return {
|
|
67
|
+
...entry,
|
|
68
|
+
paid_conversions: 0,
|
|
69
|
+
date: moment(entry.date).format('YYYY-MM-DD')
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
paidConversionEntries.forEach((entry) => {
|
|
74
|
+
const entryDate = moment(entry.date).format('YYYY-MM-DD');
|
|
75
|
+
const existingEntry = allEntries.find(e => e.source === entry.source && e.date === entryDate);
|
|
76
|
+
|
|
77
|
+
if (existingEntry) {
|
|
78
|
+
existingEntry.paid_conversions = entry.paid_conversions;
|
|
79
|
+
} else {
|
|
80
|
+
allEntries.push({
|
|
81
|
+
...entry,
|
|
82
|
+
signups: 0,
|
|
83
|
+
date: entryDate
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// sort allEntries in date ascending format
|
|
89
|
+
allEntries.sort((a, b) => {
|
|
90
|
+
return moment(a.date).diff(moment(b.date));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
data: allEntries,
|
|
95
|
+
meta: {}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @returns {Promise<PaidConversionsCountStatDate[]>}
|
|
101
|
+
**/
|
|
102
|
+
async fetchAllPaidConversionSources() {
|
|
103
|
+
const knex = this.knex;
|
|
104
|
+
const ninetyDaysAgo = moment.utc().subtract(90, 'days').startOf('day').utc().format('YYYY-MM-DD HH:mm:ss');
|
|
105
|
+
const rows = await knex('members_subscription_created_events')
|
|
106
|
+
.select(knex.raw(`DATE(created_at) as date`))
|
|
107
|
+
.select(knex.raw(`COUNT(*) as paid_conversions`))
|
|
108
|
+
.select(knex.raw(`referrer_source as source`))
|
|
109
|
+
.where('created_at', '>=', ninetyDaysAgo)
|
|
110
|
+
.groupBy('date', 'referrer_source')
|
|
111
|
+
.orderBy('date');
|
|
112
|
+
|
|
113
|
+
return rows;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @returns {Promise<SignupCountStatDate[]>}
|
|
118
|
+
**/
|
|
119
|
+
async fetchAllSignupSources() {
|
|
120
|
+
const knex = this.knex;
|
|
121
|
+
const ninetyDaysAgo = moment.utc().subtract(90, 'days').startOf('day').utc().format('YYYY-MM-DD HH:mm:ss');
|
|
122
|
+
const rows = await knex('members_created_events')
|
|
123
|
+
.select(knex.raw(`DATE(created_at) as date`))
|
|
124
|
+
.select(knex.raw(`COUNT(*) as signups`))
|
|
125
|
+
.select(knex.raw(`referrer_source as source`))
|
|
126
|
+
.where('created_at', '>=', ninetyDaysAgo)
|
|
127
|
+
.groupBy('date', 'referrer_source')
|
|
128
|
+
.orderBy('date');
|
|
129
|
+
|
|
130
|
+
return rows;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = ReferrersStatsService;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @typedef AttributionCountStat
|
|
138
|
+
* @type {Object}
|
|
139
|
+
* @property {string} source Attribution Source
|
|
140
|
+
* @property {number} signups Total free members signed up for this source
|
|
141
|
+
* @property {number} paid_conversions Total paid conversions for this source
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @typedef AttributionCountStatDate
|
|
146
|
+
* @type {AttributionCountStat}
|
|
147
|
+
* @property {string} date The date (YYYY-MM-DD) on which these counts were recorded
|
|
148
|
+
*/
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @typedef {object} SignupCountStatDate
|
|
152
|
+
* @type {Object}
|
|
153
|
+
* @property {string} source Attribution Source
|
|
154
|
+
* @property {number} signups Total free members signed up for this source
|
|
155
|
+
* @property {string} date The date (YYYY-MM-DD) on which these counts were recorded
|
|
156
|
+
**/
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @typedef {object} PaidConversionsCountStatDate
|
|
160
|
+
* @type {Object}
|
|
161
|
+
* @property {string} source Attribution Source
|
|
162
|
+
* @property {number} paid_conversions Total paid conversions for this source
|
|
163
|
+
* @property {string} date The date (YYYY-MM-DD) on which these counts were recorded
|
|
164
|
+
**/
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const MRRService = require('./MrrStatsService');
|
|
2
|
+
const MembersService = require('./MembersStatsService');
|
|
3
|
+
const SubscriptionStatsService = require('./SubscriptionStatsService');
|
|
4
|
+
const ReferrersStatsService = require('./ReferrersStatsService');
|
|
5
|
+
|
|
6
|
+
class StatsService {
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} deps
|
|
9
|
+
* @param {MRRService} deps.mrr
|
|
10
|
+
* @param {MembersService} deps.members
|
|
11
|
+
* @param {SubscriptionStatsService} deps.subscriptions
|
|
12
|
+
* @param {ReferrersStatsService} deps.referrers
|
|
13
|
+
**/
|
|
14
|
+
constructor(deps) {
|
|
15
|
+
this.mrr = deps.mrr;
|
|
16
|
+
this.members = deps.members;
|
|
17
|
+
this.subscriptions = deps.subscriptions;
|
|
18
|
+
this.referrers = deps.referrers;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getMRRHistory() {
|
|
22
|
+
return this.mrr.getHistory();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async getMemberCountHistory() {
|
|
26
|
+
return this.members.getCountHistory();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getSubscriptionCountHistory() {
|
|
30
|
+
return this.subscriptions.getSubscriptionHistory();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getReferrersHistory() {
|
|
34
|
+
return this.referrers.getReferrersHistory();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} postId
|
|
39
|
+
*/
|
|
40
|
+
async getPostReferrers(postId) {
|
|
41
|
+
return {
|
|
42
|
+
data: await this.referrers.getForPost(postId),
|
|
43
|
+
meta: {}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {object} deps
|
|
49
|
+
* @param {import('knex').Knex} deps.knex
|
|
50
|
+
*
|
|
51
|
+
* @returns {StatsService}
|
|
52
|
+
**/
|
|
53
|
+
static create(deps) {
|
|
54
|
+
return new StatsService({
|
|
55
|
+
mrr: new MRRService(deps),
|
|
56
|
+
members: new MembersService(deps),
|
|
57
|
+
subscriptions: new SubscriptionStatsService(deps),
|
|
58
|
+
referrers: new ReferrersStatsService(deps)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = StatsService;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const moment = require('moment');
|
|
2
|
+
|
|
3
|
+
class SubscriptionStatsService {
|
|
4
|
+
/**
|
|
5
|
+
* @param {object} deps
|
|
6
|
+
* @param {import('knex').Knex} deps.knex*/
|
|
7
|
+
constructor({knex}) {
|
|
8
|
+
this.knex = knex;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @returns {Promise<{data: SubscriptionHistoryEntry[]}>}
|
|
13
|
+
**/
|
|
14
|
+
async getSubscriptionHistory() {
|
|
15
|
+
const subscriptionDeltaEntries = await this.fetchAllSubscriptionDeltas();
|
|
16
|
+
const counts = await this.fetchSubscriptionCounts();
|
|
17
|
+
|
|
18
|
+
/** @type {Object.<string, Object.<string, number>>} */
|
|
19
|
+
const countData = {};
|
|
20
|
+
counts.forEach((count) => {
|
|
21
|
+
if (!countData[count.tier]) {
|
|
22
|
+
countData[count.tier] = {};
|
|
23
|
+
}
|
|
24
|
+
countData[count.tier][count.cadence] = count.count;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** @type {SubscriptionHistoryEntry[]} */
|
|
28
|
+
let subscriptionHistoryEntries = [];
|
|
29
|
+
|
|
30
|
+
/** @type {string[]} */
|
|
31
|
+
let cadences = [];
|
|
32
|
+
/** @type {string[]} */
|
|
33
|
+
let tiers = [];
|
|
34
|
+
|
|
35
|
+
for (let index = subscriptionDeltaEntries.length - 1; index >= 0; index -= 1) {
|
|
36
|
+
const entry = subscriptionDeltaEntries[index];
|
|
37
|
+
if (!countData[entry.tier]) {
|
|
38
|
+
countData[entry.tier] = {};
|
|
39
|
+
}
|
|
40
|
+
if (!countData[entry.tier][entry.cadence]) {
|
|
41
|
+
countData[entry.tier][entry.cadence] = 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
subscriptionHistoryEntries.unshift({
|
|
45
|
+
...entry,
|
|
46
|
+
date: moment(entry.date).format('YYYY-MM-DD'),
|
|
47
|
+
count: countData[entry.tier][entry.cadence]
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
countData[entry.tier][entry.cadence] += entry.negative_delta;
|
|
51
|
+
countData[entry.tier][entry.cadence] -= entry.positive_delta;
|
|
52
|
+
|
|
53
|
+
if (!cadences.includes(entry.cadence)) {
|
|
54
|
+
cadences.push(entry.cadence);
|
|
55
|
+
}
|
|
56
|
+
if (!tiers.includes(entry.tier)) {
|
|
57
|
+
tiers.push(entry.tier);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
data: subscriptionHistoryEntries,
|
|
63
|
+
meta: {
|
|
64
|
+
cadences,
|
|
65
|
+
tiers,
|
|
66
|
+
totals: counts
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @returns {Promise<SubscriptionDelta[]>}
|
|
73
|
+
**/
|
|
74
|
+
async fetchAllSubscriptionDeltas() {
|
|
75
|
+
const knex = this.knex;
|
|
76
|
+
const rows = await knex('members_paid_subscription_events')
|
|
77
|
+
.join('stripe_prices AS price', function () {
|
|
78
|
+
this.on('price.stripe_price_id', '=', 'members_paid_subscription_events.from_plan')
|
|
79
|
+
.orOn('price.stripe_price_id', '=', 'members_paid_subscription_events.to_plan');
|
|
80
|
+
})
|
|
81
|
+
.join('stripe_products AS product', 'product.stripe_product_id', '=', 'price.stripe_product_id')
|
|
82
|
+
.join('products AS tier', 'tier.id', '=', 'product.product_id')
|
|
83
|
+
.leftJoin('stripe_prices AS from_price', 'from_price.stripe_price_id', '=', 'members_paid_subscription_events.from_plan')
|
|
84
|
+
.leftJoin('stripe_prices AS to_price', 'to_price.stripe_price_id', '=', 'members_paid_subscription_events.to_plan')
|
|
85
|
+
.select(knex.raw(`
|
|
86
|
+
DATE(members_paid_subscription_events.created_at) as date
|
|
87
|
+
`))
|
|
88
|
+
.select(knex.raw(`
|
|
89
|
+
tier.id as tier
|
|
90
|
+
`))
|
|
91
|
+
.select(knex.raw(`
|
|
92
|
+
price.interval as cadence
|
|
93
|
+
`))
|
|
94
|
+
.select(knex.raw(`SUM(
|
|
95
|
+
CASE
|
|
96
|
+
WHEN members_paid_subscription_events.type IN ('created','reactivated','active') AND members_paid_subscription_events.mrr_delta != 0 THEN 1
|
|
97
|
+
WHEN members_paid_subscription_events.type='updated' AND price.id = to_price.id THEN 1
|
|
98
|
+
WHEN members_paid_subscription_events.type='updated' AND members_paid_subscription_events.from_plan = members_paid_subscription_events.to_plan AND members_paid_subscription_events.mrr_delta > 0 THEN 1
|
|
99
|
+
ELSE 0
|
|
100
|
+
END
|
|
101
|
+
) as positive_delta`))
|
|
102
|
+
.select(knex.raw(`SUM(
|
|
103
|
+
CASE
|
|
104
|
+
WHEN members_paid_subscription_events.type IN ('canceled', 'expired','inactive') AND members_paid_subscription_events.mrr_delta != 0 THEN 1
|
|
105
|
+
WHEN members_paid_subscription_events.type='updated' AND price.id = from_price.id THEN 1
|
|
106
|
+
ELSE 0
|
|
107
|
+
END
|
|
108
|
+
) as negative_delta`))
|
|
109
|
+
.select(knex.raw(`SUM(
|
|
110
|
+
CASE
|
|
111
|
+
WHEN members_paid_subscription_events.type IN ('created','reactivated','active') AND members_paid_subscription_events.mrr_delta != 0 THEN 1
|
|
112
|
+
WHEN members_paid_subscription_events.type='updated' AND members_paid_subscription_events.from_plan = members_paid_subscription_events.to_plan AND members_paid_subscription_events.mrr_delta > 0 THEN 1
|
|
113
|
+
ELSE 0
|
|
114
|
+
END
|
|
115
|
+
) as signups`))
|
|
116
|
+
.select(knex.raw(`SUM(
|
|
117
|
+
CASE
|
|
118
|
+
WHEN members_paid_subscription_events.type IN ('canceled', 'expired','inactive') AND members_paid_subscription_events.mrr_delta != 0 THEN 1
|
|
119
|
+
ELSE 0
|
|
120
|
+
END
|
|
121
|
+
) as cancellations`))
|
|
122
|
+
.groupBy('date', 'tier', 'cadence')
|
|
123
|
+
.orderBy('date');
|
|
124
|
+
|
|
125
|
+
return rows;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get the current total subscriptions grouped by Cadence and Tier
|
|
130
|
+
* @returns {Promise<SubscriptionCount[]>}
|
|
131
|
+
**/
|
|
132
|
+
async fetchSubscriptionCounts() {
|
|
133
|
+
const knex = this.knex;
|
|
134
|
+
|
|
135
|
+
const data = await knex('members_stripe_customers_subscriptions')
|
|
136
|
+
.select(knex.raw(`
|
|
137
|
+
COUNT(members_stripe_customers_subscriptions.id) AS count,
|
|
138
|
+
products.id AS tier,
|
|
139
|
+
stripe_prices.interval AS cadence
|
|
140
|
+
`))
|
|
141
|
+
.join('stripe_prices', 'stripe_prices.stripe_price_id', '=', 'members_stripe_customers_subscriptions.stripe_price_id')
|
|
142
|
+
.join('stripe_products', 'stripe_products.stripe_product_id', '=', 'stripe_prices.stripe_product_id')
|
|
143
|
+
.join('products', 'products.id', '=', 'stripe_products.product_id')
|
|
144
|
+
.whereNot('members_stripe_customers_subscriptions.mrr', 0)
|
|
145
|
+
.groupBy('tier', 'cadence');
|
|
146
|
+
|
|
147
|
+
return data;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @typedef {object} SubscriptionCount
|
|
152
|
+
* @prop {string} tier
|
|
153
|
+
* @prop {string} cadence
|
|
154
|
+
* @prop {number} count
|
|
155
|
+
**/
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @typedef {object} SubscriptionDelta
|
|
159
|
+
* @prop {string} tier
|
|
160
|
+
* @prop {string} cadence
|
|
161
|
+
* @prop {string} date
|
|
162
|
+
* @prop {number} positive_delta
|
|
163
|
+
* @prop {number} negative_delta
|
|
164
|
+
* @prop {number} signups
|
|
165
|
+
* @prop {number} cancellations
|
|
166
|
+
**/
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @typedef {object} SubscriptionHistoryEntry
|
|
170
|
+
* @prop {string} tier
|
|
171
|
+
* @prop {string} cadence
|
|
172
|
+
* @prop {string} date
|
|
173
|
+
* @prop {number} positive_delta
|
|
174
|
+
* @prop {number} negative_delta
|
|
175
|
+
* @prop {number} signups
|
|
176
|
+
* @prop {number} cancellations
|
|
177
|
+
* @prop {number} count
|
|
178
|
+
**/
|
|
179
|
+
|
|
180
|
+
module.exports = SubscriptionStatsService;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const debug = require('@tryghost/debug')('services:url:resources');
|
|
3
3
|
const DomainEvents = require('@tryghost/domain-events');
|
|
4
|
-
const
|
|
4
|
+
const URLResourceUpdatedEvent = require('../../../shared/events/URLResourceUpdatedEvent');
|
|
5
5
|
const Resource = require('./Resource');
|
|
6
6
|
const config = require('../../../shared/config');
|
|
7
7
|
const models = require('../../models');
|
|
@@ -446,7 +446,7 @@ class Resources {
|
|
|
446
446
|
* @returns {Object}
|
|
447
447
|
*/
|
|
448
448
|
getByIdAndType(type, id) {
|
|
449
|
-
return
|
|
449
|
+
return this.data[type]?.find(r => r.data.id === id);
|
|
450
450
|
}
|
|
451
451
|
|
|
452
452
|
/**
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module.exports = class URLResourceUpdatedEvent {
|
|
2
|
+
/**
|
|
3
|
+
* @readonly
|
|
4
|
+
* @type {Object}
|
|
5
|
+
*/
|
|
6
|
+
data;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @readonly
|
|
10
|
+
* @type {Date}
|
|
11
|
+
*/
|
|
12
|
+
timestamp;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @private
|
|
16
|
+
*/
|
|
17
|
+
constructor({timestamp, ...data}) {
|
|
18
|
+
this.data = data;
|
|
19
|
+
this.timestamp = timestamp;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} data URL Resource
|
|
25
|
+
* @returns
|
|
26
|
+
*/
|
|
27
|
+
static create(data) {
|
|
28
|
+
return new URLResourceUpdatedEvent({
|
|
29
|
+
...data,
|
|
30
|
+
timestamp: data.timestamp || new Date
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
};
|