ghost 6.54.1 → 6.55.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.
Files changed (247) hide show
  1. package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
  2. package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
  3. package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
  4. package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
  5. package/core/boot.js +3 -6
  6. package/core/built/admin/assets/{account-migration-BklF2I_I.js → account-migration-sjHLx5dy.js} +1 -1
  7. package/core/built/admin/assets/activitypub/{account-migration-CNovr6W-.js → account-migration-DoYXtnZ8.js} +4 -4
  8. package/core/built/admin/assets/activitypub/activitypub.js +1 -1
  9. package/core/built/admin/assets/activitypub/{bluesky-sharing-Ceslb1_M.js → bluesky-sharing-Bxeh6MKW.js} +4 -4
  10. package/core/built/admin/assets/activitypub/{dist-DHAjBcV3.js → dist-D8sFfdpC.js} +2 -2
  11. package/core/built/admin/assets/activitypub/{domain-C1qQvJ8C.js → domain-CWueFW6q.js} +5 -5
  12. package/core/built/admin/assets/activitypub/{edit-profile-BcH-jEfM.js → edit-profile-BGxPU7N5.js} +3 -3
  13. package/core/built/admin/assets/activitypub/{explore-AxwBh9dI.js → explore-DBK-u0Ch.js} +3 -3
  14. package/core/built/admin/assets/activitypub/{feed-BaOlbh7a.js → feed-Bcir5RQZ.js} +3 -3
  15. package/core/built/admin/assets/activitypub/{field-B7mYYQpN.js → field-BV-CIT3d.js} +2 -2
  16. package/core/built/admin/assets/activitypub/{inbox-vXez-5uJ.js → inbox-yC0nfvzl.js} +4 -4
  17. package/core/built/admin/assets/activitypub/{label-Chv5T8bw.js → label-DTRS9TDk.js} +2 -2
  18. package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +1 -0
  19. package/core/built/admin/assets/activitypub/{moderation-DtSM2i1I.js → moderation-Dwl9t0pb.js} +4 -4
  20. package/core/built/admin/assets/activitypub/{note-Dckp4S-2.js → note-B-zKsZ7r.js} +3 -3
  21. package/core/built/admin/assets/activitypub/{notifications-CG5T22vg.js → notifications-DjJZD54t.js} +3 -3
  22. package/core/built/admin/assets/activitypub/{preferences-D_799jdl.js → preferences-CoU8vs6d.js} +6 -6
  23. package/core/built/admin/assets/activitypub/{profile-D2JfXBQl.js → profile-BAsaJekx.js} +6 -6
  24. package/core/built/admin/assets/activitypub/{routes-CCHe2dQp.js → routes-CLH3OVTP.js} +18 -18
  25. package/core/built/admin/assets/activitypub/{settings-kx-9Enkz.js → settings-DA6pNJ-Q.js} +3 -3
  26. package/core/built/admin/assets/activitypub/{site-x4_IjVJ0.js → site-CQD42Pw5.js} +2 -2
  27. package/core/built/admin/assets/activitypub/{tabs-CyPRQQ1N.js → tabs-Cg0saow1.js} +7 -7
  28. package/core/built/admin/assets/activitypub/{use-reply-chain-data-BAB6fNGt.js → use-reply-chain-data-Br4aOWOh.js} +2 -2
  29. package/core/built/admin/assets/audience-DjEWNhp3.js +1 -0
  30. package/core/built/admin/assets/{automated-emails-of1MvVsv.js → automated-emails-DRy7qY-J.js} +2 -2
  31. package/core/built/admin/assets/{automations-DRFjW5fw.js → automations-Bx3VcFIL.js} +1 -1
  32. package/core/built/admin/assets/{automations-B7-y3dXt.js → automations-DhKeS_U5.js} +1 -1
  33. package/core/built/admin/assets/{bluesky-sharing-BbbjWdzs.js → bluesky-sharing-ClMd0-Ft.js} +1 -1
  34. package/core/built/admin/assets/{breadcrumb-B-M9Ip_2.js → breadcrumb-BD0U2ygg.js} +1 -1
  35. package/core/built/admin/assets/{chart-helpers-xiSsW8r0.js → chart-helpers-fjQKRVyY.js} +1 -1
  36. package/core/built/admin/assets/{checkbox-EhJKHFdZ.js → checkbox-CWa28lgO.js} +1 -1
  37. package/core/built/admin/assets/{chunk.361.3cdb584b973959e52ccc.js → chunk.481.6c7395a49484bb4583aa.js} +41 -38
  38. package/core/built/admin/assets/{chunk.524.8365cd9280405c0f6216.js → chunk.524.cffd399783a519e404a9.js} +4 -4
  39. package/core/built/admin/assets/{chunk.582.61d078937e6102769f2c.js → chunk.582.eebf0d0b2d202bb746a5.js} +6 -6
  40. package/core/built/admin/assets/{code-editor-view-CZtJFGhR.js → code-editor-view-CCr0rBWS.js} +1 -1
  41. package/core/built/admin/assets/comments-DyUSMUdj.js +1 -0
  42. package/core/built/admin/assets/{date-range-select-CTlyo8zW.js → date-range-select-BFp58EMk.js} +1 -1
  43. package/core/built/admin/assets/{disabled-sources-indicator-qs2GfLvl.js → disabled-sources-indicator-CUxMfiPa.js} +1 -1
  44. package/core/built/admin/assets/domain-C0bV2UV8.js +1 -0
  45. package/core/built/admin/assets/{dropzone-BmCXnw6J.js → dropzone-DlK-K2DQ.js} +1 -1
  46. package/core/built/admin/assets/{edit-profile-SGrJve5M.js → edit-profile-BKT-REJw.js} +1 -1
  47. package/core/built/admin/assets/editor-Dp865O-4.js +8 -0
  48. package/core/built/admin/assets/{explore-CUYLv9G9.js → explore-ClUpJZOp.js} +1 -1
  49. package/core/built/admin/assets/{feed-CPdJMttL.js → feed-DF92dUMP.js} +1 -1
  50. package/core/built/admin/assets/field-Do7lieBA.js +1 -0
  51. package/core/built/admin/assets/{filter-sources-Bieps75g.js → filter-sources-BV2rnpfA.js} +1 -1
  52. package/core/built/admin/assets/filters-CUXJ2Nm9.js +1 -0
  53. package/core/built/admin/assets/{filters-CuB-A7ma.js → filters-DfbIWc_l.js} +1 -1
  54. package/core/built/admin/assets/{ghost-3063eae29e6c9cc6d6e7131928a27da0.css → ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css} +1 -1
  55. package/core/built/admin/assets/{ghost-05f9a7182ed56903dba05878a5b8422b.js → ghost-c4057474943effc568c5659ccc4909d7.js} +190 -190
  56. package/core/built/admin/assets/{ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css → ghost-dark-db36d795d21c61426837df40af270966.css} +1 -1
  57. package/core/built/admin/assets/gift-link-modal-f9VIMNDv.js +1 -0
  58. package/core/built/admin/assets/{gift-links-nBxJjZiQ.js → gift-links-CsUhefWG.js} +1 -1
  59. package/core/built/admin/assets/growth-B3I6gNLm.js +1 -0
  60. package/core/built/admin/assets/growth-CVw1My4S.js +3 -0
  61. package/core/built/admin/assets/{hooks-BApwwBE0.js → hooks-CixO01lL.js} +1 -1
  62. package/core/built/admin/assets/{inbox-Xx1CdnfO.js → inbox-CFBSu4Ed.js} +13 -13
  63. package/core/built/admin/assets/index-9Cw8z3rA.css +1 -0
  64. package/core/built/admin/assets/index-BGG7NckO.js +67 -0
  65. package/core/built/admin/assets/{input-group-CGOXcy4l.js → input-group-BSgG_lVw.js} +1 -1
  66. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +974 -937
  67. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +28 -28
  68. package/core/built/admin/assets/koenig-lexical/style.css +1 -1
  69. package/core/built/admin/assets/{koenig-lexical-B61IALf5.js → koenig-lexical-f8vHRYvz.js} +28 -28
  70. package/core/built/admin/assets/{kpi-card-DCdlyaSS.js → kpi-card-DSap9rAy.js} +1 -1
  71. package/core/built/admin/assets/{kpi-card-BlZaG1Lo.js → kpi-card-DU0rn9Cj.js} +1 -1
  72. package/core/built/admin/assets/{kpi-helpers-DpAuS1Nb.js → kpi-helpers-Dz33AV6h.js} +1 -1
  73. package/core/built/admin/assets/{kpi-tabs-Dme44bQS.js → kpi-tabs-BtTTrKvJ.js} +1 -1
  74. package/core/built/admin/assets/{label-dSs5aepE.js → label-DS8Mi6G5.js} +1 -1
  75. package/core/built/admin/assets/label-picker-1DvSuLA1.js +1 -0
  76. package/core/built/admin/assets/labels-CDSUt4J_.js +1 -0
  77. package/core/built/admin/assets/layout-yQEi2qDM.js +1 -0
  78. package/core/built/admin/assets/{link-helpers-tOlxmuH5.js → link-helpers-7EYqvvHs.js} +1 -1
  79. package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +1 -0
  80. package/core/built/admin/assets/member-detail-BAq7gDaa.js +1 -0
  81. package/core/built/admin/assets/members-CQpVFJu4.js +10 -0
  82. package/core/built/admin/assets/modals-ReORvWnu.js +31 -0
  83. package/core/built/admin/assets/moderation-DySbgyUS.js +1 -0
  84. package/core/built/admin/assets/newsletter-CJQvtHqN.js +1 -0
  85. package/core/built/admin/assets/newsletters-B3QQXfGT.js +1 -0
  86. package/core/built/admin/assets/{newsletters-D1jbtV-h.js → newsletters-BXJbJgA-.js} +1 -1
  87. package/core/built/admin/assets/note-C8kxrz6w.js +1 -0
  88. package/core/built/admin/assets/notifications-CgtSfX7t.js +1 -0
  89. package/core/built/admin/assets/{offers-CrY6nSRR.js → offers-DEniVRD5.js} +1 -1
  90. package/core/built/admin/assets/onboarding-route-4qtziA9d.js +1 -0
  91. package/core/built/admin/assets/overview-Bf2sbznq.js +1 -0
  92. package/core/built/admin/assets/overview-CBfePxcr.js +1 -0
  93. package/core/built/admin/assets/{page-header-op0eN36I.js → page-header-B3Wb9Jqv.js} +1 -1
  94. package/core/built/admin/assets/{pages-BbNWFcGj.js → pages-B4goRPUn.js} +1 -1
  95. package/core/built/admin/assets/{post-analytics-WwRDEnrV.js → post-analytics-We-ko5Cb.js} +1 -1
  96. package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +1 -0
  97. package/core/built/admin/assets/post-analytics-provider-B6UynTGb.js +1 -0
  98. package/core/built/admin/assets/{post-share-modal-Dx-MEjhI.js → post-share-modal-DFfO93_0.js} +1 -1
  99. package/core/built/admin/assets/{posts-EWpZIjkr.js → posts-DhXE7b6r.js} +1 -1
  100. package/core/built/admin/assets/preferences-DJPfHEGu.js +5 -0
  101. package/core/built/admin/assets/profile-DqBQp-gT.js +1 -0
  102. package/core/built/admin/assets/{referrers-BCfFYSHc.js → referrers-pmJT4k3-.js} +1 -1
  103. package/core/built/admin/assets/{select-C1qDZnW4.js → select-C5_xEt5b.js} +1 -1
  104. package/core/built/admin/assets/settings-BwiVIXau.js +4 -0
  105. package/core/built/admin/assets/{settings-H6HxFgfI.js → settings-WXNeWyU1.js} +1 -1
  106. package/core/built/admin/assets/{share-modal-BJCadJyp.js → share-modal-DsgW3wbg.js} +1 -1
  107. package/core/built/admin/assets/social-urls-BQPFEk1_.js +8 -0
  108. package/core/built/admin/assets/{sort-button-DyYlfYp7.js → sort-button-Bb_FCl7e.js} +1 -1
  109. package/core/built/admin/assets/{source-icon-_ubRDbg_.js → source-icon-BeaRxUn9.js} +1 -1
  110. package/core/built/admin/assets/{stats-BQZMGMvn.js → stats-Bjj_kmJ1.js} +1 -1
  111. package/core/built/admin/assets/{stats-view-Doei-qCX.js → stats-view-BxPbW6K1.js} +1 -1
  112. package/core/built/admin/assets/{step-1-IKiLBPoC.js → step-1-B1e3-1F8.js} +1 -1
  113. package/core/built/admin/assets/{step-2-jJ41PsDN.js → step-2-C5WaeBJo.js} +1 -1
  114. package/core/built/admin/assets/{step-3-CaqFAEqf.js → step-3-B46iVPwu.js} +2 -2
  115. package/core/built/admin/assets/{switch-t-WyENU2.js → switch-z5Y6Uzt6.js} +1 -1
  116. package/core/built/admin/assets/{table-DxUL78jU.js → table-CB-4_HBJ.js} +1 -1
  117. package/core/built/admin/assets/tabs-CCEgKYzq.js +1 -0
  118. package/core/built/admin/assets/{tags-CNujGwkT.js → tags-Cg2VJGmG.js} +1 -1
  119. package/core/built/admin/assets/{tiers-BgYLWRJv.js → tiers-DYsa4DCo.js} +1 -1
  120. package/core/built/admin/assets/{toggle-group-BeEpQ69p.js → toggle-group-b3IlxvhV.js} +1 -1
  121. package/core/built/admin/assets/{topic-filter-Bb5MOD-1.js → topic-filter-DAFT87tQ.js} +1 -1
  122. package/core/built/admin/assets/{use-analytics-data-Cds6XDjg.js → use-analytics-data-S0FTd43s.js} +1 -1
  123. package/core/built/admin/assets/use-filter-params-Gpoq2E1L.js +1 -0
  124. package/core/built/admin/assets/{use-growth-stats-D8uf98-G.js → use-growth-stats-CIaiHOVb.js} +1 -1
  125. package/core/built/admin/assets/{use-post-referrers-CRtgyuXB.js → use-post-referrers-Cq2vBPrA.js} +1 -1
  126. package/core/built/admin/assets/{use-reply-chain-data-DbKYX50v.js → use-reply-chain-data-DBDRtGu-.js} +1 -1
  127. package/core/built/admin/assets/{use-simple-pagination-DC01kpo0.js → use-simple-pagination-CjYNxdwe.js} +1 -1
  128. package/core/built/admin/assets/{use-tinybird-query-C7bQRJWw.js → use-tinybird-query-falHAtuK.js} +1 -1
  129. package/core/built/admin/assets/{use-tinybird-token-Az4tam7_.js → use-tinybird-token-D7-8ND1y.js} +1 -1
  130. package/core/built/admin/assets/{users-CcSCQFcQ.js → users-Dkv8NQZe.js} +1 -1
  131. package/core/built/admin/assets/{virtual-list-Dhuh9fGx.js → virtual-list-CvnC9zF2.js} +1 -1
  132. package/core/built/admin/assets/web-B_5GZRvH.js +1 -0
  133. package/core/built/admin/assets/web-jFYWCKDn.js +2 -0
  134. package/core/built/admin/index.html +9 -9
  135. package/core/frontend/services/proxy.js +7 -8
  136. package/core/frontend/services/routing/router-manager.js +14 -14
  137. package/core/frontend/services/routing/static-routes-router.js +8 -10
  138. package/core/server/api/endpoints/automation-action-links.js +68 -0
  139. package/core/server/api/endpoints/automation-action-links.ts +46 -0
  140. package/core/server/api/endpoints/index.js +4 -0
  141. package/core/server/api/endpoints/settings.js +0 -2
  142. package/core/server/api/endpoints/utils/serializers/output/utils/url.js +11 -1
  143. package/core/server/data/migrations/versions/6.55/2026-07-27-11-25-44-add-to-hash-to-redirects.js +51 -0
  144. package/core/server/data/migrations/versions/6.55/2026-07-28-12-08-00-remove-routes-hash-from-settings.js +5 -0
  145. package/core/server/data/schema/commands.js +5 -0
  146. package/core/server/data/schema/default-settings/default-settings.json +0 -4
  147. package/core/server/data/schema/schema.js +5 -1
  148. package/core/server/services/automations/automations-api.js +15 -0
  149. package/core/server/services/automations/automations-api.ts +19 -0
  150. package/core/server/services/automations/automations-repository.ts +18 -0
  151. package/core/server/services/automations/database-automations-repository.js +109 -26
  152. package/core/server/services/automations/database-automations-repository.ts +110 -7
  153. package/core/server/services/automations/database-date.js +10 -0
  154. package/core/server/services/automations/database-date.ts +5 -0
  155. package/core/server/services/automations/poll.js +5 -2
  156. package/core/server/services/automations/poll.ts +7 -2
  157. package/core/server/services/automations/scheduler-poll-time.js +40 -0
  158. package/core/server/services/automations/scheduler-poll-time.ts +38 -0
  159. package/core/server/services/automations/service.js +15 -3
  160. package/core/server/services/automations/service.ts +16 -3
  161. package/core/server/services/comments/comments-service-emails.js +10 -7
  162. package/core/server/services/comments/comments-service.js +9 -4
  163. package/core/server/services/email-rendering/partials/email-wrapper.hbs +3 -1
  164. package/core/server/services/email-service/email-service-wrapper.js +6 -1
  165. package/core/server/services/email-service/email-templates/partials/styles.hbs +8 -0
  166. package/core/server/services/email-service/newsletter-email-event-storage.js +41 -10
  167. package/core/server/services/lib/dynamic-redirect-manager.js +12 -3
  168. package/core/server/services/link-redirection/link-redirect-repository.js +41 -4
  169. package/core/server/services/link-redirection/link-redirect.js +2 -0
  170. package/core/server/services/link-redirection/link-redirect.ts +3 -1
  171. package/core/server/services/link-redirection/link-redirects-service.js +38 -2
  172. package/core/server/services/link-tracking/click-event.js +3 -0
  173. package/core/server/services/link-tracking/index.js +38 -6
  174. package/core/server/services/link-tracking/link-click-repository.js +30 -9
  175. package/core/server/services/link-tracking/link-click-tracking-service.js +44 -3
  176. package/core/server/services/member-attribution/outbound-link-tagger.js +3 -1
  177. package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +19 -4
  178. package/core/server/services/member-welcome-emails/service.js +11 -3
  179. package/core/server/services/members/import-export/csv/formula.js +16 -0
  180. package/core/server/services/members/import-export/csv/formula.ts +14 -0
  181. package/core/server/services/members/import-export/csv/index.js +3 -1
  182. package/core/server/services/members/import-export/csv/index.ts +1 -0
  183. package/core/server/services/members/import-export/import/completion-email.js +22 -3
  184. package/core/server/services/members/import-export/import/completion-email.ts +29 -7
  185. package/core/server/services/members/import-export/import/importer.js +25 -4
  186. package/core/server/services/members/import-export/import/importer.ts +42 -4
  187. package/core/server/services/members/import-export/index.js +9 -0
  188. package/core/server/services/members/import-export/index.ts +20 -1
  189. package/core/server/services/members/service.js +8 -1
  190. package/core/server/services/members-custom-fields/values-service.js +19 -7
  191. package/core/server/services/members-custom-fields/values-service.ts +19 -7
  192. package/core/server/services/mentions/service.js +5 -1
  193. package/core/server/services/route-settings/activation-bridge.js +59 -49
  194. package/core/server/services/route-settings/activation-bridge.ts +91 -51
  195. package/core/server/services/route-settings/dynamic-routing-service.js +22 -51
  196. package/core/server/services/route-settings/index.js +1 -16
  197. package/core/server/services/settings/settings-service.js +0 -18
  198. package/core/server/services/url/url-service-facade.js +1 -1
  199. package/core/server/services/url/url-service-facade.ts +8 -1
  200. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  201. package/package.json +7 -7
  202. package/pnpm-lock.yaml +8 -8
  203. package/pnpm-workspace.yaml +5 -4
  204. package/core/built/admin/assets/Growth-BTP1LthE.js +0 -3
  205. package/core/built/admin/assets/Newsletters-CSEX0myc.js +0 -1
  206. package/core/built/admin/assets/Overview-BjACEsJF.js +0 -1
  207. package/core/built/admin/assets/Web-CxxxjCcv.js +0 -2
  208. package/core/built/admin/assets/activitypub/layout-CziktJXo.js +0 -1
  209. package/core/built/admin/assets/audience-X_XsZivI.js +0 -1
  210. package/core/built/admin/assets/comments-CvyXxOyN.js +0 -1
  211. package/core/built/admin/assets/constants-c-JNF9c2.js +0 -1
  212. package/core/built/admin/assets/domain-CPNXKVOV.js +0 -1
  213. package/core/built/admin/assets/editor-BOG2YPUm.js +0 -8
  214. package/core/built/admin/assets/field-D0XISCK2.js +0 -1
  215. package/core/built/admin/assets/filters-bXkkWro8.js +0 -1
  216. package/core/built/admin/assets/gift-link-modal-su8MsRdg.js +0 -1
  217. package/core/built/admin/assets/growth-Ba6S5FQX.js +0 -1
  218. package/core/built/admin/assets/index-D8PbzuT9.css +0 -1
  219. package/core/built/admin/assets/index-ukW_0Uie.js +0 -67
  220. package/core/built/admin/assets/label-picker-Uq91atex.js +0 -1
  221. package/core/built/admin/assets/labels-Cyayfd-e.js +0 -1
  222. package/core/built/admin/assets/layout-WUcz6tu1.js +0 -1
  223. package/core/built/admin/assets/member-custom-fields-NCzrMuba.js +0 -1
  224. package/core/built/admin/assets/member-detail-DPWDtYnN.js +0 -1
  225. package/core/built/admin/assets/members-B9rO7qr4.js +0 -10
  226. package/core/built/admin/assets/modals-CxRaan1J.js +0 -31
  227. package/core/built/admin/assets/moderation-D8zon60E.js +0 -1
  228. package/core/built/admin/assets/multi-select-combobox-1wYm3nle.js +0 -1
  229. package/core/built/admin/assets/newsletter-DbYMRxRS.js +0 -1
  230. package/core/built/admin/assets/note-PiWPnnrh.js +0 -1
  231. package/core/built/admin/assets/notifications-DEgqqB9G.js +0 -1
  232. package/core/built/admin/assets/onboarding-route-BzF6HeVn.js +0 -1
  233. package/core/built/admin/assets/overview-DixLeIqY.js +0 -1
  234. package/core/built/admin/assets/post-analytics-header-DsMoLx0R.js +0 -1
  235. package/core/built/admin/assets/post-analytics-provider-BJCPLMIb.js +0 -1
  236. package/core/built/admin/assets/preferences-D7-j6A4C.js +0 -5
  237. package/core/built/admin/assets/profile-Dl69QPp0.js +0 -1
  238. package/core/built/admin/assets/settings-BbmC_Iqq.js +0 -4
  239. package/core/built/admin/assets/social-urls-BPL1nADp.js +0 -8
  240. package/core/built/admin/assets/tabs-BQcJQ1ke.js +0 -1
  241. package/core/built/admin/assets/use-filter-params-DjXkhO54.js +0 -1
  242. package/core/built/admin/assets/web-LtHNiNBV.js +0 -1
  243. package/core/server/services/route-settings/default-settings-manager.js +0 -63
  244. package/core/server/services/route-settings/route-settings.js +0 -170
  245. package/core/server/services/route-settings/settings-loader.js +0 -60
  246. package/core/server/services/route-settings/settings-path-manager.js +0 -47
  247. package/core/server/services/route-settings/validate.js +0 -440
@@ -9,11 +9,13 @@ const tpl_1 = __importDefault(require("@tryghost/tpl"));
9
9
  const node_crypto_1 = __importDefault(require("node:crypto"));
10
10
  const bson_objectid_1 = __importDefault(require("bson-objectid"));
11
11
  const dequal_1 = require("dequal");
12
- const moment_1 = __importDefault(require("moment"));
12
+ // @ts-expect-error This module currently lacks type definitions.
13
+ const lexical_1 = __importDefault(require("../../lib/lexical"));
14
+ const url_utils_1 = __importDefault(require("../../../shared/url-utils"));
13
15
  const constants_1 = require("../member-welcome-emails/constants");
16
+ const database_date_1 = require("./database-date");
14
17
  const stale_lock_cutoff_1 = require("./stale-lock-cutoff");
15
18
  const HOUR_MS = 60 * 60 * 1000;
16
- const DATABASE_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
17
19
  const DEFAULT_WELCOME_EMAIL_AUTOMATIONS = [{
18
20
  name: 'Free member welcome flow',
19
21
  slug: constants_1.MEMBER_WELCOME_EMAIL_SLUGS.free
@@ -28,9 +30,6 @@ const messages = {
28
30
  defaultEmailDesignSettingNotFound: 'Default automated email design setting not found.'
29
31
  };
30
32
  const DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE = constants_1.DEFAULT_EMAIL_DESIGN_SETTING_SLUG;
31
- function toDatabaseDate(date) {
32
- return (0, moment_1.default)(date).format(DATABASE_DATE_FORMAT);
33
- }
34
33
  function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier }) {
35
34
  return {
36
35
  async browse() {
@@ -54,6 +53,33 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
54
53
  return await buildAutomation(trx, automation);
55
54
  });
56
55
  },
56
+ async getAutomationActionLinks(automationId, actionId) {
57
+ const action = await knex('automation_actions')
58
+ .select('id')
59
+ .where({
60
+ id: actionId,
61
+ automation_id: automationId
62
+ })
63
+ .whereNull('deleted_at')
64
+ .first();
65
+ if (!action) {
66
+ return null;
67
+ }
68
+ const rows = await knex('redirects as redirects')
69
+ .countDistinct({ clicked_count: 'members_click_events.member_id' })
70
+ .select(knex.raw('MIN(??) as ??', ['redirects.to', 'url']))
71
+ .innerJoin('automation_action_revisions as revisions', 'revisions.id', 'redirects.automation_action_revision_id')
72
+ .leftJoin('members_click_events', 'members_click_events.redirect_id', 'redirects.id')
73
+ .where('revisions.action_id', actionId)
74
+ .whereNotNull('redirects.to_hash')
75
+ .groupBy('redirects.to_hash')
76
+ .orderBy('clicked_count', 'desc')
77
+ .orderBy('url', 'asc');
78
+ return rows.map(row => ({
79
+ url: url_utils_1.default.transformReadyToAbsolute(row.url),
80
+ clicked_count: Number(row.clicked_count)
81
+ }));
82
+ },
57
83
  async edit(id, data) {
58
84
  return await knex.transaction(async (trx) => {
59
85
  const automation = await loadAutomation(trx, id);
@@ -64,7 +90,7 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
64
90
  const updatedAutomation = await updateAutomation(trx, {
65
91
  ...automation,
66
92
  status: data.status,
67
- updated_at: toDatabaseDate(now)
93
+ updated_at: (0, database_date_1.toDatabaseDate)(now)
68
94
  });
69
95
  await replaceAutomationGraph(trx, updatedAutomation.id, data.actions, data.edges);
70
96
  if (updatedAutomation.status === 'inactive') {
@@ -101,7 +127,7 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
101
127
  .update({
102
128
  email_sent_count: trx.raw('COALESCE(??, 0) + ?', ['email_sent_count', 1])
103
129
  });
104
- const now = toDatabaseDate(new Date());
130
+ const now = (0, database_date_1.toDatabaseDate)(new Date());
105
131
  await trx('automated_email_recipients').insert({
106
132
  id: (0, bson_objectid_1.default)().toHexString(),
107
133
  member_id: options.memberId,
@@ -162,6 +188,48 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
162
188
  });
163
189
  }
164
190
  });
191
+ },
192
+ async trackEmailClicked({ automationActionRevisionId, memberId, clickedAt }, { transacting } = {}) {
193
+ const trackClick = async (trx) => {
194
+ // Match recordEmailSent's lock order to avoid deadlocks.
195
+ await trx('automation_action_revisions')
196
+ .select('id')
197
+ .where({ id: automationActionRevisionId })
198
+ .forUpdate()
199
+ .first();
200
+ const recipient = await trx('automated_email_recipients')
201
+ .select('id', 'clicked_at')
202
+ .where({
203
+ automation_action_revision_id: automationActionRevisionId,
204
+ member_id: memberId,
205
+ track_clicks: true
206
+ })
207
+ .where('created_at', '<=', (0, database_date_1.toDatabaseDate)(clickedAt))
208
+ .orderBy('created_at', 'desc')
209
+ .orderBy('id', 'desc')
210
+ .forUpdate()
211
+ .first();
212
+ if (!recipient || recipient.clicked_at !== null) {
213
+ return;
214
+ }
215
+ const updated = await trx('automated_email_recipients')
216
+ .where({ id: recipient.id })
217
+ .whereNull('clicked_at')
218
+ .update({ clicked_at: clickedAt });
219
+ if (updated === 0) {
220
+ return;
221
+ }
222
+ await trx('automation_action_revisions')
223
+ .where({ id: automationActionRevisionId })
224
+ .update({
225
+ email_clicked_count: trx.raw('COALESCE(email_clicked_count, 0) + 1')
226
+ });
227
+ };
228
+ if (transacting) {
229
+ await trackClick(transacting);
230
+ return;
231
+ }
232
+ await knex.transaction(trackClick);
165
233
  }
166
234
  };
167
235
  }
@@ -194,7 +262,7 @@ async function ensureDefaultAutomations(trx) {
194
262
  }
195
263
  }
196
264
  async function ensureAutomation(trx, defaults) {
197
- const now = toDatabaseDate(new Date());
265
+ const now = (0, database_date_1.toDatabaseDate)(new Date());
198
266
  const id = (0, bson_objectid_1.default)().toHexString();
199
267
  await trx('automations')
200
268
  .insert({
@@ -233,7 +301,7 @@ async function ensureWelcomeEmailAction(trx, automationId) {
233
301
  if (!email) {
234
302
  return;
235
303
  }
236
- const now = toDatabaseDate(new Date());
304
+ const now = (0, database_date_1.toDatabaseDate)(new Date());
237
305
  const actionId = (0, bson_objectid_1.default)().toHexString();
238
306
  await insertActions(trx, [{
239
307
  id: actionId,
@@ -263,7 +331,7 @@ async function trigger(trx, options) {
263
331
  return;
264
332
  }
265
333
  const now = new Date();
266
- const nowString = toDatabaseDate(now);
334
+ const nowString = (0, database_date_1.toDatabaseDate)(now);
267
335
  const readyAt = getReadyAtForAction(firstAction, now, fakeWaitHoursMultiplier);
268
336
  const run = {
269
337
  id: (0, bson_objectid_1.default)().toHexString(),
@@ -282,14 +350,14 @@ async function trigger(trx, options) {
282
350
  });
283
351
  }
284
352
  async function insertRunStep(trx, { automationRunId, automationActionRevisionId, now, readyAt }) {
285
- const nowString = toDatabaseDate(now);
353
+ const nowString = (0, database_date_1.toDatabaseDate)(now);
286
354
  await trx('automation_run_steps').insert({
287
355
  id: (0, bson_objectid_1.default)().toHexString(),
288
356
  created_at: nowString,
289
357
  updated_at: nowString,
290
358
  automation_run_id: automationRunId,
291
359
  automation_action_revision_id: automationActionRevisionId,
292
- ready_at: toDatabaseDate(readyAt)
360
+ ready_at: (0, database_date_1.toDatabaseDate)(readyAt)
293
361
  });
294
362
  }
295
363
  async function fetchAndLockSteps(trx, limit) {
@@ -304,9 +372,9 @@ async function fetchAndLockSteps(trx, limit) {
304
372
  // 2. Try to lock those rows.
305
373
  // 3. Select any rows we successfully locked.
306
374
  const now = new Date();
307
- const nowString = toDatabaseDate(now);
375
+ const nowString = (0, database_date_1.toDatabaseDate)(now);
308
376
  const staleLockCutoff = (0, stale_lock_cutoff_1.getStaleLockCutoff)(now);
309
- const staleLockCutoffString = toDatabaseDate(staleLockCutoff);
377
+ const staleLockCutoffString = (0, database_date_1.toDatabaseDate)(staleLockCutoff);
310
378
  const lockId = node_crypto_1.default.randomUUID();
311
379
  // 1. Select up to `limit` candidate rows.
312
380
  const candidates = await trx('automation_run_steps')
@@ -374,7 +442,7 @@ async function findNextPendingReadyAt(trx, staleLockCutoff) {
374
442
  .where((builder) => {
375
443
  builder
376
444
  .whereNull('locked_by')
377
- .orWhere('locked_at', '<', toDatabaseDate(staleLockCutoff));
445
+ .orWhere('locked_at', '<', (0, database_date_1.toDatabaseDate)(staleLockCutoff));
378
446
  })
379
447
  .orderBy('ready_at')
380
448
  .first();
@@ -479,7 +547,7 @@ async function findNextActionRevision(trx, sourceActionId) {
479
547
  return row ?? null;
480
548
  }
481
549
  async function markStepTerminal(trx, step, status) {
482
- const nowString = toDatabaseDate(new Date());
550
+ const nowString = (0, database_date_1.toDatabaseDate)(new Date());
483
551
  return await updateStep(trx, step, {
484
552
  status,
485
553
  finished_at: nowString,
@@ -491,12 +559,12 @@ async function retryStep(trx, step, retryAt) {
491
559
  await markStepTerminal(trx, step, 'automation disabled');
492
560
  return false;
493
561
  }
494
- const nowString = toDatabaseDate(new Date());
562
+ const nowString = (0, database_date_1.toDatabaseDate)(new Date());
495
563
  return await updateStep(trx, step, {
496
564
  status: 'pending',
497
565
  started_at: null,
498
566
  finished_at: null,
499
- ready_at: toDatabaseDate(retryAt),
567
+ ready_at: (0, database_date_1.toDatabaseDate)(retryAt),
500
568
  updated_at: nowString
501
569
  });
502
570
  }
@@ -521,8 +589,8 @@ function getReadyAtForAction(action, now, fakeWaitHoursMultiplier) {
521
589
  }
522
590
  }
523
591
  async function cancelCancelablePendingStepsForAutomation(trx, automationId, now) {
524
- const nowString = toDatabaseDate(now);
525
- const staleLockCutoff = toDatabaseDate((0, stale_lock_cutoff_1.getStaleLockCutoff)(now));
592
+ const nowString = (0, database_date_1.toDatabaseDate)(now);
593
+ const staleLockCutoff = (0, database_date_1.toDatabaseDate)((0, stale_lock_cutoff_1.getStaleLockCutoff)(now));
526
594
  await trx('automation_run_steps')
527
595
  .update({
528
596
  status: 'automation disabled',
@@ -626,11 +694,11 @@ async function updateAutomation(trx, automation) {
626
694
  async function replaceAutomationGraph(trx, automationId, submittedActions, edges) {
627
695
  const existingActions = await loadAutomationActionRows(trx, automationId);
628
696
  const existingActionById = new Map(existingActions.map(action => [action.id, action]));
629
- const actions = await resolveEmailDesignSettingIds(trx, submittedActions);
697
+ const actions = (await resolveEmailDesignSettingIds(trx, submittedActions)).map(formatActionOnWrite);
630
698
  const submittedActionIds = new Set(actions.map(action => action.id));
631
699
  const actionIdsWithOwners = await loadActionIdsWithOwners(trx, [...submittedActionIds]);
632
700
  const latestRevisionByActionId = new Map((await loadLatestActionRevisions(trx, [...submittedActionIds])).map(revision => [revision.action_id, revision]));
633
- const now = toDatabaseDate(new Date());
701
+ const now = (0, database_date_1.toDatabaseDate)(new Date());
634
702
  const actionsToInsert = [];
635
703
  const revisionsToInsert = [];
636
704
  for (const action of actions) {
@@ -680,6 +748,21 @@ async function replaceAutomationGraph(trx, automationId, submittedActions, edges
680
748
  await deleteAutomationEdges(trx, automationId);
681
749
  await insertActionEdges(trx, edges);
682
750
  }
751
+ function formatActionOnWrite(action) {
752
+ if (action.type !== 'send_email') {
753
+ return action;
754
+ }
755
+ return {
756
+ ...action,
757
+ data: {
758
+ ...action.data,
759
+ email_lexical: url_utils_1.default.lexicalToTransformReady(action.data.email_lexical, {
760
+ nodes: lexical_1.default.nodes,
761
+ transformMap: lexical_1.default.urlTransformMap
762
+ })
763
+ }
764
+ };
765
+ }
683
766
  async function resolveEmailDesignSettingIds(trx, actions) {
684
767
  if (!actions.some(action => action.type === 'send_email' && action.data.email_design_setting_id === DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE)) {
685
768
  return [...actions];
@@ -793,14 +876,14 @@ async function insertActionRevisions(trx, revisions) {
793
876
  }
794
877
  function getNextRevisionCreatedAt(latestCreatedAt, requestedCreatedAt) {
795
878
  if (!latestCreatedAt) {
796
- return toDatabaseDate(requestedCreatedAt);
879
+ return (0, database_date_1.toDatabaseDate)(requestedCreatedAt);
797
880
  }
798
881
  const requestedTime = new Date(requestedCreatedAt).getTime();
799
882
  const latestTime = new Date(latestCreatedAt).getTime();
800
883
  if (requestedTime > latestTime) {
801
- return toDatabaseDate(requestedCreatedAt);
884
+ return (0, database_date_1.toDatabaseDate)(requestedCreatedAt);
802
885
  }
803
- return toDatabaseDate(new Date(latestTime + 1000));
886
+ return (0, database_date_1.toDatabaseDate)(new Date(latestTime + 1000));
804
887
  }
805
888
  function buildActionRevision(actionId, action, createdAt) {
806
889
  switch (action.type) {
@@ -946,7 +1029,7 @@ function buildActionPayload(row, stats) {
946
1029
  type: 'send_email',
947
1030
  data: {
948
1031
  email_subject: requireValue(row, 'email_subject'),
949
- email_lexical: requireValue(row, 'email_lexical'),
1032
+ email_lexical: url_utils_1.default.transformReadyToAbsolute(requireValue(row, 'email_lexical')),
950
1033
  email_design_setting_id: requireValue(row, 'email_design_setting_id')
951
1034
  },
952
1035
  stats: stats ?? EMPTY_EMAIL_STATS
@@ -4,7 +4,9 @@ import crypto from 'node:crypto';
4
4
  import ObjectId from 'bson-objectid';
5
5
  import {dequal} from 'dequal';
6
6
  import {type Knex} from 'knex';
7
- import moment from 'moment';
7
+ // @ts-expect-error This module currently lacks type definitions.
8
+ import lexicalLib from '../../lib/lexical';
9
+ import urlUtils from '../../../shared/url-utils';
8
10
  import {DEFAULT_EMAIL_DESIGN_SETTING_SLUG, MEMBER_WELCOME_EMAIL_SLUGS} from '../member-welcome-emails/constants';
9
11
  import type {
10
12
  AutomatedEmailEvents,
@@ -19,11 +21,11 @@ import type {
19
21
  EditAutomationData,
20
22
  Page
21
23
  } from './automations-repository';
24
+ import {toDatabaseDate} from './database-date';
22
25
  import {getStaleLockCutoff} from './stale-lock-cutoff';
23
26
  import type {ExclusifyUnion, ReadonlyDeep} from 'type-fest';
24
27
 
25
28
  const HOUR_MS = 60 * 60 * 1000;
26
- const DATABASE_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
27
29
  const DEFAULT_WELCOME_EMAIL_AUTOMATIONS = [{
28
30
  name: 'Free member welcome flow',
29
31
  slug: MEMBER_WELCOME_EMAIL_SLUGS.free
@@ -50,9 +52,6 @@ interface AutomationRow {
50
52
  updated_at: string;
51
53
  }
52
54
 
53
- function toDatabaseDate(date: Date | string): string {
54
- return moment(date).format(DATABASE_DATE_FORMAT);
55
- }
56
55
 
57
56
  interface ActionRow {
58
57
  id: string;
@@ -70,6 +69,11 @@ type ActionStatsRow = {
70
69
  email_opened_count: number | null;
71
70
  };
72
71
 
72
+ type ActionLinkRow = {
73
+ url: string;
74
+ clicked_count: string | number;
75
+ };
76
+
73
77
  type ActionRevisionRow = {
74
78
  action_id: string;
75
79
  created_at: string;
@@ -166,6 +170,37 @@ export function createDatabaseAutomationsRepository({
166
170
  });
167
171
  },
168
172
 
173
+ async getAutomationActionLinks(automationId, actionId) {
174
+ const action = await knex('automation_actions')
175
+ .select('id')
176
+ .where({
177
+ id: actionId,
178
+ automation_id: automationId
179
+ })
180
+ .whereNull('deleted_at')
181
+ .first();
182
+
183
+ if (!action) {
184
+ return null;
185
+ }
186
+
187
+ const rows = await knex('redirects as redirects')
188
+ .countDistinct({clicked_count: 'members_click_events.member_id'})
189
+ .select<ActionLinkRow[]>(knex.raw('MIN(??) as ??', ['redirects.to', 'url']))
190
+ .innerJoin('automation_action_revisions as revisions', 'revisions.id', 'redirects.automation_action_revision_id')
191
+ .leftJoin('members_click_events', 'members_click_events.redirect_id', 'redirects.id')
192
+ .where('revisions.action_id', actionId)
193
+ .whereNotNull('redirects.to_hash')
194
+ .groupBy('redirects.to_hash')
195
+ .orderBy('clicked_count', 'desc')
196
+ .orderBy('url', 'asc');
197
+
198
+ return rows.map(row => ({
199
+ url: urlUtils.transformReadyToAbsolute(row.url),
200
+ clicked_count: Number(row.clicked_count)
201
+ }));
202
+ },
203
+
169
204
  async edit(id: string, data: EditAutomationData): Promise<Automation | null> {
170
205
  return await knex.transaction(async (trx) => {
171
206
  const automation = await loadAutomation(trx, id);
@@ -304,6 +339,57 @@ export function createDatabaseAutomationsRepository({
304
339
  });
305
340
  }
306
341
  });
342
+ },
343
+
344
+ async trackEmailClicked({automationActionRevisionId, memberId, clickedAt}, {transacting} = {}) {
345
+ const trackClick = async (trx: Knex.Transaction) => {
346
+ // Match recordEmailSent's lock order to avoid deadlocks.
347
+ await trx('automation_action_revisions')
348
+ .select('id')
349
+ .where({id: automationActionRevisionId})
350
+ .forUpdate()
351
+ .first();
352
+
353
+ const recipient = await trx('automated_email_recipients')
354
+ .select('id', 'clicked_at')
355
+ .where({
356
+ automation_action_revision_id: automationActionRevisionId,
357
+ member_id: memberId,
358
+ track_clicks: true
359
+ })
360
+ .where('created_at', '<=', toDatabaseDate(clickedAt))
361
+ .orderBy('created_at', 'desc')
362
+ .orderBy('id', 'desc')
363
+ .forUpdate()
364
+ .first();
365
+
366
+ if (!recipient || recipient.clicked_at !== null) {
367
+ return;
368
+ }
369
+
370
+ const updated = await trx('automated_email_recipients')
371
+ .where({id: recipient.id})
372
+ .whereNull('clicked_at')
373
+ .update({clicked_at: clickedAt});
374
+
375
+ if (updated === 0) {
376
+ return;
377
+ }
378
+
379
+ await trx('automation_action_revisions')
380
+ .where({id: automationActionRevisionId})
381
+ .update({
382
+ email_clicked_count: trx.raw('COALESCE(email_clicked_count, 0) + 1')
383
+ });
384
+
385
+ };
386
+
387
+ if (transacting) {
388
+ await trackClick(transacting);
389
+ return;
390
+ }
391
+
392
+ await knex.transaction(trackClick);
307
393
  }
308
394
  };
309
395
  }
@@ -921,7 +1007,7 @@ async function updateAutomation(trx: Knex.Transaction, automation: AutomationRow
921
1007
  async function replaceAutomationGraph(trx: Knex.Transaction, automationId: string, submittedActions: AutomationAction[], edges: AutomationEdge[]): Promise<void> {
922
1008
  const existingActions = await loadAutomationActionRows(trx, automationId);
923
1009
  const existingActionById = new Map(existingActions.map(action => [action.id, action]));
924
- const actions = await resolveEmailDesignSettingIds(trx, submittedActions);
1010
+ const actions = (await resolveEmailDesignSettingIds(trx, submittedActions)).map(formatActionOnWrite);
925
1011
  const submittedActionIds = new Set(actions.map(action => action.id));
926
1012
  const actionIdsWithOwners = await loadActionIdsWithOwners(trx, [...submittedActionIds]);
927
1013
  const latestRevisionByActionId = new Map((await loadLatestActionRevisions(trx, [...submittedActionIds])).map(revision => [revision.action_id, revision]));
@@ -982,6 +1068,23 @@ async function replaceAutomationGraph(trx: Knex.Transaction, automationId: strin
982
1068
  await insertActionEdges(trx, edges);
983
1069
  }
984
1070
 
1071
+ function formatActionOnWrite(action: AutomationAction): AutomationAction {
1072
+ if (action.type !== 'send_email') {
1073
+ return action;
1074
+ }
1075
+
1076
+ return {
1077
+ ...action,
1078
+ data: {
1079
+ ...action.data,
1080
+ email_lexical: urlUtils.lexicalToTransformReady(action.data.email_lexical, {
1081
+ nodes: lexicalLib.nodes,
1082
+ transformMap: lexicalLib.urlTransformMap
1083
+ })
1084
+ }
1085
+ };
1086
+ }
1087
+
985
1088
  async function resolveEmailDesignSettingIds(trx: Knex.Transaction, actions: ReadonlyArray<AutomationAction>): Promise<AutomationAction[]> {
986
1089
  if (!actions.some(action => action.type === 'send_email' && action.data.email_design_setting_id === DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE)) {
987
1090
  return [...actions];
@@ -1312,7 +1415,7 @@ function buildActionPayload(row: ActionRow, stats: AutomationEmailStats | null):
1312
1415
  type: 'send_email',
1313
1416
  data: {
1314
1417
  email_subject: requireValue(row, 'email_subject'),
1315
- email_lexical: requireValue(row, 'email_lexical'),
1418
+ email_lexical: urlUtils.transformReadyToAbsolute(requireValue(row, 'email_lexical')),
1316
1419
  email_design_setting_id: requireValue(row, 'email_design_setting_id')
1317
1420
  },
1318
1421
  stats: stats ?? EMPTY_EMAIL_STATS
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.toDatabaseDate = void 0;
7
+ const moment_1 = __importDefault(require("moment"));
8
+ const DATABASE_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
9
+ const toDatabaseDate = (date) => (0, moment_1.default)(date).format(DATABASE_DATE_FORMAT);
10
+ exports.toDatabaseDate = toDatabaseDate;
@@ -0,0 +1,5 @@
1
+ import moment from 'moment';
2
+
3
+ const DATABASE_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
4
+
5
+ export const toDatabaseDate = (date: Date | string): string => moment(date).format(DATABASE_DATE_FORMAT);
@@ -12,6 +12,7 @@ const constants_2 = require("./constants");
12
12
  // @ts-expect-error Models currently lack type definitions.
13
13
  const models_1 = require("../../models");
14
14
  const settingsCache = require('../../../shared/settings-cache');
15
+ const labs = require('../../../shared/labs');
15
16
  const slugToMemberStatus = new Map(Object.entries(constants_1.MEMBER_WELCOME_EMAIL_SLUGS).map(([status, slug]) => [slug, status]));
16
17
  const hasUpdatesAndAnnouncementsEnabled = (member) => {
17
18
  const preference = member.get('enable_updates_and_announcements');
@@ -112,7 +113,7 @@ const processStep = async ({ automationsApi, memberWelcomeEmailService, schedule
112
113
  break;
113
114
  }
114
115
  memberWelcomeEmailService.init();
115
- const trackClicks = Boolean(settingsCache.get('email_track_clicks'));
116
+ const trackClicks = labs.isSet('automationAnalytics') && Boolean(settingsCache.get('email_track_clicks'));
116
117
  const trackOpens = Boolean(settingsCache.get('email_track_opens'));
117
118
  const sendResult = await memberWelcomeEmailService.api.sendAutomationEmail({
118
119
  email: {
@@ -126,7 +127,9 @@ const processStep = async ({ automationsApi, memberWelcomeEmailService, schedule
126
127
  uuid: member.get('uuid')
127
128
  },
128
129
  memberStatus,
129
- trackOpens
130
+ trackOpens,
131
+ trackClicks,
132
+ automationActionRevisionId: step.automation_action_revision_id
130
133
  });
131
134
  const mailgunMessageId = (0, mailgun_message_id_1.getMailgunMessageId)(sendResult);
132
135
  // Only Mailgun sends can produce open events for automation emails
@@ -8,6 +8,7 @@ import {MAX_ATTEMPTS, MAX_STEPS_PER_BATCH, RETRY_DELAY_MS} from './constants';
8
8
  import {Member} from '../../models';
9
9
 
10
10
  const settingsCache = require('../../../shared/settings-cache');
11
+ const labs = require('../../../shared/labs');
11
12
 
12
13
  type MemberWelcomeEmailService = {
13
14
  init: () => unknown;
@@ -25,6 +26,8 @@ type MemberWelcomeEmailService = {
25
26
  };
26
27
  memberStatus: 'free' | 'paid';
27
28
  trackOpens: boolean;
29
+ trackClicks: boolean;
30
+ automationActionRevisionId: string;
28
31
  }) => Promise<unknown>;
29
32
  };
30
33
  };
@@ -183,7 +186,7 @@ const processStep = async ({
183
186
  break;
184
187
  }
185
188
  memberWelcomeEmailService.init();
186
- const trackClicks = Boolean(settingsCache.get('email_track_clicks'));
189
+ const trackClicks = labs.isSet('automationAnalytics') && Boolean(settingsCache.get('email_track_clicks'));
187
190
  const trackOpens = Boolean(settingsCache.get('email_track_opens'));
188
191
  const sendResult = await memberWelcomeEmailService.api.sendAutomationEmail({
189
192
  email: {
@@ -197,7 +200,9 @@ const processStep = async ({
197
200
  uuid: member.get('uuid')
198
201
  },
199
202
  memberStatus,
200
- trackOpens
203
+ trackOpens,
204
+ trackClicks,
205
+ automationActionRevisionId: step.automation_action_revision_id
201
206
  });
202
207
  const mailgunMessageId = getMailgunMessageId(sendResult);
203
208
  // Only Mailgun sends can produce open events for automation emails
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getSchedulerPollTime = void 0;
4
+ const SCHEDULER_POLL_INTERVAL_MS = 15 * 60 * 1000;
5
+ /**
6
+ * An implementation of the [FNV-1a hash][0].
7
+ *
8
+ * We need something that generates per-site jitter within a 15-minute
9
+ * interval. We want a 32-bit number which we can modulo at the end.
10
+ *
11
+ * FNV-1a is good for this purpose because:
12
+ *
13
+ * - it is simple
14
+ * - it generates a 32-bit number
15
+ * - it is [more efficient than CRC32, MD5, and SHA][1], using less CPU and RAM
16
+ *
17
+ * This implementation assumes ASCII strings, which we expect callers to always
18
+ * use. If that assumption is ever violated, we'll compute incorrect jitter,
19
+ * which is acceptable.
20
+ *
21
+ * [0]: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV-1a_hash
22
+ * [1]: https://gist.github.com/EvanHahn/20d845c8157691d24977831159487950
23
+ */
24
+ const fnv1a32 = (asciiStr) => {
25
+ let hash = 0x811c9dc5;
26
+ for (let i = 0; i < asciiStr.length; i++) {
27
+ hash ^= asciiStr.charCodeAt(i);
28
+ hash = Math.imul(hash, 0x01000193);
29
+ }
30
+ return hash >>> 0;
31
+ };
32
+ /**
33
+ * Round the time up to a 15-minute interval with site-specific jitter.
34
+ */
35
+ const getSchedulerPollTime = (date, siteIdentifier) => {
36
+ const siteOffset = fnv1a32(siteIdentifier) % SCHEDULER_POLL_INTERVAL_MS;
37
+ const interval = Math.ceil((date.getTime() - siteOffset) / SCHEDULER_POLL_INTERVAL_MS);
38
+ return new Date(interval * SCHEDULER_POLL_INTERVAL_MS + siteOffset);
39
+ };
40
+ exports.getSchedulerPollTime = getSchedulerPollTime;
@@ -0,0 +1,38 @@
1
+ const SCHEDULER_POLL_INTERVAL_MS = 15 * 60 * 1000;
2
+
3
+ /**
4
+ * An implementation of the [FNV-1a hash][0].
5
+ *
6
+ * We need something that generates per-site jitter within a 15-minute
7
+ * interval. We want a 32-bit number which we can modulo at the end.
8
+ *
9
+ * FNV-1a is good for this purpose because:
10
+ *
11
+ * - it is simple
12
+ * - it generates a 32-bit number
13
+ * - it is [more efficient than CRC32, MD5, and SHA][1], using less CPU and RAM
14
+ *
15
+ * This implementation assumes ASCII strings, which we expect callers to always
16
+ * use. If that assumption is ever violated, we'll compute incorrect jitter,
17
+ * which is acceptable.
18
+ *
19
+ * [0]: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV-1a_hash
20
+ * [1]: https://gist.github.com/EvanHahn/20d845c8157691d24977831159487950
21
+ */
22
+ const fnv1a32 = (asciiStr: string): number => {
23
+ let hash = 0x811c9dc5;
24
+ for (let i = 0; i < asciiStr.length; i++) {
25
+ hash ^= asciiStr.charCodeAt(i);
26
+ hash = Math.imul(hash, 0x01000193);
27
+ }
28
+ return hash >>> 0;
29
+ };
30
+
31
+ /**
32
+ * Round the time up to a 15-minute interval with site-specific jitter.
33
+ */
34
+ export const getSchedulerPollTime = (date: Readonly<Date>, siteIdentifier: string): Date => {
35
+ const siteOffset = fnv1a32(siteIdentifier) % SCHEDULER_POLL_INTERVAL_MS;
36
+ const interval = Math.ceil((date.getTime() - siteOffset) / SCHEDULER_POLL_INTERVAL_MS);
37
+ return new Date(interval * SCHEDULER_POLL_INTERVAL_MS + siteOffset);
38
+ }