ghost 5.121.0 → 5.123.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-i18n-5.123.0.tgz +0 -0
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +14017 -14035
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-03f7aa4d.mjs → CodeEditorView-aba69ca9.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
- package/core/built/admin/assets/admin-x-settings/{index-eea9098a.mjs → index-87586072.mjs} +4 -4
- package/core/built/admin/assets/admin-x-settings/{index-b0484a21.mjs → index-b3cfde97.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-85a92c66.mjs → modals-890d62ee.mjs} +1669 -1618
- package/core/built/admin/assets/{chunk.383.77dd3c8cbac5f464c6da.js → chunk.383.3dcd1102188f9d5e0467.js} +7 -7
- package/core/built/admin/assets/{chunk.524.22c1abc09947848be03d.js → chunk.524.f1c003b0bae2aa6a9805.js} +7 -7
- package/core/built/admin/assets/{chunk.582.a7cbc2ce76922c8870bc.js → chunk.582.4d677cde396354a18509.js} +9 -9
- package/core/built/admin/assets/{ghost-1c91950af9d6f9cee826820f056e0025.js → ghost-4dac0876cd7426ba504e4143b9772689.js} +141 -132
- package/core/built/admin/assets/posts/posts.js +71680 -41896
- package/core/built/admin/assets/stats/stats.js +35194 -35499
- package/core/built/admin/index.html +4 -4
- package/core/frontend/public/ghost-stats.min.js +3 -3
- package/core/frontend/src/ghost-stats/ghost-stats.js +0 -12
- package/core/server/api/endpoints/previews.js +25 -4
- package/core/server/api/endpoints/stats.js +50 -0
- package/core/server/data/migrations/versions/5.122/2025-06-03-19-32-57-change-default-for-newsletters-button-color.js +37 -0
- package/core/server/data/schema/schema.js +1 -1
- package/core/server/data/tinybird/endpoints/api_top_pages.pipe +1 -0
- package/core/server/data/tinybird/fixtures/analytics_events.ndjson +31 -31
- package/core/server/data/tinybird/pipes/mv_hits.pipe +51 -11
- package/core/server/data/tinybird/tests/api_top_pages.yaml +7 -0
- package/core/server/models/newsletter.js +1 -0
- package/core/server/services/adapter-manager/AdapterManager.js +5 -1
- package/core/server/services/comments/CommentsServiceEmails.js +8 -6
- package/core/server/services/email-service/EmailRenderer.js +80 -23
- package/core/server/services/email-service/email-templates/partials/paywall.hbs +56 -33
- package/core/server/services/email-service/email-templates/partials/styles.hbs +129 -4
- package/core/server/services/email-service/email-templates/template.hbs +1 -1
- package/core/server/services/koenig/node-renderers/button-renderer.js +1 -21
- package/core/server/services/koenig/node-renderers/call-to-action-renderer.js +10 -44
- package/core/server/services/koenig/node-renderers/header-v2-renderer.js +15 -31
- package/core/server/services/koenig/node-renderers/horizontalrule-renderer.js +43 -2
- package/core/server/services/koenig/node-renderers/product-renderer.js +0 -1
- package/core/server/services/koenig/render-partials/email-button.js +127 -21
- package/core/server/services/koenig/render-utils/stylex.js +104 -0
- package/core/server/services/stats/MembersStatsService.js +129 -5
- package/core/server/services/stats/PostsStatsService.js +281 -1
- package/core/server/services/stats/StatsService.js +23 -1
- package/core/server/services/stats/utils/tinybird.js +25 -5
- package/core/server/web/api/endpoints/admin/routes.js +2 -0
- package/package.json +13 -13
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +851 -310
- package/components/tryghost-i18n-5.121.0.tgz +0 -0
- package/core/frontend/src/utils/session-storage.js +0 -68
- /package/core/built/admin/assets/{chunk.383.77dd3c8cbac5f464c6da.js.LICENSE.txt → chunk.383.3dcd1102188f9d5e0467.js.LICENSE.txt} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
2
|
const errors = require('@tryghost/errors');
|
|
3
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @typedef {Object} StatsServiceOptions
|
|
@@ -49,9 +50,11 @@ class PostsStatsService {
|
|
|
49
50
|
/**
|
|
50
51
|
* @param {object} deps
|
|
51
52
|
* @param {import('knex').Knex} deps.knex - Database client
|
|
53
|
+
* @param {object} [deps.tinybirdClient] - Tinybird client for analytics
|
|
52
54
|
*/
|
|
53
55
|
constructor(deps) {
|
|
54
56
|
this.knex = deps.knex;
|
|
57
|
+
this.tinybirdClient = deps.tinybirdClient;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
/**
|
|
@@ -60,7 +63,7 @@ class PostsStatsService {
|
|
|
60
63
|
* @param {StatsServiceOptions} options
|
|
61
64
|
* @returns {Promise<{data: TopPostResult[]}>} The top posts based on the requested attribution metric
|
|
62
65
|
*/
|
|
63
|
-
async getTopPosts(options
|
|
66
|
+
async getTopPosts(options) {
|
|
64
67
|
try {
|
|
65
68
|
const order = options.order || 'free_members desc';
|
|
66
69
|
const limitRaw = Number.parseInt(String(options.limit ?? 20), 10); // Ensure options.limit is a string for parseInt
|
|
@@ -594,6 +597,283 @@ class PostsStatsService {
|
|
|
594
597
|
};
|
|
595
598
|
}
|
|
596
599
|
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Get stats for the latest published post including open rate, member attribution counts, and visitor count
|
|
603
|
+
* @returns {Promise<{data: Array<{id: string, title: string, slug: string, feature_image: string|null, published_at: string, recipient_count: number|null, opened_count: number|null, open_rate: number|null, member_delta: number, free_members: number, paid_members: number, visitors: number}>}>}
|
|
604
|
+
*/
|
|
605
|
+
async getLatestPostStats() {
|
|
606
|
+
try {
|
|
607
|
+
// Get the latest published post
|
|
608
|
+
const latestPost = await this.knex('posts as p')
|
|
609
|
+
.select(
|
|
610
|
+
'p.id',
|
|
611
|
+
'p.uuid',
|
|
612
|
+
'p.title',
|
|
613
|
+
'p.slug',
|
|
614
|
+
'p.feature_image',
|
|
615
|
+
'p.published_at',
|
|
616
|
+
'e.email_count',
|
|
617
|
+
'e.opened_count'
|
|
618
|
+
)
|
|
619
|
+
.leftJoin('emails as e', 'p.id', 'e.post_id')
|
|
620
|
+
.where('p.status', 'published')
|
|
621
|
+
.whereNotNull('p.published_at')
|
|
622
|
+
.orderBy('p.published_at', 'desc')
|
|
623
|
+
.first();
|
|
624
|
+
|
|
625
|
+
if (!latestPost) {
|
|
626
|
+
return {data: []};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Get member attribution counts using the same logic as other methods
|
|
630
|
+
const memberAttributionCounts = await this._getMemberAttributionCounts([latestPost.id]);
|
|
631
|
+
const attributionCount = memberAttributionCounts.find(ac => ac.post_id === latestPost.id);
|
|
632
|
+
|
|
633
|
+
const freeMembers = attributionCount ? attributionCount.free_members : 0;
|
|
634
|
+
const paidMembers = attributionCount ? attributionCount.paid_members : 0;
|
|
635
|
+
const totalMembers = freeMembers + paidMembers;
|
|
636
|
+
|
|
637
|
+
// Calculate open rate
|
|
638
|
+
const openRate = latestPost.email_count ?
|
|
639
|
+
(latestPost.opened_count / latestPost.email_count) * 100 :
|
|
640
|
+
null;
|
|
641
|
+
|
|
642
|
+
// Get visitor count from Tinybird
|
|
643
|
+
let visitors = 0;
|
|
644
|
+
if (this.tinybirdClient) {
|
|
645
|
+
try {
|
|
646
|
+
const dateFrom = new Date(latestPost.published_at).toISOString().split('T')[0];
|
|
647
|
+
const visitorData = await this.tinybirdClient.fetch('api_top_pages', {
|
|
648
|
+
post_uuid: latestPost.uuid,
|
|
649
|
+
dateFrom: dateFrom
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
visitors = visitorData?.[0]?.visits || 0;
|
|
653
|
+
} catch (error) {
|
|
654
|
+
logging.error('Error fetching visitor data from Tinybird:', error);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return {
|
|
659
|
+
data: [{
|
|
660
|
+
id: latestPost.id,
|
|
661
|
+
title: latestPost.title,
|
|
662
|
+
slug: latestPost.slug,
|
|
663
|
+
feature_image: latestPost.feature_image ? urlUtils.transformReadyToAbsolute(latestPost.feature_image) : latestPost.feature_image,
|
|
664
|
+
published_at: latestPost.published_at,
|
|
665
|
+
recipient_count: latestPost.email_count,
|
|
666
|
+
opened_count: latestPost.opened_count,
|
|
667
|
+
open_rate: openRate,
|
|
668
|
+
member_delta: totalMembers,
|
|
669
|
+
free_members: freeMembers,
|
|
670
|
+
paid_members: paidMembers,
|
|
671
|
+
visitors: visitors
|
|
672
|
+
}]
|
|
673
|
+
};
|
|
674
|
+
} catch (error) {
|
|
675
|
+
// Log the error but return a valid response
|
|
676
|
+
logging.error('Error fetching latest post stats:', error);
|
|
677
|
+
return {data: []};
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Get top posts by views for a given date range
|
|
683
|
+
* @param {Object} options
|
|
684
|
+
* @param {string} options.date_from - Start date in YYYY-MM-DD format
|
|
685
|
+
* @param {string} options.date_to - End date in YYYY-MM-DD format
|
|
686
|
+
* @param {string} options.timezone - Timezone to use for date interpretation
|
|
687
|
+
* @param {number} [options.limit] - Maximum number of posts to return (default: 5)
|
|
688
|
+
* @returns {Promise<Object>} Top posts with view counts and additional Ghost data
|
|
689
|
+
*/
|
|
690
|
+
async getTopPostsViews(options) {
|
|
691
|
+
try {
|
|
692
|
+
const limit = options.limit || 5;
|
|
693
|
+
let viewsData = [];
|
|
694
|
+
|
|
695
|
+
if (this.tinybirdClient) {
|
|
696
|
+
const tinybirdOptions = {
|
|
697
|
+
dateFrom: options.date_from,
|
|
698
|
+
dateTo: options.date_to,
|
|
699
|
+
timezone: options.timezone,
|
|
700
|
+
post_type: 'post',
|
|
701
|
+
limit: limit
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
viewsData = await this.tinybirdClient.fetch('api_top_pages', tinybirdOptions) || [];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Filter out any rows without post_uuid and get unique UUIDs
|
|
708
|
+
const postUuids = [...new Set(viewsData.filter(row => row.post_uuid).map(row => row.post_uuid))];
|
|
709
|
+
|
|
710
|
+
// Get posts data from Ghost DB for the posts we have views for
|
|
711
|
+
const posts = await this.knex('posts as p')
|
|
712
|
+
.select(
|
|
713
|
+
'p.id as post_id',
|
|
714
|
+
'p.uuid as post_uuid',
|
|
715
|
+
'p.title',
|
|
716
|
+
'p.published_at',
|
|
717
|
+
'p.feature_image',
|
|
718
|
+
'emails.email_count',
|
|
719
|
+
'emails.opened_count'
|
|
720
|
+
)
|
|
721
|
+
.leftJoin('emails', 'emails.post_id', 'p.id')
|
|
722
|
+
.whereIn('p.uuid', postUuids);
|
|
723
|
+
|
|
724
|
+
// Get member attribution counts for these posts (model after GrowthStats logic)
|
|
725
|
+
const memberAttributionCounts = await this._getMemberAttributionCounts(posts.map(p => p.post_id), options);
|
|
726
|
+
|
|
727
|
+
// Process posts with views
|
|
728
|
+
const postsWithViews = viewsData.map((row) => {
|
|
729
|
+
const post = posts.find(p => p.post_uuid === row.post_uuid);
|
|
730
|
+
|
|
731
|
+
if (!post) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Find the member attribution count for this post
|
|
736
|
+
const attributionCount = memberAttributionCounts.find(ac => ac.post_id === post.post_id);
|
|
737
|
+
const memberCount = attributionCount ? (attributionCount.free_members + attributionCount.paid_members) : 0;
|
|
738
|
+
|
|
739
|
+
return {
|
|
740
|
+
post_id: post.post_id,
|
|
741
|
+
title: post.title,
|
|
742
|
+
published_at: post.published_at,
|
|
743
|
+
feature_image: post.feature_image ? urlUtils.transformReadyToAbsolute(post.feature_image) : post.feature_image,
|
|
744
|
+
views: row.visits,
|
|
745
|
+
open_rate: post.email_count > 0 ? (post.opened_count / post.email_count) * 100 : null,
|
|
746
|
+
members: memberCount
|
|
747
|
+
};
|
|
748
|
+
}).filter(Boolean);
|
|
749
|
+
|
|
750
|
+
// Calculate how many more posts we need - we want to always return 5 posts
|
|
751
|
+
const remainingCount = limit - postsWithViews.length;
|
|
752
|
+
|
|
753
|
+
// If we need more posts, get the latest ones excluding the ones we already have
|
|
754
|
+
let additionalPosts = [];
|
|
755
|
+
let additionalMemberAttributionCounts = [];
|
|
756
|
+
if (remainingCount > 0) {
|
|
757
|
+
additionalPosts = await this.knex('posts as p')
|
|
758
|
+
.select(
|
|
759
|
+
'p.id as post_id',
|
|
760
|
+
'p.uuid as post_uuid',
|
|
761
|
+
'p.title',
|
|
762
|
+
'p.published_at',
|
|
763
|
+
'p.feature_image',
|
|
764
|
+
'emails.email_count',
|
|
765
|
+
'emails.opened_count'
|
|
766
|
+
)
|
|
767
|
+
.leftJoin('emails', 'emails.post_id', 'p.id')
|
|
768
|
+
.whereNotIn('p.uuid', postUuids)
|
|
769
|
+
.where('p.status', 'published')
|
|
770
|
+
.whereNotNull('p.published_at')
|
|
771
|
+
.orderBy('p.published_at', 'desc')
|
|
772
|
+
.limit(remainingCount);
|
|
773
|
+
|
|
774
|
+
// Get member attribution counts for additional posts
|
|
775
|
+
if (additionalPosts.length > 0) {
|
|
776
|
+
additionalMemberAttributionCounts = await this._getMemberAttributionCounts(additionalPosts.map(p => p.post_id), options);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Process additional posts with 0 views
|
|
781
|
+
const additionalPostsWithZeroViews = additionalPosts.map((post) => {
|
|
782
|
+
// Find the member attribution count for this post
|
|
783
|
+
const attributionCount = additionalMemberAttributionCounts.find(ac => ac.post_id === post.post_id);
|
|
784
|
+
const memberCount = attributionCount ? (attributionCount.free_members + attributionCount.paid_members) : 0;
|
|
785
|
+
|
|
786
|
+
return {
|
|
787
|
+
post_id: post.post_id,
|
|
788
|
+
title: post.title,
|
|
789
|
+
published_at: post.published_at,
|
|
790
|
+
feature_image: post.feature_image ? urlUtils.transformReadyToAbsolute(post.feature_image) : post.feature_image,
|
|
791
|
+
views: 0,
|
|
792
|
+
open_rate: post.email_count > 0 ? (post.opened_count / post.email_count) * 100 : null,
|
|
793
|
+
members: memberCount
|
|
794
|
+
};
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// Combine both sets of posts
|
|
798
|
+
return [...postsWithViews, ...additionalPostsWithZeroViews];
|
|
799
|
+
} catch (error) {
|
|
800
|
+
logging.error('Error fetching top posts views:', error);
|
|
801
|
+
return [];
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Get member attribution counts for a set of post IDs, modeling after GrowthStats logic
|
|
807
|
+
* Properly handles both free and paid members with deduplication
|
|
808
|
+
* @private
|
|
809
|
+
* @param {string[]} postIds - Array of post IDs to get attribution counts for
|
|
810
|
+
* @param {Object} options - Date filter options
|
|
811
|
+
* @returns {Promise<Array<{post_id: string, free_members: number, paid_members: number}>>}
|
|
812
|
+
*/
|
|
813
|
+
async _getMemberAttributionCounts(postIds, options = {}) {
|
|
814
|
+
if (!postIds.length) {
|
|
815
|
+
return [];
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
try {
|
|
819
|
+
// Build free members query (modeled after _buildFreeMembersSubquery)
|
|
820
|
+
// Members who signed up on post but paid elsewhere/never
|
|
821
|
+
let freeMembersQuery = this.knex('members_created_events as mce')
|
|
822
|
+
.select('mce.attribution_id as post_id')
|
|
823
|
+
.countDistinct('mce.member_id as free_members')
|
|
824
|
+
.leftJoin('members_subscription_created_events as msce', function () {
|
|
825
|
+
this.on('mce.member_id', '=', 'msce.member_id')
|
|
826
|
+
.andOn('mce.attribution_id', '=', 'msce.attribution_id')
|
|
827
|
+
.andOnVal('msce.attribution_type', '=', 'post');
|
|
828
|
+
})
|
|
829
|
+
.where('mce.attribution_type', 'post')
|
|
830
|
+
.whereIn('mce.attribution_id', postIds)
|
|
831
|
+
.whereNull('msce.id')
|
|
832
|
+
.groupBy('mce.attribution_id');
|
|
833
|
+
|
|
834
|
+
// Apply date filter to free members query
|
|
835
|
+
this._applyDateFilter(freeMembersQuery, options, 'mce.created_at');
|
|
836
|
+
|
|
837
|
+
// Build paid members query (modeled after _buildPaidMembersSubquery)
|
|
838
|
+
// Members whose paid conversion was attributed to this post
|
|
839
|
+
let paidMembersQuery = this.knex('members_subscription_created_events as msce')
|
|
840
|
+
.select('msce.attribution_id as post_id')
|
|
841
|
+
.countDistinct('msce.member_id as paid_members')
|
|
842
|
+
.where('msce.attribution_type', 'post')
|
|
843
|
+
.whereIn('msce.attribution_id', postIds)
|
|
844
|
+
.groupBy('msce.attribution_id');
|
|
845
|
+
|
|
846
|
+
// Apply date filter to paid members query
|
|
847
|
+
this._applyDateFilter(paidMembersQuery, options, 'msce.created_at');
|
|
848
|
+
|
|
849
|
+
// Execute both queries
|
|
850
|
+
const [freeResults, paidResults] = await Promise.all([
|
|
851
|
+
freeMembersQuery,
|
|
852
|
+
paidMembersQuery
|
|
853
|
+
]);
|
|
854
|
+
|
|
855
|
+
// Combine results for each post
|
|
856
|
+
const combinedResults = postIds.map((postId) => {
|
|
857
|
+
const freeResult = freeResults.find(r => r.post_id === postId);
|
|
858
|
+
const paidResult = paidResults.find(r => r.post_id === postId);
|
|
859
|
+
|
|
860
|
+
return {
|
|
861
|
+
post_id: postId,
|
|
862
|
+
free_members: freeResult ? freeResult.free_members : 0,
|
|
863
|
+
paid_members: paidResult ? paidResult.paid_members : 0
|
|
864
|
+
};
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
return combinedResults;
|
|
868
|
+
} catch (error) {
|
|
869
|
+
logging.error('Error fetching member attribution counts:', error);
|
|
870
|
+
return postIds.map(postId => ({
|
|
871
|
+
post_id: postId,
|
|
872
|
+
free_members: 0,
|
|
873
|
+
paid_members: 0
|
|
874
|
+
}));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
597
877
|
}
|
|
598
878
|
|
|
599
879
|
module.exports = PostsStatsService;
|
|
@@ -89,6 +89,20 @@ class StatsService {
|
|
|
89
89
|
return result;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Get top posts by views
|
|
94
|
+
* @param {Object} options
|
|
95
|
+
* @param {string} options.date_from - Start date in YYYY-MM-DD format
|
|
96
|
+
* @param {string} options.date_to - End date in YYYY-MM-DD format
|
|
97
|
+
* @param {string} options.timezone - Timezone to use for date interpretation
|
|
98
|
+
* @param {number} [options.limit=5] - Maximum number of posts to return
|
|
99
|
+
* @returns {Promise<{data: import('./PostsStatsService').TopPostResult[]}>}
|
|
100
|
+
*/
|
|
101
|
+
async getTopPostsViews(options) {
|
|
102
|
+
const result = await this.posts.getTopPostsViews(options);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
/**
|
|
93
107
|
* @param {string} postId
|
|
94
108
|
*/
|
|
@@ -142,6 +156,14 @@ class StatsService {
|
|
|
142
156
|
return result;
|
|
143
157
|
}
|
|
144
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Get stats for the latest published post
|
|
161
|
+
* @returns {Promise<{data: Object}>}
|
|
162
|
+
*/
|
|
163
|
+
async getLatestPostStats() {
|
|
164
|
+
return await this.posts.getLatestPostStats();
|
|
165
|
+
}
|
|
166
|
+
|
|
145
167
|
/**
|
|
146
168
|
* @param {object} deps
|
|
147
169
|
*
|
|
@@ -172,7 +194,7 @@ class StatsService {
|
|
|
172
194
|
members: new MembersService(deps),
|
|
173
195
|
subscriptions: new SubscriptionStatsService(deps),
|
|
174
196
|
referrers: new ReferrersStatsService(deps),
|
|
175
|
-
posts: new PostsStatsService(
|
|
197
|
+
posts: new PostsStatsService(depsWithTinybird),
|
|
176
198
|
content: new ContentStatsService(depsWithTinybird)
|
|
177
199
|
});
|
|
178
200
|
}
|
|
@@ -16,6 +16,7 @@ const create = ({config, request}) => {
|
|
|
16
16
|
* @param {string} [options.dateTo] - End date in YYYY-MM-DD format
|
|
17
17
|
* @param {string} [options.timezone] - Timezone for the query
|
|
18
18
|
* @param {string} [options.memberStatus] - Member status filter (defaults to 'all')
|
|
19
|
+
* @param {string} [options.postType] - Post type filter
|
|
19
20
|
* @param {string} [options.tbVersion] - Tinybird version for API URL
|
|
20
21
|
* @returns {Object} Object with URL and request options
|
|
21
22
|
*/
|
|
@@ -34,12 +35,31 @@ const create = ({config, request}) => {
|
|
|
34
35
|
|
|
35
36
|
// Use snake_case for query parameters as expected by Tinybird API
|
|
36
37
|
const searchParams = {
|
|
37
|
-
site_uuid: statsConfig.id
|
|
38
|
-
date_from: options.dateFrom,
|
|
39
|
-
date_to: options.dateTo,
|
|
40
|
-
timezone: options.timezone || config.get('timezone'),
|
|
41
|
-
member_status: options.memberStatus || 'all'
|
|
38
|
+
site_uuid: statsConfig.id
|
|
42
39
|
};
|
|
40
|
+
|
|
41
|
+
// todo: refactor all uses to simply pass options through
|
|
42
|
+
if (options.dateFrom) {
|
|
43
|
+
searchParams.date_from = options.dateFrom;
|
|
44
|
+
}
|
|
45
|
+
if (options.dateTo) {
|
|
46
|
+
searchParams.date_to = options.dateTo;
|
|
47
|
+
}
|
|
48
|
+
if (options.timezone) {
|
|
49
|
+
searchParams.timezone = options.timezone;
|
|
50
|
+
}
|
|
51
|
+
if (options.memberStatus) {
|
|
52
|
+
searchParams.member_status = options.memberStatus;
|
|
53
|
+
}
|
|
54
|
+
if (options.postType) {
|
|
55
|
+
searchParams.post_type = options.postType;
|
|
56
|
+
}
|
|
57
|
+
// Add any other options that might be needed
|
|
58
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
59
|
+
if (!['dateFrom', 'dateTo', 'timezone', 'memberStatus'].includes(key)) {
|
|
60
|
+
searchParams[key] = value;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
43
63
|
|
|
44
64
|
// Convert searchParams to query string and append to URL
|
|
45
65
|
const queryString = new URLSearchParams(searchParams).toString();
|
|
@@ -156,7 +156,9 @@ module.exports = function apiRoutes() {
|
|
|
156
156
|
router.get('/stats/referrers/posts/:id', mw.authAdminApi, http(api.stats.postReferrers));
|
|
157
157
|
router.get('/stats/referrers', mw.authAdminApi, http(api.stats.referrersHistory));
|
|
158
158
|
if (labs.isSet('trafficAnalytics')) {
|
|
159
|
+
router.get('/stats/latest-post', mw.authAdminApi, http(api.stats.latestPost));
|
|
159
160
|
router.get('/stats/top-posts', mw.authAdminApi, http(api.stats.topPosts));
|
|
161
|
+
router.get('/stats/top-posts-views', mw.authAdminApi, http(api.stats.topPostsViews));
|
|
160
162
|
router.get('/stats/top-content', mw.authAdminApi, http(api.stats.topContent));
|
|
161
163
|
router.get('/stats/newsletter-stats', mw.authAdminApi, http(api.stats.newsletterStats));
|
|
162
164
|
router.get('/stats/subscriber-count', mw.authAdminApi, http(api.stats.subscriberCount));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ghost",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.123.0",
|
|
4
4
|
"description": "The professional publishing platform",
|
|
5
5
|
"author": "Ghost Foundation",
|
|
6
6
|
"homepage": "https://ghost.org",
|
|
@@ -36,18 +36,18 @@
|
|
|
36
36
|
"test:unit:base": "yarn test:base './test/unit' --timeout=2000",
|
|
37
37
|
"test:integration": "yarn test:base './test/integration' --timeout=10000",
|
|
38
38
|
"test:e2e": "yarn test:base ./test/e2e-* --timeout=15000",
|
|
39
|
-
"test:
|
|
39
|
+
"test:legacy": "yarn test:base './test/legacy' --timeout=60000",
|
|
40
40
|
"test:browser": "NODE_ENV=testing-browser playwright test",
|
|
41
41
|
"test:browser:admin": "NODE_ENV=testing-browser playwright test test/e2e-browser --project=admin",
|
|
42
42
|
"test:browser:portal": "NODE_ENV=testing-browser playwright test test/e2e-browser --project=portal",
|
|
43
43
|
"test:browser:setup": "npx playwright install",
|
|
44
44
|
"test:ci:e2e": "c8 -c ./.c8rc.e2e.json -o coverage-e2e yarn test:e2e -b",
|
|
45
|
-
"test:ci:
|
|
45
|
+
"test:ci:legacy": "yarn test:legacy -b",
|
|
46
46
|
"test:ci:integration": "c8 -c ./.c8rc.e2e.json -o coverage-integration --lines 52 --functions 47 --branches 73 --statements 52 yarn test:integration -b",
|
|
47
47
|
"test:unit:slow": "yarn test:unit --reporter=mocha-slow-test-reporter",
|
|
48
48
|
"test:int:slow": "yarn test:integration --reporter=mocha-slow-test-reporter",
|
|
49
49
|
"test:e2e:slow": "yarn test:e2e --reporter=mocha-slow-test-reporter",
|
|
50
|
-
"test:
|
|
50
|
+
"test:leg:slow": "mocha --reporter dot --require=./test/utils/overrides.js --exit --trace-warnings --recursive --extension=test.js './test/legacy' --timeout=60000 --reporter=mocha-slow-test-reporter",
|
|
51
51
|
"lint:server": "eslint --ignore-path .eslintignore 'core/server/**/*.js' 'core/*.js' '*.js' --cache",
|
|
52
52
|
"lint:shared": "eslint --ignore-path .eslintignore 'core/shared/**/*.js' --cache",
|
|
53
53
|
"lint:frontend": "eslint --ignore-path .eslintignore 'core/frontend/**/*.js' --cache",
|
|
@@ -79,11 +79,11 @@
|
|
|
79
79
|
"@tryghost/domain-events": "1.0.1",
|
|
80
80
|
"@tryghost/email-mock-receiver": "0.3.11",
|
|
81
81
|
"@tryghost/errors": "1.3.8",
|
|
82
|
-
"@tryghost/helpers": "1.1.
|
|
82
|
+
"@tryghost/helpers": "1.1.95",
|
|
83
83
|
"@tryghost/html-to-plaintext": "1.0.4",
|
|
84
84
|
"@tryghost/http-cache-utils": "0.1.20",
|
|
85
|
-
"@tryghost/i18n": "file:components/tryghost-i18n-5.
|
|
86
|
-
"@tryghost/image-transform": "1.4.
|
|
85
|
+
"@tryghost/i18n": "file:components/tryghost-i18n-5.123.0.tgz",
|
|
86
|
+
"@tryghost/image-transform": "1.4.5",
|
|
87
87
|
"@tryghost/job-manager": "1.0.1",
|
|
88
88
|
"@tryghost/kg-card-factory": "5.1.1",
|
|
89
89
|
"@tryghost/kg-clean-basic-html": "4.2.5",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"@tryghost/pretty-cli": "1.2.47",
|
|
108
108
|
"@tryghost/prometheus-metrics": "1.0.1",
|
|
109
109
|
"@tryghost/promise": "0.3.15",
|
|
110
|
-
"@tryghost/referrer-parser": "0.1.
|
|
110
|
+
"@tryghost/referrer-parser": "0.1.7",
|
|
111
111
|
"@tryghost/request": "1.0.11",
|
|
112
112
|
"@tryghost/root-utils": "0.3.33",
|
|
113
113
|
"@tryghost/security": "1.0.1",
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
"ghost-storage-base": "1.0.0",
|
|
159
159
|
"glob": "8.1.0",
|
|
160
160
|
"got": "11.8.6",
|
|
161
|
-
"gscan": "4.
|
|
161
|
+
"gscan": "4.49.1",
|
|
162
162
|
"handlebars": "4.7.8",
|
|
163
163
|
"html-to-text": "5.1.1",
|
|
164
164
|
"html5parser": "2.0.2",
|
|
@@ -192,7 +192,7 @@
|
|
|
192
192
|
"mime-types": "2.1.35",
|
|
193
193
|
"moment": "2.24.0",
|
|
194
194
|
"moment-timezone": "0.5.45",
|
|
195
|
-
"multer": "2.0.
|
|
195
|
+
"multer": "2.0.1",
|
|
196
196
|
"mysql2": "3.14.1",
|
|
197
197
|
"nconf": "0.13.0",
|
|
198
198
|
"node-fetch": "2.7.0",
|
|
@@ -211,7 +211,7 @@
|
|
|
211
211
|
"stripe": "8.222.0",
|
|
212
212
|
"superagent": "5.3.1",
|
|
213
213
|
"superagent-throttle": "1.0.1",
|
|
214
|
-
"terser": "5.
|
|
214
|
+
"terser": "5.41.0",
|
|
215
215
|
"tiny-glob": "0.2.9",
|
|
216
216
|
"tough-cookie": "4.1.4",
|
|
217
217
|
"ua-parser-js": "1.0.40",
|
|
@@ -231,7 +231,7 @@
|
|
|
231
231
|
"@types/bookshelf": "1.2.9",
|
|
232
232
|
"@types/common-tags": "1.8.4",
|
|
233
233
|
"@types/jsonwebtoken": "9.0.9",
|
|
234
|
-
"@types/node": "22.15.
|
|
234
|
+
"@types/node": "22.15.30",
|
|
235
235
|
"@types/node-jose": "1.1.13",
|
|
236
236
|
"@types/nodemailer": "6.4.17",
|
|
237
237
|
"@types/sinon": "17.0.4",
|
|
@@ -272,7 +272,7 @@
|
|
|
272
272
|
"jackspeak": "2.3.6",
|
|
273
273
|
"moment": "2.24.0",
|
|
274
274
|
"moment-timezone": "0.5.45",
|
|
275
|
-
"@tryghost/i18n": "file:components/tryghost-i18n-5.
|
|
275
|
+
"@tryghost/i18n": "file:components/tryghost-i18n-5.123.0.tgz"
|
|
276
276
|
},
|
|
277
277
|
"nx": {
|
|
278
278
|
"targets": {
|