ghost 6.8.1 → 6.9.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-6.9.0.tgz +0 -0
- package/core/built/admin/assets/activitypub/activitypub.js +2 -2
- package/core/built/admin/assets/activitypub/{index-BbINZU9U.mjs → index-B29oZuTp.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{index-C3M839De.mjs → index-C19nEXqT.mjs} +7998 -7923
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-Dh1am4P6.mjs → CodeEditorView-BNKxdfRt.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-DqbTDHzA.mjs → index-B-_a183c.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-Hwd1HJ0-.mjs → index-CjRGpMVv.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-SxxA3jSX.mjs → index-Q0XmL0KU.mjs} +5 -5
- package/core/built/admin/assets/admin-x-settings/{modals-yhgQVHax.mjs → modals-omgXN6i-.mjs} +31 -27
- package/core/built/admin/assets/{chunk.524.bc2b771f38286b4a51ed.js → chunk.524.774a2df444e2ffde4942.js} +7 -7
- package/core/built/admin/assets/{chunk.582.a28f953895ae17fb3b24.js → chunk.582.ca4f05f3c39fda05b54c.js} +9 -9
- package/core/built/admin/assets/{ghost-7556359b8bd4ec08a6c23890b04bb56e.js → ghost-94d0fbb20e8e880fa9ba144cf26ab050.js} +27 -27
- package/core/built/admin/assets/ghost-dark-6c9cfa9c364e28c57e5983f68ec6f2fc.css +1 -0
- package/core/built/admin/assets/ghost-f724c1d53f5402f78a2d8cf8beb7c716.css +1 -0
- package/core/built/admin/assets/posts/posts.js +1 -1
- package/core/built/admin/assets/stats/stats.js +1 -1
- package/core/built/admin/index.html +4 -4
- package/core/frontend/public/robots.txt +1 -0
- package/core/frontend/web/middleware/index.js +0 -1
- package/core/frontend/web/routers/serve-favicon.js +56 -0
- package/core/frontend/web/site.js +2 -1
- package/core/server/data/tinybird/endpoints/api_kpis.pipe +0 -3
- package/core/server/data/tinybird/endpoints/api_top_locations.pipe +0 -3
- package/core/server/data/tinybird/endpoints/api_top_pages.pipe +0 -5
- package/core/server/data/tinybird/pipes/filtered_sessions.pipe +0 -3
- package/core/server/data/tinybird/scripts/analytics-generator.js +106 -2
- package/core/server/data/tinybird/tests/api_kpis.yaml +0 -36
- package/core/server/data/tinybird/tests/api_top_locations.yaml +2 -31
- package/core/server/data/tinybird/tests/api_top_pages.yaml +1 -30
- package/core/server/data/tinybird/tests/api_top_sources.yaml +2 -41
- package/core/server/data/tinybird/tests/api_top_utm_campaigns.yaml +3 -33
- package/core/server/data/tinybird/tests/api_top_utm_contents.yaml +3 -39
- package/core/server/data/tinybird/tests/api_top_utm_mediums.yaml +3 -34
- package/core/server/data/tinybird/tests/api_top_utm_sources.yaml +3 -43
- package/core/server/data/tinybird/tests/api_top_utm_terms.yaml +3 -36
- package/core/server/services/email-address/EmailAddressService.js +4 -1
- package/core/server/services/email-address/EmailAddressService.ts +5 -1
- package/core/server/services/email-analytics/EmailAnalyticsService.js +70 -16
- package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +6 -0
- package/core/server/services/email-analytics/jobs/update-member-email-analytics/index.js +4 -3
- package/core/server/services/email-analytics/lib/queries.js +84 -0
- package/core/server/services/email-service/EmailEventProcessor.js +113 -13
- package/core/server/services/email-service/EmailEventStorage.js +191 -26
- package/core/server/services/email-service/EmailRenderer.js +10 -3
- package/core/server/services/email-service/SendingService.js +1 -1
- package/core/server/services/lib/MailgunClient.js +2 -1
- package/core/server/services/member-welcome-emails/jobs/index.js +2 -2
- package/core/server/services/members/members-api/repositories/MemberRepository.js +2 -1
- package/core/server/services/tinybird/TinybirdService.js +0 -3
- package/core/server/web/admin/app.js +3 -1
- package/core/server/web/admin/controller.js +2 -1
- package/core/shared/config/defaults.json +2 -1
- package/core/shared/config/helpers.js +13 -0
- package/core/shared/url-utils.js +4 -2
- package/package.json +5 -5
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +158 -105
- package/components/tryghost-i18n-6.8.1.tgz +0 -0
- package/core/built/admin/assets/ghost-ca67b9eb701b867ae2a2fdd76cebdc17.css +0 -1
- package/core/built/admin/assets/ghost-dark-a5c3c5101d50a0af1f7b828ee387846d.css +0 -1
- package/core/frontend/web/middleware/serve-favicon.js +0 -72
- package/core/server/data/tinybird/endpoints/api_top_browsers.pipe +0 -54
- package/core/server/data/tinybird/endpoints/api_top_devices.pipe +0 -53
- package/core/server/data/tinybird/endpoints/api_top_os.pipe +0 -53
- package/core/server/data/tinybird/tests/api_top_browsers.yaml +0 -98
- package/core/server/data/tinybird/tests/api_top_devices.yaml +0 -75
- package/core/server/data/tinybird/tests/api_top_os.yaml +0 -80
|
@@ -12,28 +12,6 @@
|
|
|
12
12
|
{"utm_content":"search_ad","visits":1}
|
|
13
13
|
{"utm_content":"header_link","visits":1}
|
|
14
14
|
|
|
15
|
-
- name: Filtered by browser - Chrome
|
|
16
|
-
description: Filtered by browser - Chrome
|
|
17
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&browser=chrome
|
|
18
|
-
expected_result: |
|
|
19
|
-
{"utm_content":"post_123","visits":1}
|
|
20
|
-
{"utm_content":"video_ad","visits":1}
|
|
21
|
-
{"utm_content":"story_789","visits":1}
|
|
22
|
-
{"utm_content":"banner_ad","visits":1}
|
|
23
|
-
|
|
24
|
-
- name: Filtered by device - desktop
|
|
25
|
-
description: Filtered by device - desktop
|
|
26
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop
|
|
27
|
-
expected_result: |
|
|
28
|
-
{"utm_content":"post_123","visits":1}
|
|
29
|
-
{"utm_content":"video_ad","visits":1}
|
|
30
|
-
{"utm_content":"story_789","visits":1}
|
|
31
|
-
{"utm_content":"sponsored_post","visits":1}
|
|
32
|
-
{"utm_content":"tweet_456","visits":1}
|
|
33
|
-
{"utm_content":"banner_ad","visits":1}
|
|
34
|
-
{"utm_content":"search_ad","visits":1}
|
|
35
|
-
{"utm_content":"header_link","visits":1}
|
|
36
|
-
|
|
37
15
|
- name: Filtered by location - UK
|
|
38
16
|
description: Filtered by location - UK
|
|
39
17
|
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&location=GB
|
|
@@ -43,19 +21,6 @@
|
|
|
43
21
|
{"utm_content":"banner_ad","visits":1}
|
|
44
22
|
{"utm_content":"header_link","visits":1}
|
|
45
23
|
|
|
46
|
-
- name: Filtered by OS - Windows
|
|
47
|
-
description: Filtered by OS - Windows
|
|
48
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&os=windows
|
|
49
|
-
expected_result: |
|
|
50
|
-
{"utm_content":"post_123","visits":1}
|
|
51
|
-
{"utm_content":"video_ad","visits":1}
|
|
52
|
-
{"utm_content":"story_789","visits":1}
|
|
53
|
-
{"utm_content":"sponsored_post","visits":1}
|
|
54
|
-
{"utm_content":"tweet_456","visits":1}
|
|
55
|
-
{"utm_content":"banner_ad","visits":1}
|
|
56
|
-
{"utm_content":"search_ad","visits":1}
|
|
57
|
-
{"utm_content":"header_link","visits":1}
|
|
58
|
-
|
|
59
24
|
- name: Filtered by pathname - /about/
|
|
60
25
|
description: Filtered by pathname - /about/
|
|
61
26
|
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&pathname=%2Fabout%2F
|
|
@@ -110,8 +75,7 @@
|
|
|
110
75
|
|
|
111
76
|
- name: Test with multiple filters combined
|
|
112
77
|
description: Test with multiple filters combined
|
|
113
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&
|
|
78
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&source=bing.com&pathname=%2Fabout%2F
|
|
114
79
|
expected_result: |
|
|
115
|
-
{"utm_content":"
|
|
116
|
-
{"utm_content":"
|
|
117
|
-
{"utm_content":"search_ad","visits":1}
|
|
80
|
+
{"utm_content":"story_789","visits":1}
|
|
81
|
+
{"utm_content":"header_link","visits":1}
|
|
@@ -10,26 +10,6 @@
|
|
|
10
10
|
{"utm_medium":"display","visits":1}
|
|
11
11
|
{"utm_medium":"email","visits":1}
|
|
12
12
|
|
|
13
|
-
- name: Filtered by browser - Chrome
|
|
14
|
-
description: Filtered by browser - Chrome
|
|
15
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&browser=chrome
|
|
16
|
-
expected_result: |
|
|
17
|
-
{"utm_medium":"social","visits":2}
|
|
18
|
-
{"utm_medium":"organic","visits":1}
|
|
19
|
-
{"utm_medium":"referral","visits":1}
|
|
20
|
-
{"utm_medium":"display","visits":1}
|
|
21
|
-
|
|
22
|
-
- name: Filtered by device - desktop
|
|
23
|
-
description: Filtered by device - desktop
|
|
24
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop
|
|
25
|
-
expected_result: |
|
|
26
|
-
{"utm_medium":"social","visits":5}
|
|
27
|
-
{"utm_medium":"cpc","visits":1}
|
|
28
|
-
{"utm_medium":"organic","visits":1}
|
|
29
|
-
{"utm_medium":"referral","visits":1}
|
|
30
|
-
{"utm_medium":"display","visits":1}
|
|
31
|
-
{"utm_medium":"email","visits":1}
|
|
32
|
-
|
|
33
13
|
- name: Filtered by location - UK
|
|
34
14
|
description: Filtered by location - UK
|
|
35
15
|
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&location=GB
|
|
@@ -41,17 +21,6 @@
|
|
|
41
21
|
{"utm_medium":"display","visits":1}
|
|
42
22
|
{"utm_medium":"email","visits":1}
|
|
43
23
|
|
|
44
|
-
- name: Filtered by OS - Windows
|
|
45
|
-
description: Filtered by OS - Windows
|
|
46
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&os=windows
|
|
47
|
-
expected_result: |
|
|
48
|
-
{"utm_medium":"social","visits":5}
|
|
49
|
-
{"utm_medium":"cpc","visits":1}
|
|
50
|
-
{"utm_medium":"organic","visits":1}
|
|
51
|
-
{"utm_medium":"referral","visits":1}
|
|
52
|
-
{"utm_medium":"display","visits":1}
|
|
53
|
-
{"utm_medium":"email","visits":1}
|
|
54
|
-
|
|
55
24
|
- name: Filtered by pathname - /about/
|
|
56
25
|
description: Filtered by pathname - /about/
|
|
57
26
|
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&pathname=%2Fabout%2F
|
|
@@ -104,7 +73,7 @@
|
|
|
104
73
|
|
|
105
74
|
- name: Test with multiple filters combined
|
|
106
75
|
description: Test with multiple filters combined
|
|
107
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&
|
|
76
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&source=bing.com&pathname=%2Fabout%2F
|
|
108
77
|
expected_result: |
|
|
109
|
-
{"utm_medium":"social","visits":
|
|
110
|
-
{"utm_medium":"
|
|
78
|
+
{"utm_medium":"social","visits":1}
|
|
79
|
+
{"utm_medium":"email","visits":1}
|
|
@@ -13,30 +13,6 @@
|
|
|
13
13
|
{"utm_source":"partner_site","visits":1}
|
|
14
14
|
{"utm_source":"newsletter","visits":1}
|
|
15
15
|
|
|
16
|
-
- name: Filtered by browser - Chrome
|
|
17
|
-
description: Filtered by browser - Chrome
|
|
18
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&browser=chrome
|
|
19
|
-
expected_result: |
|
|
20
|
-
{"utm_source":"google","visits":1}
|
|
21
|
-
{"utm_source":"facebook","visits":1}
|
|
22
|
-
{"utm_source":"bing","visits":1}
|
|
23
|
-
{"utm_source":"instagram","visits":1}
|
|
24
|
-
{"utm_source":"partner_site","visits":1}
|
|
25
|
-
|
|
26
|
-
- name: Filtered by device - desktop
|
|
27
|
-
description: Filtered by device - desktop
|
|
28
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop
|
|
29
|
-
expected_result: |
|
|
30
|
-
{"utm_source":"google","visits":2}
|
|
31
|
-
{"utm_source":"linkedin","visits":1}
|
|
32
|
-
{"utm_source":"twitter","visits":1}
|
|
33
|
-
{"utm_source":"facebook","visits":1}
|
|
34
|
-
{"utm_source":"bing","visits":1}
|
|
35
|
-
{"utm_source":"reddit","visits":1}
|
|
36
|
-
{"utm_source":"instagram","visits":1}
|
|
37
|
-
{"utm_source":"partner_site","visits":1}
|
|
38
|
-
{"utm_source":"newsletter","visits":1}
|
|
39
|
-
|
|
40
16
|
- name: Filtered by location - UK
|
|
41
17
|
description: Filtered by location - UK
|
|
42
18
|
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&location=GB
|
|
@@ -47,20 +23,6 @@
|
|
|
47
23
|
{"utm_source":"partner_site","visits":1}
|
|
48
24
|
{"utm_source":"newsletter","visits":1}
|
|
49
25
|
|
|
50
|
-
- name: Filtered by OS - Windows
|
|
51
|
-
description: Filtered by OS - Windows
|
|
52
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&os=windows
|
|
53
|
-
expected_result: |
|
|
54
|
-
{"utm_source":"google","visits":2}
|
|
55
|
-
{"utm_source":"linkedin","visits":1}
|
|
56
|
-
{"utm_source":"twitter","visits":1}
|
|
57
|
-
{"utm_source":"facebook","visits":1}
|
|
58
|
-
{"utm_source":"bing","visits":1}
|
|
59
|
-
{"utm_source":"reddit","visits":1}
|
|
60
|
-
{"utm_source":"instagram","visits":1}
|
|
61
|
-
{"utm_source":"partner_site","visits":1}
|
|
62
|
-
{"utm_source":"newsletter","visits":1}
|
|
63
|
-
|
|
64
26
|
- name: Filtered by pathname - /about/
|
|
65
27
|
description: Filtered by pathname - /about/
|
|
66
28
|
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&pathname=%2Fabout%2F
|
|
@@ -120,9 +82,7 @@
|
|
|
120
82
|
|
|
121
83
|
- name: Test with multiple filters combined
|
|
122
84
|
description: Test with multiple filters combined
|
|
123
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&
|
|
85
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&source=bing.com&pathname=%2Fabout%2F
|
|
124
86
|
expected_result: |
|
|
125
|
-
{"utm_source":"
|
|
126
|
-
{"utm_source":"
|
|
127
|
-
{"utm_source":"google","visits":1}
|
|
128
|
-
{"utm_source":"reddit","visits":1}
|
|
87
|
+
{"utm_source":"instagram","visits":1}
|
|
88
|
+
{"utm_source":"newsletter","visits":1}
|
|
@@ -11,26 +11,6 @@
|
|
|
11
11
|
{"utm_term":"new_feature","visits":1}
|
|
12
12
|
{"utm_term":"announcement","visits":1}
|
|
13
13
|
|
|
14
|
-
- name: Filtered by browser - Chrome
|
|
15
|
-
description: Filtered by browser - Chrome
|
|
16
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&browser=chrome
|
|
17
|
-
expected_result: |
|
|
18
|
-
{"utm_term":"discount","visits":1}
|
|
19
|
-
{"utm_term":"ghost_blog","visits":1}
|
|
20
|
-
{"utm_term":"loyal_customers","visits":1}
|
|
21
|
-
|
|
22
|
-
- name: Filtered by device - desktop
|
|
23
|
-
description: Filtered by device - desktop
|
|
24
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop
|
|
25
|
-
expected_result: |
|
|
26
|
-
{"utm_term":"discount","visits":1}
|
|
27
|
-
{"utm_term":"ghost_blog","visits":1}
|
|
28
|
-
{"utm_term":"subscribers","visits":1}
|
|
29
|
-
{"utm_term":"loyal_customers","visits":1}
|
|
30
|
-
{"utm_term":"black_friday","visits":1}
|
|
31
|
-
{"utm_term":"new_feature","visits":1}
|
|
32
|
-
{"utm_term":"announcement","visits":1}
|
|
33
|
-
|
|
34
14
|
- name: Filtered by location - UK
|
|
35
15
|
description: Filtered by location - UK
|
|
36
16
|
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&location=GB
|
|
@@ -40,18 +20,6 @@
|
|
|
40
20
|
{"utm_term":"subscribers","visits":1}
|
|
41
21
|
{"utm_term":"new_feature","visits":1}
|
|
42
22
|
|
|
43
|
-
- name: Filtered by OS - Windows
|
|
44
|
-
description: Filtered by OS - Windows
|
|
45
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&os=windows
|
|
46
|
-
expected_result: |
|
|
47
|
-
{"utm_term":"discount","visits":1}
|
|
48
|
-
{"utm_term":"ghost_blog","visits":1}
|
|
49
|
-
{"utm_term":"subscribers","visits":1}
|
|
50
|
-
{"utm_term":"loyal_customers","visits":1}
|
|
51
|
-
{"utm_term":"black_friday","visits":1}
|
|
52
|
-
{"utm_term":"new_feature","visits":1}
|
|
53
|
-
{"utm_term":"announcement","visits":1}
|
|
54
|
-
|
|
55
23
|
- name: Filtered by pathname - /about/
|
|
56
24
|
description: Filtered by pathname - /about/
|
|
57
25
|
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&pathname=%2Fabout%2F
|
|
@@ -108,8 +76,7 @@
|
|
|
108
76
|
|
|
109
77
|
- name: Test with multiple filters combined
|
|
110
78
|
description: Test with multiple filters combined
|
|
111
|
-
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&
|
|
79
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&source=bing.com&pathname=%2Fabout%2F
|
|
112
80
|
expected_result: |
|
|
113
|
-
{"utm_term":"
|
|
114
|
-
{"utm_term":"
|
|
115
|
-
{"utm_term":"announcement","visits":1}
|
|
81
|
+
{"utm_term":"subscribers","visits":1}
|
|
82
|
+
{"utm_term":"loyal_customers","visits":1}
|
|
@@ -83,9 +83,12 @@ class EmailAddressService {
|
|
|
83
83
|
if (this.#labs.isSet('domainWarmup') && options.useFallbackAddress) {
|
|
84
84
|
const fallbackEmail = this.fallbackEmail;
|
|
85
85
|
if (fallbackEmail) {
|
|
86
|
+
if (!fallbackEmail.name) {
|
|
87
|
+
fallbackEmail.name = preferred.from.name || this.defaultFromEmail.name;
|
|
88
|
+
}
|
|
86
89
|
return {
|
|
87
90
|
from: fallbackEmail,
|
|
88
|
-
replyTo: preferred.replyTo || preferred.from
|
|
91
|
+
replyTo: preferred.replyTo || preferred.from || this.defaultFromEmail
|
|
89
92
|
};
|
|
90
93
|
}
|
|
91
94
|
else {
|
|
@@ -120,9 +120,13 @@ export class EmailAddressService {
|
|
|
120
120
|
if (this.#labs.isSet('domainWarmup') && options.useFallbackAddress) {
|
|
121
121
|
const fallbackEmail = this.fallbackEmail;
|
|
122
122
|
if (fallbackEmail) {
|
|
123
|
+
if (!fallbackEmail.name) {
|
|
124
|
+
fallbackEmail.name = preferred.from.name || this.defaultFromEmail.name;
|
|
125
|
+
}
|
|
126
|
+
|
|
123
127
|
return {
|
|
124
128
|
from: fallbackEmail,
|
|
125
|
-
replyTo: preferred.replyTo || preferred.from
|
|
129
|
+
replyTo: preferred.replyTo || preferred.from || this.defaultFromEmail
|
|
126
130
|
};
|
|
127
131
|
} else {
|
|
128
132
|
logging.error('[EmailAddresses] Fallback email address is not configured, cannot use fallback address for sending email.');
|
|
@@ -437,26 +437,55 @@ module.exports = class EmailAnalyticsService {
|
|
|
437
437
|
* @returns {Promise<void>}
|
|
438
438
|
*/
|
|
439
439
|
async processEventBatch(events, result, fetchData) {
|
|
440
|
-
|
|
441
|
-
const batchResult = await this.processEvent(event);
|
|
440
|
+
const useBatchProcessing = this.config.get('emailAnalytics:batchProcessing');
|
|
442
441
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
442
|
+
if (useBatchProcessing) {
|
|
443
|
+
// Batched mode: pre-fetch all recipients, then process events using cache
|
|
444
|
+
const emailIdentifications = events.map(event => ({
|
|
445
|
+
emailId: event.emailId,
|
|
446
|
+
providerId: event.providerId,
|
|
447
|
+
email: event.recipientEmail
|
|
448
|
+
}));
|
|
449
|
+
|
|
450
|
+
const recipientCache = await this.eventProcessor.batchGetRecipients(emailIdentifications);
|
|
451
|
+
|
|
452
|
+
for (const event of events) {
|
|
453
|
+
const batchResult = await this.processEvent(event, recipientCache);
|
|
454
|
+
|
|
455
|
+
// Save last event timestamp
|
|
456
|
+
if (!fetchData.lastEventTimestamp || (event.timestamp && event.timestamp > fetchData.lastEventTimestamp)) {
|
|
457
|
+
fetchData.lastEventTimestamp = event.timestamp;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
result.merge(batchResult);
|
|
446
461
|
}
|
|
447
462
|
|
|
448
|
-
|
|
463
|
+
// Flush all batched updates to the database
|
|
464
|
+
await this.eventProcessor.flushBatchedUpdates();
|
|
465
|
+
} else {
|
|
466
|
+
// Sequential mode: process events one by one (original behavior)
|
|
467
|
+
for (const event of events) {
|
|
468
|
+
const batchResult = await this.processEvent(event);
|
|
469
|
+
|
|
470
|
+
// Save last event timestamp
|
|
471
|
+
if (!fetchData.lastEventTimestamp || (event.timestamp && event.timestamp > fetchData.lastEventTimestamp)) {
|
|
472
|
+
fetchData.lastEventTimestamp = event.timestamp;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
result.merge(batchResult);
|
|
476
|
+
}
|
|
449
477
|
}
|
|
450
478
|
}
|
|
451
479
|
|
|
452
480
|
/**
|
|
453
481
|
*
|
|
454
482
|
* @param {{id: string, type: any; severity: any; recipientEmail: any; emailId?: string; providerId: string; timestamp: Date; error: {code: number; message: string; enhandedCode: string|number} | null}} event
|
|
483
|
+
* @param {Map<string, any>} [recipientCache] Optional cache for batched processing
|
|
455
484
|
* @returns {Promise<EventProcessingResult>}
|
|
456
485
|
*/
|
|
457
|
-
async processEvent(event) {
|
|
486
|
+
async processEvent(event, recipientCache) {
|
|
458
487
|
if (event.type === 'delivered') {
|
|
459
|
-
const recipient = await this.eventProcessor.handleDelivered({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp);
|
|
488
|
+
const recipient = await this.eventProcessor.handleDelivered({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp, recipientCache);
|
|
460
489
|
|
|
461
490
|
if (recipient) {
|
|
462
491
|
return new EventProcessingResult({
|
|
@@ -470,7 +499,7 @@ module.exports = class EmailAnalyticsService {
|
|
|
470
499
|
}
|
|
471
500
|
|
|
472
501
|
if (event.type === 'opened') {
|
|
473
|
-
const recipient = await this.eventProcessor.handleOpened({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp);
|
|
502
|
+
const recipient = await this.eventProcessor.handleOpened({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp, recipientCache);
|
|
474
503
|
|
|
475
504
|
if (recipient) {
|
|
476
505
|
return new EventProcessingResult({
|
|
@@ -485,7 +514,7 @@ module.exports = class EmailAnalyticsService {
|
|
|
485
514
|
|
|
486
515
|
if (event.type === 'failed') {
|
|
487
516
|
if (event.severity === 'permanent') {
|
|
488
|
-
const recipient = await this.eventProcessor.handlePermanentFailed({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, {id: event.id, timestamp: event.timestamp, error: event.error});
|
|
517
|
+
const recipient = await this.eventProcessor.handlePermanentFailed({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, {id: event.id, timestamp: event.timestamp, error: event.error}, recipientCache);
|
|
489
518
|
|
|
490
519
|
if (recipient) {
|
|
491
520
|
return new EventProcessingResult({
|
|
@@ -497,7 +526,7 @@ module.exports = class EmailAnalyticsService {
|
|
|
497
526
|
|
|
498
527
|
return new EventProcessingResult({unprocessable: 1});
|
|
499
528
|
} else {
|
|
500
|
-
const recipient = await this.eventProcessor.handleTemporaryFailed({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, {id: event.id, timestamp: event.timestamp, error: event.error});
|
|
529
|
+
const recipient = await this.eventProcessor.handleTemporaryFailed({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, {id: event.id, timestamp: event.timestamp, error: event.error}, recipientCache);
|
|
501
530
|
|
|
502
531
|
if (recipient) {
|
|
503
532
|
return new EventProcessingResult({
|
|
@@ -512,7 +541,7 @@ module.exports = class EmailAnalyticsService {
|
|
|
512
541
|
}
|
|
513
542
|
|
|
514
543
|
if (event.type === 'unsubscribed') {
|
|
515
|
-
const recipient = await this.eventProcessor.handleUnsubscribed({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp);
|
|
544
|
+
const recipient = await this.eventProcessor.handleUnsubscribed({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp, recipientCache);
|
|
516
545
|
|
|
517
546
|
if (recipient) {
|
|
518
547
|
return new EventProcessingResult({
|
|
@@ -526,7 +555,7 @@ module.exports = class EmailAnalyticsService {
|
|
|
526
555
|
}
|
|
527
556
|
|
|
528
557
|
if (event.type === 'complained') {
|
|
529
|
-
const recipient = await this.eventProcessor.handleComplained({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp);
|
|
558
|
+
const recipient = await this.eventProcessor.handleComplained({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp, recipientCache);
|
|
530
559
|
|
|
531
560
|
if (recipient) {
|
|
532
561
|
return new EventProcessingResult({
|
|
@@ -547,15 +576,31 @@ module.exports = class EmailAnalyticsService {
|
|
|
547
576
|
* @param {boolean} includeOpenedEvents
|
|
548
577
|
*/
|
|
549
578
|
async aggregateStats({emailIds = [], memberIds = []}, includeOpenedEvents = true) {
|
|
579
|
+
const useBatchProcessing = this.config.get('emailAnalytics:batchProcessing');
|
|
580
|
+
|
|
550
581
|
for (const emailId of emailIds) {
|
|
551
582
|
await this.aggregateEmailStats(emailId, includeOpenedEvents);
|
|
552
583
|
}
|
|
553
584
|
|
|
554
585
|
// @ts-expect-error
|
|
555
586
|
const memberMetric = this.prometheusClient?.getMetric('email_analytics_aggregate_member_stats_count');
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
587
|
+
|
|
588
|
+
if (useBatchProcessing) {
|
|
589
|
+
// Batched mode: process 100 members at a time
|
|
590
|
+
logging.info(`[EmailAnalytics] Aggregating stats for ${memberIds.length} members using BATCHED mode (batch size: 100)`);
|
|
591
|
+
const BATCH_SIZE = 100;
|
|
592
|
+
for (let i = 0; i < memberIds.length; i += BATCH_SIZE) {
|
|
593
|
+
const batch = memberIds.slice(i, i + BATCH_SIZE);
|
|
594
|
+
await this.aggregateMemberStatsBatch(batch);
|
|
595
|
+
memberMetric?.inc(batch.length);
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
// Sequential mode: process one member at a time
|
|
599
|
+
logging.info(`[EmailAnalytics] Aggregating stats for ${memberIds.length} members using SEQUENTIAL mode`);
|
|
600
|
+
for (const memberId of memberIds) {
|
|
601
|
+
await this.aggregateMemberStats(memberId);
|
|
602
|
+
memberMetric?.inc();
|
|
603
|
+
}
|
|
559
604
|
}
|
|
560
605
|
}
|
|
561
606
|
|
|
@@ -577,4 +622,13 @@ module.exports = class EmailAnalyticsService {
|
|
|
577
622
|
async aggregateMemberStats(memberId) {
|
|
578
623
|
return this.queries.aggregateMemberStats(memberId);
|
|
579
624
|
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Aggregate member stats for multiple members in a batch.
|
|
628
|
+
* @param {string[]} memberIds - Array of member IDs to aggregate stats for.
|
|
629
|
+
* @returns {Promise<void>}
|
|
630
|
+
*/
|
|
631
|
+
async aggregateMemberStatsBatch(memberIds) {
|
|
632
|
+
return this.queries.aggregateMemberStatsBatch(memberIds);
|
|
633
|
+
}
|
|
580
634
|
};
|
|
@@ -57,6 +57,10 @@ class EmailAnalyticsServiceWrapper {
|
|
|
57
57
|
prometheusClient
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
// Log the processing mode on initialization
|
|
61
|
+
const batchProcessingEnabled = config.get('emailAnalytics:batchProcessing');
|
|
62
|
+
logging.info(`[EmailAnalytics] Initialized with ${batchProcessingEnabled ? 'BATCHED' : 'SEQUENTIAL'} processing mode`);
|
|
63
|
+
|
|
60
64
|
// We currently cannot trigger a non-offloaded job from the job manager
|
|
61
65
|
// So the email analytics jobs simply emits an event.
|
|
62
66
|
domainEvents.subscribe(StartEmailAnalyticsJobEvent, async () => {
|
|
@@ -81,10 +85,12 @@ class EmailAnalyticsServiceWrapper {
|
|
|
81
85
|
const apiPercent = totalDurationMs > 0 ? Math.round((apiPollingTimeMs / totalDurationMs) * 100) : 0;
|
|
82
86
|
const processingPercent = totalDurationMs > 0 ? Math.round((processingTimeMs / totalDurationMs) * 100) : 0;
|
|
83
87
|
const aggregationPercent = totalDurationMs > 0 ? Math.round((aggregationTimeMs / totalDurationMs) * 100) : 0;
|
|
88
|
+
const batchMode = config.get('emailAnalytics:batchProcessing') ? 'BATCHED' : 'SEQUENTIAL';
|
|
84
89
|
|
|
85
90
|
const logMessage = [
|
|
86
91
|
`[EmailAnalytics] Job complete: ${jobType}`,
|
|
87
92
|
`${eventCount} events in ${(totalDurationMs / 1000).toFixed(1)}s (${throughput.toFixed(2)} events/s)`,
|
|
93
|
+
`Mode: ${batchMode}`,
|
|
88
94
|
`Timings: API ${(apiPollingTimeMs / 1000).toFixed(1)}s (${apiPercent}%) / Processing ${(processingTimeMs / 1000).toFixed(1)}s (${processingPercent}%) / Aggregation ${(aggregationTimeMs / 1000).toFixed(1)}s (${aggregationPercent}%)`,
|
|
89
95
|
`Events: opened=${result.opened} delivered=${result.delivered} failed=${result.permanentFailed + result.temporaryFailed} unprocessable=${result.unprocessable}`
|
|
90
96
|
].join(' | ');
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
const queries = require('../../lib/queries');
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
/**
|
|
4
4
|
* Updates email analytics for a specific member
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* @param {Object} options - The options object
|
|
7
7
|
* @param {string} options.memberId - The ID of the member to update analytics for
|
|
8
8
|
* @returns {Promise<Object>} The result of the aggregation query (1/0)
|
|
9
9
|
*/
|
|
10
10
|
module.exports = async function updateMemberEmailAnalytics({memberId}) {
|
|
11
|
-
|
|
11
|
+
// Use the batch method with a single member for consistency
|
|
12
|
+
const result = await queries.aggregateMemberStatsBatch([memberId]);
|
|
12
13
|
return result;
|
|
13
14
|
};
|
|
@@ -201,5 +201,89 @@ module.exports = {
|
|
|
201
201
|
await db.knex('members')
|
|
202
202
|
.update(updateQuery)
|
|
203
203
|
.where('id', memberId);
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
async aggregateMemberStatsBatch(memberIds) {
|
|
207
|
+
if (!memberIds || memberIds.length === 0) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Batch query to get stats for all members at once
|
|
212
|
+
const stats = await db.knex('email_recipients')
|
|
213
|
+
.leftJoin('emails', 'emails.id', 'email_recipients.email_id')
|
|
214
|
+
.select(
|
|
215
|
+
'email_recipients.member_id',
|
|
216
|
+
db.knex.raw('COUNT(email_recipients.id) as email_count'),
|
|
217
|
+
db.knex.raw('SUM(CASE WHEN email_recipients.opened_at IS NOT NULL THEN 1 ELSE 0 END) as email_opened_count'),
|
|
218
|
+
db.knex.raw('SUM(CASE WHEN emails.track_opens = 1 THEN 1 ELSE 0 END) as tracked_count')
|
|
219
|
+
)
|
|
220
|
+
.whereIn('email_recipients.member_id', memberIds)
|
|
221
|
+
.groupBy('email_recipients.member_id');
|
|
222
|
+
|
|
223
|
+
// Build update data for each member
|
|
224
|
+
const memberStatsMap = new Map();
|
|
225
|
+
for (const stat of stats) {
|
|
226
|
+
const emailOpenRate = stat.tracked_count >= MIN_EMAIL_COUNT_FOR_OPEN_RATE
|
|
227
|
+
? Math.round((stat.email_opened_count / stat.tracked_count) * 100)
|
|
228
|
+
: null;
|
|
229
|
+
|
|
230
|
+
memberStatsMap.set(stat.member_id, {
|
|
231
|
+
email_count: stat.email_count,
|
|
232
|
+
email_opened_count: stat.email_opened_count,
|
|
233
|
+
email_open_rate: emailOpenRate
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Build CASE statements for batch update
|
|
238
|
+
const emailCountCases = [];
|
|
239
|
+
const emailOpenedCountCases = [];
|
|
240
|
+
const emailOpenRateCases = [];
|
|
241
|
+
const emailCountBindings = [];
|
|
242
|
+
const emailOpenedCountBindings = [];
|
|
243
|
+
const emailOpenRateBindings = [];
|
|
244
|
+
|
|
245
|
+
for (const memberId of memberIds) {
|
|
246
|
+
const memberStats = memberStatsMap.get(memberId) || {
|
|
247
|
+
email_count: 0,
|
|
248
|
+
email_opened_count: 0,
|
|
249
|
+
email_open_rate: null
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
emailCountCases.push(`WHEN ? THEN ?`);
|
|
253
|
+
emailCountBindings.push(memberId, memberStats.email_count);
|
|
254
|
+
|
|
255
|
+
emailOpenedCountCases.push(`WHEN ? THEN ?`);
|
|
256
|
+
emailOpenedCountBindings.push(memberId, memberStats.email_opened_count);
|
|
257
|
+
|
|
258
|
+
if (memberStats.email_open_rate !== null) {
|
|
259
|
+
emailOpenRateCases.push(`WHEN ? THEN ?`);
|
|
260
|
+
emailOpenRateBindings.push(memberId, memberStats.email_open_rate);
|
|
261
|
+
} else {
|
|
262
|
+
emailOpenRateCases.push(`WHEN ? THEN NULL`);
|
|
263
|
+
emailOpenRateBindings.push(memberId);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Combine bindings in the order they appear in the SQL statement:
|
|
268
|
+
// 1. All bindings for email_count CASE statement
|
|
269
|
+
// 2. All bindings for email_opened_count CASE statement
|
|
270
|
+
// 3. All bindings for email_open_rate CASE statement
|
|
271
|
+
// 4. Member IDs for the WHERE IN clause
|
|
272
|
+
const bindings = [
|
|
273
|
+
...emailCountBindings,
|
|
274
|
+
...emailOpenedCountBindings,
|
|
275
|
+
...emailOpenRateBindings,
|
|
276
|
+
...memberIds
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
// Execute batched update with CASE statements
|
|
280
|
+
await db.knex.raw(`
|
|
281
|
+
UPDATE members
|
|
282
|
+
SET
|
|
283
|
+
email_count = CASE id ${emailCountCases.join(' ')} END,
|
|
284
|
+
email_opened_count = CASE id ${emailOpenedCountCases.join(' ')} END,
|
|
285
|
+
email_open_rate = CASE id ${emailOpenRateCases.join(' ')} END
|
|
286
|
+
WHERE id IN (${memberIds.map(() => '?').join(',')})
|
|
287
|
+
`, bindings);
|
|
204
288
|
}
|
|
205
289
|
};
|