ghost 6.8.1 → 6.9.1
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.1.tgz +0 -0
- package/content/themes/source/package.json +1 -1
- 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-C0HG4CwA.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-Cjl9AHTm.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-SxxA3jSX.mjs → index-Cz3Y1eRN.mjs} +5 -5
- package/core/built/admin/assets/admin-x-settings/{index-Hwd1HJ0-.mjs → index-DRpMCs_c.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-yhgQVHax.mjs → modals-DXD3MDLp.mjs} +8814 -8818
- package/core/built/admin/assets/{chunk.524.bc2b771f38286b4a51ed.js → chunk.524.56454d8add89b041f4df.js} +5 -5
- package/core/built/admin/assets/{chunk.582.a28f953895ae17fb3b24.js → chunk.582.a4916bd533becb58fd9c.js} +8 -8
- 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 +123 -22
- 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.');
|
|
@@ -330,22 +330,56 @@ module.exports = class EmailAnalyticsService {
|
|
|
330
330
|
|
|
331
331
|
// We keep the processing result here, so we also have a result in case of failures
|
|
332
332
|
let processingResult = new EventProcessingResult();
|
|
333
|
+
// Track cumulative event counts separately since processingResult gets reset during intermediate aggregations
|
|
334
|
+
const cumulativeResult = new EventProcessingResult();
|
|
335
|
+
// Track all unique emailIds and memberIds that need aggregation
|
|
336
|
+
const allEmailIds = new Set();
|
|
337
|
+
const allMemberIds = new Set();
|
|
333
338
|
let error = null;
|
|
334
339
|
|
|
335
340
|
/**
|
|
336
341
|
* Process a batch of events
|
|
337
342
|
* @param {Array<Object>} events - Array of event objects to process
|
|
338
|
-
* @param {EventProcessingResult} processingResult - Object to store the processing results
|
|
339
|
-
* @param {FetchData} fetchData - Object containing fetch operation data
|
|
340
343
|
* @returns {Promise<void>}
|
|
341
344
|
*/
|
|
342
345
|
const processBatch = async (events) => {
|
|
343
346
|
// Even if the fetching is interrupted because of an error, we still store the last event timestamp
|
|
344
347
|
const processingStart = Date.now();
|
|
348
|
+
// Capture the state before processing to calculate delta
|
|
349
|
+
const beforeCounts = {
|
|
350
|
+
opened: processingResult.opened,
|
|
351
|
+
delivered: processingResult.delivered,
|
|
352
|
+
temporaryFailed: processingResult.temporaryFailed,
|
|
353
|
+
permanentFailed: processingResult.permanentFailed,
|
|
354
|
+
unsubscribed: processingResult.unsubscribed,
|
|
355
|
+
complained: processingResult.complained,
|
|
356
|
+
unhandled: processingResult.unhandled,
|
|
357
|
+
unprocessable: processingResult.unprocessable
|
|
358
|
+
};
|
|
359
|
+
const beforeEmailIds = new Set(processingResult.emailIds);
|
|
360
|
+
const beforeMemberIds = new Set(processingResult.memberIds);
|
|
361
|
+
|
|
345
362
|
await this.processEventBatch(events, processingResult, fetchData);
|
|
346
363
|
processingTimeMs += (Date.now() - processingStart);
|
|
347
364
|
eventCount += events.length;
|
|
348
365
|
|
|
366
|
+
// Calculate delta (only new counts from this batch) and accumulate for final reporting
|
|
367
|
+
const batchDelta = new EventProcessingResult({
|
|
368
|
+
opened: processingResult.opened - beforeCounts.opened,
|
|
369
|
+
delivered: processingResult.delivered - beforeCounts.delivered,
|
|
370
|
+
temporaryFailed: processingResult.temporaryFailed - beforeCounts.temporaryFailed,
|
|
371
|
+
permanentFailed: processingResult.permanentFailed - beforeCounts.permanentFailed,
|
|
372
|
+
unsubscribed: processingResult.unsubscribed - beforeCounts.unsubscribed,
|
|
373
|
+
complained: processingResult.complained - beforeCounts.complained,
|
|
374
|
+
unhandled: processingResult.unhandled - beforeCounts.unhandled,
|
|
375
|
+
unprocessable: processingResult.unprocessable - beforeCounts.unprocessable,
|
|
376
|
+
emailIds: processingResult.emailIds.filter(id => !beforeEmailIds.has(id)),
|
|
377
|
+
memberIds: processingResult.memberIds.filter(id => !beforeMemberIds.has(id))
|
|
378
|
+
});
|
|
379
|
+
cumulativeResult.merge(batchDelta);
|
|
380
|
+
batchDelta.emailIds.forEach(id => allEmailIds.add(id));
|
|
381
|
+
batchDelta.memberIds.forEach(id => allMemberIds.add(id));
|
|
382
|
+
|
|
349
383
|
// Every 5 minutes or 5000 members we do an aggregation and clear the processingResult
|
|
350
384
|
// Otherwise we need to loop a lot of members afterwards, and this takes too long without updating the stat counts in between
|
|
351
385
|
if ((Date.now() - lastAggregation > 5 * 60 * 1000 || processingResult.memberIds.length > 5000) && eventCount > 0) {
|
|
@@ -356,6 +390,9 @@ module.exports = class EmailAnalyticsService {
|
|
|
356
390
|
await this.aggregateStats(processingResult, includeOpenedEvents);
|
|
357
391
|
aggregationTimeMs += (Date.now() - aggregationStart);
|
|
358
392
|
lastAggregation = Date.now();
|
|
393
|
+
// Remove aggregated emailIds and memberIds from tracking sets to avoid re-aggregating at the end
|
|
394
|
+
processingResult.emailIds.forEach(id => allEmailIds.delete(id));
|
|
395
|
+
processingResult.memberIds.forEach(id => allMemberIds.delete(id));
|
|
359
396
|
processingResult = new EventProcessingResult();
|
|
360
397
|
} catch (err) {
|
|
361
398
|
logging.error('[EmailAnalytics] Error while aggregating stats');
|
|
@@ -386,10 +423,20 @@ module.exports = class EmailAnalyticsService {
|
|
|
386
423
|
}
|
|
387
424
|
}
|
|
388
425
|
|
|
389
|
-
|
|
426
|
+
// Final aggregation: aggregate any remaining events and ensure all emailIds are aggregated
|
|
427
|
+
// We need to aggregate all unique emailIds to ensure the emails table is updated
|
|
428
|
+
const finalEmailIds = Array.from(new Set([...processingResult.emailIds, ...allEmailIds]));
|
|
429
|
+
const finalMemberIds = Array.from(new Set([...processingResult.memberIds, ...allMemberIds]));
|
|
430
|
+
|
|
431
|
+
if (finalMemberIds.length > 0 || finalEmailIds.length > 0) {
|
|
390
432
|
try {
|
|
391
433
|
const aggregationStart = Date.now();
|
|
392
|
-
|
|
434
|
+
// Create a result object with all emailIds and memberIds for final aggregation
|
|
435
|
+
const finalAggregationResult = {
|
|
436
|
+
emailIds: finalEmailIds,
|
|
437
|
+
memberIds: finalMemberIds
|
|
438
|
+
};
|
|
439
|
+
await this.aggregateStats(finalAggregationResult, includeOpenedEvents);
|
|
393
440
|
aggregationTimeMs += (Date.now() - aggregationStart);
|
|
394
441
|
} catch (err) {
|
|
395
442
|
logging.error('[EmailAnalytics] Error while aggregating stats');
|
|
@@ -404,7 +451,7 @@ module.exports = class EmailAnalyticsService {
|
|
|
404
451
|
// Small trick: if reached the end of new events, we are going to keep
|
|
405
452
|
// fetching the same events because 'begin' won't change
|
|
406
453
|
// So if we didn't have errors while fetching, and total events < maxEvents, increase lastEventTimestamp with one second
|
|
407
|
-
if (!error && eventCount > 0 &&
|
|
454
|
+
if (!error && eventCount > 0 && fetchData.lastEventTimestamp && fetchData.lastEventTimestamp.getTime() < Date.now() - 2000) {
|
|
408
455
|
// set the data on the db so we can store it for fetching after reboot
|
|
409
456
|
await this.queries.setJobTimestamp(fetchData.jobName, 'finished', new Date(fetchData.lastEventTimestamp.getTime()));
|
|
410
457
|
// increment and store in local memory
|
|
@@ -425,7 +472,7 @@ module.exports = class EmailAnalyticsService {
|
|
|
425
472
|
apiPollingTimeMs,
|
|
426
473
|
processingTimeMs,
|
|
427
474
|
aggregationTimeMs,
|
|
428
|
-
result:
|
|
475
|
+
result: cumulativeResult
|
|
429
476
|
};
|
|
430
477
|
}
|
|
431
478
|
|
|
@@ -437,26 +484,55 @@ module.exports = class EmailAnalyticsService {
|
|
|
437
484
|
* @returns {Promise<void>}
|
|
438
485
|
*/
|
|
439
486
|
async processEventBatch(events, result, fetchData) {
|
|
440
|
-
|
|
441
|
-
|
|
487
|
+
const useBatchProcessing = this.config.get('emailAnalytics:batchProcessing');
|
|
488
|
+
|
|
489
|
+
if (useBatchProcessing) {
|
|
490
|
+
// Batched mode: pre-fetch all recipients, then process events using cache
|
|
491
|
+
const emailIdentifications = events.map(event => ({
|
|
492
|
+
emailId: event.emailId,
|
|
493
|
+
providerId: event.providerId,
|
|
494
|
+
email: event.recipientEmail
|
|
495
|
+
}));
|
|
442
496
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
497
|
+
const recipientCache = await this.eventProcessor.batchGetRecipients(emailIdentifications);
|
|
498
|
+
|
|
499
|
+
for (const event of events) {
|
|
500
|
+
const batchResult = await this.processEvent(event, recipientCache);
|
|
501
|
+
|
|
502
|
+
// Save last event timestamp
|
|
503
|
+
if (!fetchData.lastEventTimestamp || (event.timestamp && event.timestamp > fetchData.lastEventTimestamp)) {
|
|
504
|
+
fetchData.lastEventTimestamp = event.timestamp;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
result.merge(batchResult);
|
|
446
508
|
}
|
|
447
509
|
|
|
448
|
-
|
|
510
|
+
// Flush all batched updates to the database
|
|
511
|
+
await this.eventProcessor.flushBatchedUpdates();
|
|
512
|
+
} else {
|
|
513
|
+
// Sequential mode: process events one by one (original behavior)
|
|
514
|
+
for (const event of events) {
|
|
515
|
+
const batchResult = await this.processEvent(event);
|
|
516
|
+
|
|
517
|
+
// Save last event timestamp
|
|
518
|
+
if (!fetchData.lastEventTimestamp || (event.timestamp && event.timestamp > fetchData.lastEventTimestamp)) {
|
|
519
|
+
fetchData.lastEventTimestamp = event.timestamp;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
result.merge(batchResult);
|
|
523
|
+
}
|
|
449
524
|
}
|
|
450
525
|
}
|
|
451
526
|
|
|
452
527
|
/**
|
|
453
528
|
*
|
|
454
529
|
* @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
|
|
530
|
+
* @param {Map<string, any>} [recipientCache] Optional cache for batched processing
|
|
455
531
|
* @returns {Promise<EventProcessingResult>}
|
|
456
532
|
*/
|
|
457
|
-
async processEvent(event) {
|
|
533
|
+
async processEvent(event, recipientCache) {
|
|
458
534
|
if (event.type === 'delivered') {
|
|
459
|
-
const recipient = await this.eventProcessor.handleDelivered({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp);
|
|
535
|
+
const recipient = await this.eventProcessor.handleDelivered({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp, recipientCache);
|
|
460
536
|
|
|
461
537
|
if (recipient) {
|
|
462
538
|
return new EventProcessingResult({
|
|
@@ -470,7 +546,7 @@ module.exports = class EmailAnalyticsService {
|
|
|
470
546
|
}
|
|
471
547
|
|
|
472
548
|
if (event.type === 'opened') {
|
|
473
|
-
const recipient = await this.eventProcessor.handleOpened({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp);
|
|
549
|
+
const recipient = await this.eventProcessor.handleOpened({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp, recipientCache);
|
|
474
550
|
|
|
475
551
|
if (recipient) {
|
|
476
552
|
return new EventProcessingResult({
|
|
@@ -485,7 +561,7 @@ module.exports = class EmailAnalyticsService {
|
|
|
485
561
|
|
|
486
562
|
if (event.type === 'failed') {
|
|
487
563
|
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});
|
|
564
|
+
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
565
|
|
|
490
566
|
if (recipient) {
|
|
491
567
|
return new EventProcessingResult({
|
|
@@ -497,7 +573,7 @@ module.exports = class EmailAnalyticsService {
|
|
|
497
573
|
|
|
498
574
|
return new EventProcessingResult({unprocessable: 1});
|
|
499
575
|
} 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});
|
|
576
|
+
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
577
|
|
|
502
578
|
if (recipient) {
|
|
503
579
|
return new EventProcessingResult({
|
|
@@ -512,7 +588,7 @@ module.exports = class EmailAnalyticsService {
|
|
|
512
588
|
}
|
|
513
589
|
|
|
514
590
|
if (event.type === 'unsubscribed') {
|
|
515
|
-
const recipient = await this.eventProcessor.handleUnsubscribed({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp);
|
|
591
|
+
const recipient = await this.eventProcessor.handleUnsubscribed({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp, recipientCache);
|
|
516
592
|
|
|
517
593
|
if (recipient) {
|
|
518
594
|
return new EventProcessingResult({
|
|
@@ -526,7 +602,7 @@ module.exports = class EmailAnalyticsService {
|
|
|
526
602
|
}
|
|
527
603
|
|
|
528
604
|
if (event.type === 'complained') {
|
|
529
|
-
const recipient = await this.eventProcessor.handleComplained({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp);
|
|
605
|
+
const recipient = await this.eventProcessor.handleComplained({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp, recipientCache);
|
|
530
606
|
|
|
531
607
|
if (recipient) {
|
|
532
608
|
return new EventProcessingResult({
|
|
@@ -547,15 +623,31 @@ module.exports = class EmailAnalyticsService {
|
|
|
547
623
|
* @param {boolean} includeOpenedEvents
|
|
548
624
|
*/
|
|
549
625
|
async aggregateStats({emailIds = [], memberIds = []}, includeOpenedEvents = true) {
|
|
626
|
+
const useBatchProcessing = this.config.get('emailAnalytics:batchProcessing');
|
|
627
|
+
|
|
550
628
|
for (const emailId of emailIds) {
|
|
551
629
|
await this.aggregateEmailStats(emailId, includeOpenedEvents);
|
|
552
630
|
}
|
|
553
631
|
|
|
554
632
|
// @ts-expect-error
|
|
555
633
|
const memberMetric = this.prometheusClient?.getMetric('email_analytics_aggregate_member_stats_count');
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
634
|
+
|
|
635
|
+
if (useBatchProcessing) {
|
|
636
|
+
// Batched mode: process 100 members at a time
|
|
637
|
+
logging.info(`[EmailAnalytics] Aggregating stats for ${memberIds.length} members using BATCHED mode (batch size: 100)`);
|
|
638
|
+
const BATCH_SIZE = 100;
|
|
639
|
+
for (let i = 0; i < memberIds.length; i += BATCH_SIZE) {
|
|
640
|
+
const batch = memberIds.slice(i, i + BATCH_SIZE);
|
|
641
|
+
await this.aggregateMemberStatsBatch(batch);
|
|
642
|
+
memberMetric?.inc(batch.length);
|
|
643
|
+
}
|
|
644
|
+
} else {
|
|
645
|
+
// Sequential mode: process one member at a time
|
|
646
|
+
logging.info(`[EmailAnalytics] Aggregating stats for ${memberIds.length} members using SEQUENTIAL mode`);
|
|
647
|
+
for (const memberId of memberIds) {
|
|
648
|
+
await this.aggregateMemberStats(memberId);
|
|
649
|
+
memberMetric?.inc();
|
|
650
|
+
}
|
|
559
651
|
}
|
|
560
652
|
}
|
|
561
653
|
|
|
@@ -577,4 +669,13 @@ module.exports = class EmailAnalyticsService {
|
|
|
577
669
|
async aggregateMemberStats(memberId) {
|
|
578
670
|
return this.queries.aggregateMemberStats(memberId);
|
|
579
671
|
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Aggregate member stats for multiple members in a batch.
|
|
675
|
+
* @param {string[]} memberIds - Array of member IDs to aggregate stats for.
|
|
676
|
+
* @returns {Promise<void>}
|
|
677
|
+
*/
|
|
678
|
+
async aggregateMemberStatsBatch(memberIds) {
|
|
679
|
+
return this.queries.aggregateMemberStatsBatch(memberIds);
|
|
680
|
+
}
|
|
580
681
|
};
|
|
@@ -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
|
};
|