ghost 6.0.0-rc.1 → 6.0.0-rc.3
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/README.md +3 -3
- package/components/tryghost-i18n-6.0.0-rc.3.tgz +0 -0
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +1 -1
- package/core/built/admin/assets/admin-x-activitypub/{index-D0b_o57K.mjs → index-BV-9xDf-.mjs} +2 -2
- package/core/built/admin/assets/admin-x-activitypub/{index-C3jWt_0g.mjs → index-D1M-ytsZ.mjs} +949 -910
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-63sCJxA8.mjs → CodeEditorView-Dk8bRHNh.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-CL8liuyk.mjs → index-B_Owqg5X.mjs} +12 -9
- package/core/built/admin/assets/admin-x-settings/{index-DXFGYHE_.mjs → index-Uj24wdZL.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-DJg5yTqf.mjs → modals-B21zmw-Y.mjs} +7697 -7705
- package/core/built/admin/assets/{chunk.524.f75344acd3938cf05152.js → chunk.524.124c2d3b27c954a9aaa6.js} +7 -7
- package/core/built/admin/assets/{chunk.582.4775488f7f26ad2a7d9c.js → chunk.582.a5dde33c7ca18e9ed7d9.js} +9 -9
- package/core/built/admin/assets/{ghost-11843760824cb811ee4a35896c817a34.js → ghost-338989d8cb5fce4a84d53ddd39ac80d4.js} +7 -7
- package/core/built/admin/assets/posts/posts.js +971 -964
- package/core/built/admin/assets/stats/stats.js +326 -319
- package/core/built/admin/index.html +3 -3
- package/core/server/api/endpoints/stats.js +1 -6
- package/core/server/services/stats/PostsStatsService.js +48 -105
- package/core/server/services/stats/ReferrersStatsService.js +122 -78
- package/core/server/services/stats/StatsService.js +13 -2
- package/core/server/services/stats/utils/date-utils.js +37 -0
- package/package.json +3 -3
- package/PRIVACY.md +0 -47
- package/components/tryghost-i18n-6.0.0-rc.1.tgz +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>Ghost</title>
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
<meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%226.0%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%2237bd1e3e4d%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%
|
|
9
|
+
<meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%226.0%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%2237bd1e3e4d%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%223958492b48%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%22a6a89eb153%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%221a3a628146%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%229a36081a2d%22%2C%22adminXActivitypubCustomUrl%22%3A%22https%3A%2F%2Fcdn.jsdelivr.net%2Fghost%2Fadmin-x-activitypub%400%2Fdist%2Fadmin-x-activitypub.js%22%7D" />
|
|
10
10
|
|
|
11
11
|
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1, minimal-ui, viewport-fit=cover" />
|
|
12
12
|
<meta name="pinterest" content="nopin" />
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
|
|
50
50
|
<script src="assets/vendor-aed0068cf9b67d042dd23a6343545b7b.js"></script>
|
|
51
51
|
<script src="assets/chunk.397.f965bec6bb556d2750de.js"></script>
|
|
52
|
-
<script src="assets/chunk.524.
|
|
53
|
-
<script src="assets/ghost-
|
|
52
|
+
<script src="assets/chunk.524.124c2d3b27c954a9aaa6.js"></script>
|
|
53
|
+
<script src="assets/ghost-338989d8cb5fce4a84d53ddd39ac80d4.js"></script>
|
|
54
54
|
</body>
|
|
55
55
|
</html>
|
|
@@ -533,12 +533,7 @@ const controller = {
|
|
|
533
533
|
};
|
|
534
534
|
},
|
|
535
535
|
async query(frame) {
|
|
536
|
-
return await statsService.api.getTopSourcesWithRange(
|
|
537
|
-
frame.options.date_from,
|
|
538
|
-
frame.options.date_to,
|
|
539
|
-
frame.options.order || 'free_members desc',
|
|
540
|
-
frame.options.limit || 50
|
|
541
|
-
);
|
|
536
|
+
return await statsService.api.getTopSourcesWithRange(frame.options);
|
|
542
537
|
}
|
|
543
538
|
}
|
|
544
539
|
|
|
@@ -4,6 +4,8 @@ const urlUtils = require('../../../shared/url-utils');
|
|
|
4
4
|
|
|
5
5
|
// Import source normalization from ReferrersStatsService
|
|
6
6
|
const {normalizeSource} = require('./ReferrersStatsService');
|
|
7
|
+
// Import centralized date utilities
|
|
8
|
+
const {getDateBoundaries, applyDateFilter} = require('./utils/date-utils');
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* @typedef {Object} StatsServiceOptions
|
|
@@ -21,6 +23,7 @@ const {normalizeSource} = require('./ReferrersStatsService');
|
|
|
21
23
|
* @property {number} [limit=20] - Maximum number of results to return
|
|
22
24
|
* @property {string} [date_from] - Start date filter in YYYY-MM-DD format
|
|
23
25
|
* @property {string} [date_to] - End date filter in YYYY-MM-DD format
|
|
26
|
+
* @property {string} [timezone='UTC'] - optional timezone for date interpretation
|
|
24
27
|
* @property {string} [post_type] - Filter by post type ('post', 'page')
|
|
25
28
|
*/
|
|
26
29
|
|
|
@@ -99,6 +102,7 @@ class PostsStatsService {
|
|
|
99
102
|
const limitRaw = Number.parseInt(String(options.limit ?? 20), 10);
|
|
100
103
|
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : 20;
|
|
101
104
|
const [orderField, orderDirection = 'desc'] = order.split(' ');
|
|
105
|
+
const {dateFrom, dateTo} = getDateBoundaries(options);
|
|
102
106
|
|
|
103
107
|
if (!['free_members', 'paid_members', 'mrr'].includes(orderField)) {
|
|
104
108
|
throw new errors.BadRequestError({
|
|
@@ -142,27 +146,13 @@ class PostsStatsService {
|
|
|
142
146
|
const subquery1 = this.select('attribution_url', 'attribution_type', 'attribution_id')
|
|
143
147
|
.from('members_created_events')
|
|
144
148
|
.whereNotNull('attribution_url');
|
|
145
|
-
|
|
146
|
-
if (options.date_from) {
|
|
147
|
-
subquery1.where('created_at', '>=', options.date_from);
|
|
148
|
-
}
|
|
149
|
-
if (options.date_to) {
|
|
150
|
-
subquery1.where('created_at', '<=', options.date_to + ' 23:59:59');
|
|
151
|
-
}
|
|
152
|
-
}
|
|
149
|
+
applyDateFilter(subquery1, dateFrom, dateTo, 'created_at');
|
|
153
150
|
|
|
154
151
|
subquery1.union(function () {
|
|
155
152
|
const subquery2 = this.select('attribution_url', 'attribution_type', 'attribution_id')
|
|
156
153
|
.from('members_subscription_created_events')
|
|
157
154
|
.whereNotNull('attribution_url');
|
|
158
|
-
|
|
159
|
-
if (options.date_from) {
|
|
160
|
-
subquery2.where('created_at', '>=', options.date_from);
|
|
161
|
-
}
|
|
162
|
-
if (options.date_to) {
|
|
163
|
-
subquery2.where('created_at', '<=', options.date_to + ' 23:59:59');
|
|
164
|
-
}
|
|
165
|
-
}
|
|
155
|
+
applyDateFilter(subquery2, dateFrom, dateTo, 'created_at');
|
|
166
156
|
})
|
|
167
157
|
.as('combined');
|
|
168
158
|
})
|
|
@@ -462,6 +452,7 @@ class PostsStatsService {
|
|
|
462
452
|
const selectField = groupByUrl ? 'mce.attribution_url' : 'mce.attribution_id as post_id';
|
|
463
453
|
const groupByField = groupByUrl ? 'mce.attribution_url' : 'mce.attribution_id';
|
|
464
454
|
const joinCondition = groupByUrl ? 'mce.attribution_url' : 'mce.attribution_id';
|
|
455
|
+
const {dateFrom, dateTo} = getDateBoundaries(options);
|
|
465
456
|
|
|
466
457
|
let subquery = knex('members_created_events as mce')
|
|
467
458
|
.select(selectField)
|
|
@@ -507,7 +498,7 @@ class PostsStatsService {
|
|
|
507
498
|
}
|
|
508
499
|
}
|
|
509
500
|
|
|
510
|
-
|
|
501
|
+
applyDateFilter(subquery, dateFrom, dateTo, 'mce.created_at');
|
|
511
502
|
return subquery;
|
|
512
503
|
}
|
|
513
504
|
|
|
@@ -523,6 +514,7 @@ class PostsStatsService {
|
|
|
523
514
|
const knex = this.knex;
|
|
524
515
|
const selectField = groupByUrl ? 'msce.attribution_url' : 'msce.attribution_id as post_id';
|
|
525
516
|
const groupByField = groupByUrl ? 'msce.attribution_url' : 'msce.attribution_id';
|
|
517
|
+
const {dateFrom, dateTo} = getDateBoundaries(options);
|
|
526
518
|
|
|
527
519
|
let subquery = knex('members_subscription_created_events as msce')
|
|
528
520
|
.select(selectField)
|
|
@@ -551,7 +543,7 @@ class PostsStatsService {
|
|
|
551
543
|
}
|
|
552
544
|
}
|
|
553
545
|
|
|
554
|
-
|
|
546
|
+
applyDateFilter(subquery, dateFrom, dateTo, 'msce.created_at');
|
|
555
547
|
return subquery;
|
|
556
548
|
}
|
|
557
549
|
|
|
@@ -566,6 +558,7 @@ class PostsStatsService {
|
|
|
566
558
|
_buildMrrSubquery(options, groupByUrl = false) {
|
|
567
559
|
const selectField = groupByUrl ? 'msce.attribution_url' : 'msce.attribution_id as post_id';
|
|
568
560
|
const groupByField = groupByUrl ? 'msce.attribution_url' : 'msce.attribution_id';
|
|
561
|
+
const {dateFrom, dateTo} = getDateBoundaries(options);
|
|
569
562
|
|
|
570
563
|
let subquery = this.knex('members_subscription_created_events as msce')
|
|
571
564
|
.select(selectField)
|
|
@@ -599,7 +592,7 @@ class PostsStatsService {
|
|
|
599
592
|
}
|
|
600
593
|
}
|
|
601
594
|
|
|
602
|
-
|
|
595
|
+
applyDateFilter(subquery, dateFrom, dateTo, 'msce.created_at');
|
|
603
596
|
return subquery;
|
|
604
597
|
}
|
|
605
598
|
|
|
@@ -615,6 +608,7 @@ class PostsStatsService {
|
|
|
615
608
|
*/
|
|
616
609
|
_buildFreeReferrersSubquery(postId, options) {
|
|
617
610
|
const knex = this.knex;
|
|
611
|
+
const {dateFrom, dateTo} = getDateBoundaries(options);
|
|
618
612
|
|
|
619
613
|
// Simpler approach mirroring _buildFreeMembersSubquery
|
|
620
614
|
let subquery = knex('members_created_events as mce')
|
|
@@ -631,7 +625,7 @@ class PostsStatsService {
|
|
|
631
625
|
.whereNull('msce.id') // Keep only signups where no matching paid conversion (same post/referrer) exists
|
|
632
626
|
.groupBy('mce.referrer_source');
|
|
633
627
|
|
|
634
|
-
|
|
628
|
+
applyDateFilter(subquery, dateFrom, dateTo, 'mce.created_at');
|
|
635
629
|
return subquery;
|
|
636
630
|
}
|
|
637
631
|
|
|
@@ -645,6 +639,7 @@ class PostsStatsService {
|
|
|
645
639
|
*/
|
|
646
640
|
_buildPaidReferrersSubquery(postId, options) {
|
|
647
641
|
const knex = this.knex;
|
|
642
|
+
const {dateFrom, dateTo} = getDateBoundaries(options);
|
|
648
643
|
let subquery = knex('members_subscription_created_events as msce')
|
|
649
644
|
.select('msce.referrer_source as source')
|
|
650
645
|
.countDistinct('msce.member_id as paid_members')
|
|
@@ -652,8 +647,7 @@ class PostsStatsService {
|
|
|
652
647
|
.where('msce.attribution_type', 'post')
|
|
653
648
|
.groupBy('msce.referrer_source');
|
|
654
649
|
|
|
655
|
-
|
|
656
|
-
this._applyDateFilter(subquery, options, 'msce.created_at');
|
|
650
|
+
applyDateFilter(subquery, dateFrom, dateTo, 'msce.created_at');
|
|
657
651
|
return subquery;
|
|
658
652
|
}
|
|
659
653
|
|
|
@@ -667,6 +661,7 @@ class PostsStatsService {
|
|
|
667
661
|
*/
|
|
668
662
|
_buildMrrReferrersSubquery(postId, options) {
|
|
669
663
|
const knex = this.knex;
|
|
664
|
+
const {dateFrom, dateTo} = getDateBoundaries(options);
|
|
670
665
|
let subquery = knex('members_subscription_created_events as msce')
|
|
671
666
|
.select('msce.referrer_source as source')
|
|
672
667
|
.sum('mpse.mrr_delta as mrr')
|
|
@@ -679,51 +674,10 @@ class PostsStatsService {
|
|
|
679
674
|
.where('msce.attribution_type', 'post')
|
|
680
675
|
.groupBy('msce.referrer_source');
|
|
681
676
|
|
|
682
|
-
|
|
683
|
-
this._applyDateFilter(subquery, options, 'msce.created_at');
|
|
677
|
+
applyDateFilter(subquery, dateFrom, dateTo, 'msce.created_at');
|
|
684
678
|
return subquery;
|
|
685
679
|
}
|
|
686
680
|
|
|
687
|
-
/**
|
|
688
|
-
* Apply date filters to a query builder instance
|
|
689
|
-
* @private
|
|
690
|
-
* @param {import('knex').Knex.QueryBuilder} query
|
|
691
|
-
* @param {StatsServiceOptions} options
|
|
692
|
-
* @param {string} dateColumn - The date column to filter on
|
|
693
|
-
*/
|
|
694
|
-
_applyDateFilter(query, options, dateColumn) {
|
|
695
|
-
// Note: Timezone handling might require converting dates before querying,
|
|
696
|
-
// depending on how created_at is stored (UTC assumed here).
|
|
697
|
-
if (options.date_from) {
|
|
698
|
-
try {
|
|
699
|
-
// Attempt to parse and validate the date
|
|
700
|
-
const fromDate = new Date(options.date_from);
|
|
701
|
-
if (!isNaN(fromDate.getTime())) {
|
|
702
|
-
query.where(dateColumn, '>=', fromDate);
|
|
703
|
-
} else {
|
|
704
|
-
logging.warn(`Invalid date_from format: ${options.date_from}. Skipping filter.`);
|
|
705
|
-
}
|
|
706
|
-
} catch (e) {
|
|
707
|
-
logging.warn(`Error parsing date_from: ${options.date_from}. Skipping filter.`);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
if (options.date_to) {
|
|
711
|
-
try {
|
|
712
|
-
const toDate = new Date(options.date_to);
|
|
713
|
-
if (!isNaN(toDate.getTime())) {
|
|
714
|
-
// Include the whole day for the 'to' date by setting to end of day
|
|
715
|
-
const endOfDay = new Date(toDate);
|
|
716
|
-
endOfDay.setHours(23, 59, 59, 999);
|
|
717
|
-
query.where(dateColumn, '<=', endOfDay);
|
|
718
|
-
} else {
|
|
719
|
-
logging.warn(`Invalid date_to format: ${options.date_to}. Skipping filter.`);
|
|
720
|
-
}
|
|
721
|
-
} catch (e) {
|
|
722
|
-
logging.warn(`Error parsing date_to: ${options.date_to}. Skipping filter.`);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
681
|
/**
|
|
728
682
|
* Get newsletter stats for sent or published posts with a specific newsletter_id
|
|
729
683
|
*
|
|
@@ -733,6 +687,7 @@ class PostsStatsService {
|
|
|
733
687
|
* @param {number|string} [options.limit=20] - Maximum number of results to return
|
|
734
688
|
* @param {string} [options.date_from] - Optional start date filter (YYYY-MM-DD)
|
|
735
689
|
* @param {string} [options.date_to] - Optional end date filter (YYYY-MM-DD)
|
|
690
|
+
* @param {string} [options.timezone] - Timezone to use for date interpretation
|
|
736
691
|
* @returns {Promise<{data: NewsletterStatResult[]}>} The newsletter stats for sent/published posts with the specified newsletter_id
|
|
737
692
|
*/
|
|
738
693
|
async getNewsletterStats(newsletterId, options = {}) {
|
|
@@ -740,6 +695,7 @@ class PostsStatsService {
|
|
|
740
695
|
const order = options.order || 'date desc';
|
|
741
696
|
const limitRaw = Number.parseInt(String(options.limit ?? 20), 10);
|
|
742
697
|
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : 20;
|
|
698
|
+
const {dateFrom, dateTo} = getDateBoundaries(options);
|
|
743
699
|
|
|
744
700
|
// Parse order field and direction
|
|
745
701
|
let [orderField, orderDirection = 'desc'] = order.split(' ');
|
|
@@ -766,19 +722,6 @@ class PostsStatsService {
|
|
|
766
722
|
});
|
|
767
723
|
}
|
|
768
724
|
|
|
769
|
-
// Build date filters if provided
|
|
770
|
-
let dateFilter = this.knex.raw('1=1');
|
|
771
|
-
if (options.date_from) {
|
|
772
|
-
dateFilter = this.knex.raw(`p.published_at >= ?`, [options.date_from]);
|
|
773
|
-
}
|
|
774
|
-
if (options.date_to) {
|
|
775
|
-
// Make date_to inclusive of the entire day by adding 23:59:59
|
|
776
|
-
const endOfDay = options.date_to + ' 23:59:59';
|
|
777
|
-
dateFilter = options.date_from
|
|
778
|
-
? this.knex.raw(`p.published_at >= ? AND p.published_at <= ?`, [options.date_from, endOfDay])
|
|
779
|
-
: this.knex.raw(`p.published_at <= ?`, [endOfDay]);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
725
|
// Subquery to count clicks from members_click_events
|
|
783
726
|
const clicksSubquery = this.knex
|
|
784
727
|
.select('r.post_id')
|
|
@@ -806,11 +749,12 @@ class PostsStatsService {
|
|
|
806
749
|
.leftJoin(clicksSubquery, 'p.id', 'clicks.post_id')
|
|
807
750
|
.where('p.newsletter_id', newsletterId)
|
|
808
751
|
.whereIn('p.status', ['sent', 'published'])
|
|
809
|
-
// Show all newsletters that were sent, even if no email record exists or has 0 engagement
|
|
810
|
-
.whereRaw(dateFilter)
|
|
811
752
|
.orderBy(orderFieldMap[orderField], orderDirection)
|
|
812
753
|
.limit(limit);
|
|
813
754
|
|
|
755
|
+
// Apply centralized date filtering
|
|
756
|
+
applyDateFilter(query, dateFrom, dateTo, 'p.published_at');
|
|
757
|
+
|
|
814
758
|
const results = await query;
|
|
815
759
|
|
|
816
760
|
return {data: results};
|
|
@@ -829,6 +773,7 @@ class PostsStatsService {
|
|
|
829
773
|
* @param {number} [options.limit] - Number of results to return (default: 20)
|
|
830
774
|
* @param {string} [options.date_from] - Optional start date filter (YYYY-MM-DD)
|
|
831
775
|
* @param {string} [options.date_to] - Optional end date filter (YYYY-MM-DD)
|
|
776
|
+
* @param {string} [options.timezone] - Timezone to use for date interpretation
|
|
832
777
|
* @returns {Promise<{data: Array}>} The newsletter basic stats (with click data when ordering by click_rate)
|
|
833
778
|
*/
|
|
834
779
|
async getNewsletterBasicStats(newsletterId, options = {}) {
|
|
@@ -836,6 +781,7 @@ class PostsStatsService {
|
|
|
836
781
|
const order = options.order || 'date desc';
|
|
837
782
|
const limitRaw = Number.parseInt(String(options.limit ?? 20), 10);
|
|
838
783
|
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : 20;
|
|
784
|
+
const {dateFrom, dateTo} = getDateBoundaries(options);
|
|
839
785
|
|
|
840
786
|
// Parse order field and direction
|
|
841
787
|
let [orderField, orderDirection = 'desc'] = order.split(' ');
|
|
@@ -862,19 +808,6 @@ class PostsStatsService {
|
|
|
862
808
|
});
|
|
863
809
|
}
|
|
864
810
|
|
|
865
|
-
// Build date filters if provided
|
|
866
|
-
let dateFilter = this.knex.raw('1=1');
|
|
867
|
-
if (options.date_from) {
|
|
868
|
-
dateFilter = this.knex.raw(`p.published_at >= ?`, [options.date_from]);
|
|
869
|
-
}
|
|
870
|
-
if (options.date_to) {
|
|
871
|
-
// Make date_to inclusive of the entire day by adding 23:59:59
|
|
872
|
-
const endOfDay = options.date_to + ' 23:59:59';
|
|
873
|
-
dateFilter = options.date_from
|
|
874
|
-
? this.knex.raw(`p.published_at >= ? AND p.published_at <= ?`, [options.date_from, endOfDay])
|
|
875
|
-
: this.knex.raw(`p.published_at <= ?`, [endOfDay]);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
811
|
let query;
|
|
879
812
|
|
|
880
813
|
// If ordering by click_rate, we need to include click data
|
|
@@ -906,10 +839,11 @@ class PostsStatsService {
|
|
|
906
839
|
.leftJoin(clicksSubquery, 'p.id', 'clicks.post_id')
|
|
907
840
|
.where('p.newsletter_id', newsletterId)
|
|
908
841
|
.whereIn('p.status', ['sent', 'published'])
|
|
909
|
-
// Show all newsletters that were sent, even if no email record exists or has 0 engagement
|
|
910
|
-
.whereRaw(dateFilter)
|
|
911
842
|
.orderBy(orderFieldMap[orderField], orderDirection)
|
|
912
843
|
.limit(limit);
|
|
844
|
+
|
|
845
|
+
// Apply centralized date filtering
|
|
846
|
+
applyDateFilter(query, dateFrom, dateTo, 'p.published_at');
|
|
913
847
|
} else {
|
|
914
848
|
// Build the query without click data for better performance
|
|
915
849
|
query = this.knex
|
|
@@ -925,10 +859,11 @@ class PostsStatsService {
|
|
|
925
859
|
.leftJoin('emails as e', 'p.id', 'e.post_id')
|
|
926
860
|
.where('p.newsletter_id', newsletterId)
|
|
927
861
|
.whereIn('p.status', ['sent', 'published'])
|
|
928
|
-
// Show all newsletters that were sent, even if no email record exists or has 0 engagement
|
|
929
|
-
.whereRaw(dateFilter)
|
|
930
862
|
.orderBy(orderFieldMap[orderField], orderDirection)
|
|
931
863
|
.limit(limit);
|
|
864
|
+
|
|
865
|
+
// Apply centralized date filtering
|
|
866
|
+
applyDateFilter(query, dateFrom, dateTo, 'p.published_at');
|
|
932
867
|
}
|
|
933
868
|
|
|
934
869
|
const results = await query;
|
|
@@ -996,6 +931,7 @@ class PostsStatsService {
|
|
|
996
931
|
* @param {Object} options - Query options
|
|
997
932
|
* @param {string} [options.date_from] - Optional start date filter (YYYY-MM-DD)
|
|
998
933
|
* @param {string} [options.date_to] - Optional end date filter (YYYY-MM-DD)
|
|
934
|
+
* @param {string} [options.timezone] - Timezone to use for date interpretation
|
|
999
935
|
* @returns {Promise<{data: Array<{total: number, deltas: Array<{date: string, value: number}>}>}>} The newsletter subscriber stats
|
|
1000
936
|
*/
|
|
1001
937
|
async getNewsletterSubscriberStats(newsletterId, options = {}) {
|
|
@@ -1055,8 +991,15 @@ class PostsStatsService {
|
|
|
1055
991
|
/**
|
|
1056
992
|
* Optimized query to get newsletter subscriber deltas
|
|
1057
993
|
* @private
|
|
994
|
+
* @param {string} newsletterId - ID of the newsletter to get subscriber deltas for
|
|
995
|
+
* @param {Object} options - Query options
|
|
996
|
+
* @param {string} [options.date_from] - Optional start date filter (YYYY-MM-DD)
|
|
997
|
+
* @param {string} [options.date_to] - Optional end date filter (YYYY-MM-DD)
|
|
998
|
+
* @param {string} [options.timezone] - Timezone to use for date interpretation
|
|
1058
999
|
*/
|
|
1059
1000
|
async _getNewsletterSubscriberDeltas(newsletterId, options = {}) {
|
|
1001
|
+
const {dateFrom, dateTo} = getDateBoundaries(options);
|
|
1002
|
+
|
|
1060
1003
|
// Build optimized deltas query - avoid expensive JOIN
|
|
1061
1004
|
let deltasQuery = this.knex('members_subscribe_events as mse')
|
|
1062
1005
|
.select(
|
|
@@ -1073,13 +1016,8 @@ class PostsStatsService {
|
|
|
1073
1016
|
.groupByRaw('DATE(mse.created_at)')
|
|
1074
1017
|
.orderBy('date', 'asc');
|
|
1075
1018
|
|
|
1076
|
-
// Apply date filters
|
|
1077
|
-
|
|
1078
|
-
deltasQuery.where('mse.created_at', '>=', options.date_from);
|
|
1079
|
-
}
|
|
1080
|
-
if (options.date_to) {
|
|
1081
|
-
deltasQuery.where('mse.created_at', '<=', `${options.date_to} 23:59:59`);
|
|
1082
|
-
}
|
|
1019
|
+
// Apply timezone-aware date filters
|
|
1020
|
+
applyDateFilter(deltasQuery, dateFrom, dateTo, 'mse.created_at');
|
|
1083
1021
|
|
|
1084
1022
|
return await deltasQuery;
|
|
1085
1023
|
}
|
|
@@ -1369,6 +1307,9 @@ class PostsStatsService {
|
|
|
1369
1307
|
* @private
|
|
1370
1308
|
* @param {string[]} postIds - Array of post IDs to get attribution counts for
|
|
1371
1309
|
* @param {Object} options - Date filter options
|
|
1310
|
+
* @param {string} [options.date_from] - Start date in YYYY-MM-DD format
|
|
1311
|
+
* @param {string} [options.date_to] - End date in YYYY-MM-DD format
|
|
1312
|
+
* @param {string} [options.timezone] - Timezone to use for date interpretation
|
|
1372
1313
|
* @returns {Promise<Array<{post_id: string, free_members: number, paid_members: number}>>}
|
|
1373
1314
|
*/
|
|
1374
1315
|
async _getMemberAttributionCounts(postIds, options = {}) {
|
|
@@ -1376,6 +1317,8 @@ class PostsStatsService {
|
|
|
1376
1317
|
return [];
|
|
1377
1318
|
}
|
|
1378
1319
|
|
|
1320
|
+
const {dateFrom, dateTo} = getDateBoundaries(options);
|
|
1321
|
+
|
|
1379
1322
|
try {
|
|
1380
1323
|
// Build free members query (modeled after _buildFreeMembersSubquery)
|
|
1381
1324
|
// Members who signed up on post but paid elsewhere/never
|
|
@@ -1393,7 +1336,7 @@ class PostsStatsService {
|
|
|
1393
1336
|
.groupBy('mce.attribution_id');
|
|
1394
1337
|
|
|
1395
1338
|
// Apply date filter to free members query
|
|
1396
|
-
|
|
1339
|
+
applyDateFilter(freeMembersQuery, dateFrom, dateTo, 'mce.created_at');
|
|
1397
1340
|
|
|
1398
1341
|
// Build paid members query (modeled after _buildPaidMembersSubquery)
|
|
1399
1342
|
// Members whose paid conversion was attributed to this post
|
|
@@ -1405,7 +1348,7 @@ class PostsStatsService {
|
|
|
1405
1348
|
.groupBy('msce.attribution_id');
|
|
1406
1349
|
|
|
1407
1350
|
// Apply date filter to paid members query
|
|
1408
|
-
|
|
1351
|
+
applyDateFilter(paidMembersQuery, dateFrom, dateTo, 'msce.created_at');
|
|
1409
1352
|
|
|
1410
1353
|
// Execute both queries
|
|
1411
1354
|
const [freeResults, paidResults] = await Promise.all([
|