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.
- package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
- package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
- package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
- package/core/boot.js +3 -6
- package/core/built/admin/assets/{account-migration-BklF2I_I.js → account-migration-sjHLx5dy.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-CNovr6W-.js → account-migration-DoYXtnZ8.js} +4 -4
- package/core/built/admin/assets/activitypub/activitypub.js +1 -1
- package/core/built/admin/assets/activitypub/{bluesky-sharing-Ceslb1_M.js → bluesky-sharing-Bxeh6MKW.js} +4 -4
- package/core/built/admin/assets/activitypub/{dist-DHAjBcV3.js → dist-D8sFfdpC.js} +2 -2
- package/core/built/admin/assets/activitypub/{domain-C1qQvJ8C.js → domain-CWueFW6q.js} +5 -5
- package/core/built/admin/assets/activitypub/{edit-profile-BcH-jEfM.js → edit-profile-BGxPU7N5.js} +3 -3
- package/core/built/admin/assets/activitypub/{explore-AxwBh9dI.js → explore-DBK-u0Ch.js} +3 -3
- package/core/built/admin/assets/activitypub/{feed-BaOlbh7a.js → feed-Bcir5RQZ.js} +3 -3
- package/core/built/admin/assets/activitypub/{field-B7mYYQpN.js → field-BV-CIT3d.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-vXez-5uJ.js → inbox-yC0nfvzl.js} +4 -4
- package/core/built/admin/assets/activitypub/{label-Chv5T8bw.js → label-DTRS9TDk.js} +2 -2
- package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-DtSM2i1I.js → moderation-Dwl9t0pb.js} +4 -4
- package/core/built/admin/assets/activitypub/{note-Dckp4S-2.js → note-B-zKsZ7r.js} +3 -3
- package/core/built/admin/assets/activitypub/{notifications-CG5T22vg.js → notifications-DjJZD54t.js} +3 -3
- package/core/built/admin/assets/activitypub/{preferences-D_799jdl.js → preferences-CoU8vs6d.js} +6 -6
- package/core/built/admin/assets/activitypub/{profile-D2JfXBQl.js → profile-BAsaJekx.js} +6 -6
- package/core/built/admin/assets/activitypub/{routes-CCHe2dQp.js → routes-CLH3OVTP.js} +18 -18
- package/core/built/admin/assets/activitypub/{settings-kx-9Enkz.js → settings-DA6pNJ-Q.js} +3 -3
- package/core/built/admin/assets/activitypub/{site-x4_IjVJ0.js → site-CQD42Pw5.js} +2 -2
- package/core/built/admin/assets/activitypub/{tabs-CyPRQQ1N.js → tabs-Cg0saow1.js} +7 -7
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-BAB6fNGt.js → use-reply-chain-data-Br4aOWOh.js} +2 -2
- package/core/built/admin/assets/audience-DjEWNhp3.js +1 -0
- package/core/built/admin/assets/{automated-emails-of1MvVsv.js → automated-emails-DRy7qY-J.js} +2 -2
- package/core/built/admin/assets/{automations-DRFjW5fw.js → automations-Bx3VcFIL.js} +1 -1
- package/core/built/admin/assets/{automations-B7-y3dXt.js → automations-DhKeS_U5.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-BbbjWdzs.js → bluesky-sharing-ClMd0-Ft.js} +1 -1
- package/core/built/admin/assets/{breadcrumb-B-M9Ip_2.js → breadcrumb-BD0U2ygg.js} +1 -1
- package/core/built/admin/assets/{chart-helpers-xiSsW8r0.js → chart-helpers-fjQKRVyY.js} +1 -1
- package/core/built/admin/assets/{checkbox-EhJKHFdZ.js → checkbox-CWa28lgO.js} +1 -1
- package/core/built/admin/assets/{chunk.361.3cdb584b973959e52ccc.js → chunk.481.6c7395a49484bb4583aa.js} +41 -38
- package/core/built/admin/assets/{chunk.524.8365cd9280405c0f6216.js → chunk.524.cffd399783a519e404a9.js} +4 -4
- package/core/built/admin/assets/{chunk.582.61d078937e6102769f2c.js → chunk.582.eebf0d0b2d202bb746a5.js} +6 -6
- package/core/built/admin/assets/{code-editor-view-CZtJFGhR.js → code-editor-view-CCr0rBWS.js} +1 -1
- package/core/built/admin/assets/comments-DyUSMUdj.js +1 -0
- package/core/built/admin/assets/{date-range-select-CTlyo8zW.js → date-range-select-BFp58EMk.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-qs2GfLvl.js → disabled-sources-indicator-CUxMfiPa.js} +1 -1
- package/core/built/admin/assets/domain-C0bV2UV8.js +1 -0
- package/core/built/admin/assets/{dropzone-BmCXnw6J.js → dropzone-DlK-K2DQ.js} +1 -1
- package/core/built/admin/assets/{edit-profile-SGrJve5M.js → edit-profile-BKT-REJw.js} +1 -1
- package/core/built/admin/assets/editor-Dp865O-4.js +8 -0
- package/core/built/admin/assets/{explore-CUYLv9G9.js → explore-ClUpJZOp.js} +1 -1
- package/core/built/admin/assets/{feed-CPdJMttL.js → feed-DF92dUMP.js} +1 -1
- package/core/built/admin/assets/field-Do7lieBA.js +1 -0
- package/core/built/admin/assets/{filter-sources-Bieps75g.js → filter-sources-BV2rnpfA.js} +1 -1
- package/core/built/admin/assets/filters-CUXJ2Nm9.js +1 -0
- package/core/built/admin/assets/{filters-CuB-A7ma.js → filters-DfbIWc_l.js} +1 -1
- package/core/built/admin/assets/{ghost-3063eae29e6c9cc6d6e7131928a27da0.css → ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css} +1 -1
- package/core/built/admin/assets/{ghost-05f9a7182ed56903dba05878a5b8422b.js → ghost-c4057474943effc568c5659ccc4909d7.js} +190 -190
- package/core/built/admin/assets/{ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css → ghost-dark-db36d795d21c61426837df40af270966.css} +1 -1
- package/core/built/admin/assets/gift-link-modal-f9VIMNDv.js +1 -0
- package/core/built/admin/assets/{gift-links-nBxJjZiQ.js → gift-links-CsUhefWG.js} +1 -1
- package/core/built/admin/assets/growth-B3I6gNLm.js +1 -0
- package/core/built/admin/assets/growth-CVw1My4S.js +3 -0
- package/core/built/admin/assets/{hooks-BApwwBE0.js → hooks-CixO01lL.js} +1 -1
- package/core/built/admin/assets/{inbox-Xx1CdnfO.js → inbox-CFBSu4Ed.js} +13 -13
- package/core/built/admin/assets/index-9Cw8z3rA.css +1 -0
- package/core/built/admin/assets/index-BGG7NckO.js +67 -0
- package/core/built/admin/assets/{input-group-CGOXcy4l.js → input-group-BSgG_lVw.js} +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +974 -937
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +28 -28
- package/core/built/admin/assets/koenig-lexical/style.css +1 -1
- package/core/built/admin/assets/{koenig-lexical-B61IALf5.js → koenig-lexical-f8vHRYvz.js} +28 -28
- package/core/built/admin/assets/{kpi-card-DCdlyaSS.js → kpi-card-DSap9rAy.js} +1 -1
- package/core/built/admin/assets/{kpi-card-BlZaG1Lo.js → kpi-card-DU0rn9Cj.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-DpAuS1Nb.js → kpi-helpers-Dz33AV6h.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-Dme44bQS.js → kpi-tabs-BtTTrKvJ.js} +1 -1
- package/core/built/admin/assets/{label-dSs5aepE.js → label-DS8Mi6G5.js} +1 -1
- package/core/built/admin/assets/label-picker-1DvSuLA1.js +1 -0
- package/core/built/admin/assets/labels-CDSUt4J_.js +1 -0
- package/core/built/admin/assets/layout-yQEi2qDM.js +1 -0
- package/core/built/admin/assets/{link-helpers-tOlxmuH5.js → link-helpers-7EYqvvHs.js} +1 -1
- package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +1 -0
- package/core/built/admin/assets/member-detail-BAq7gDaa.js +1 -0
- package/core/built/admin/assets/members-CQpVFJu4.js +10 -0
- package/core/built/admin/assets/modals-ReORvWnu.js +31 -0
- package/core/built/admin/assets/moderation-DySbgyUS.js +1 -0
- package/core/built/admin/assets/newsletter-CJQvtHqN.js +1 -0
- package/core/built/admin/assets/newsletters-B3QQXfGT.js +1 -0
- package/core/built/admin/assets/{newsletters-D1jbtV-h.js → newsletters-BXJbJgA-.js} +1 -1
- package/core/built/admin/assets/note-C8kxrz6w.js +1 -0
- package/core/built/admin/assets/notifications-CgtSfX7t.js +1 -0
- package/core/built/admin/assets/{offers-CrY6nSRR.js → offers-DEniVRD5.js} +1 -1
- package/core/built/admin/assets/onboarding-route-4qtziA9d.js +1 -0
- package/core/built/admin/assets/overview-Bf2sbznq.js +1 -0
- package/core/built/admin/assets/overview-CBfePxcr.js +1 -0
- package/core/built/admin/assets/{page-header-op0eN36I.js → page-header-B3Wb9Jqv.js} +1 -1
- package/core/built/admin/assets/{pages-BbNWFcGj.js → pages-B4goRPUn.js} +1 -1
- package/core/built/admin/assets/{post-analytics-WwRDEnrV.js → post-analytics-We-ko5Cb.js} +1 -1
- package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +1 -0
- package/core/built/admin/assets/post-analytics-provider-B6UynTGb.js +1 -0
- package/core/built/admin/assets/{post-share-modal-Dx-MEjhI.js → post-share-modal-DFfO93_0.js} +1 -1
- package/core/built/admin/assets/{posts-EWpZIjkr.js → posts-DhXE7b6r.js} +1 -1
- package/core/built/admin/assets/preferences-DJPfHEGu.js +5 -0
- package/core/built/admin/assets/profile-DqBQp-gT.js +1 -0
- package/core/built/admin/assets/{referrers-BCfFYSHc.js → referrers-pmJT4k3-.js} +1 -1
- package/core/built/admin/assets/{select-C1qDZnW4.js → select-C5_xEt5b.js} +1 -1
- package/core/built/admin/assets/settings-BwiVIXau.js +4 -0
- package/core/built/admin/assets/{settings-H6HxFgfI.js → settings-WXNeWyU1.js} +1 -1
- package/core/built/admin/assets/{share-modal-BJCadJyp.js → share-modal-DsgW3wbg.js} +1 -1
- package/core/built/admin/assets/social-urls-BQPFEk1_.js +8 -0
- package/core/built/admin/assets/{sort-button-DyYlfYp7.js → sort-button-Bb_FCl7e.js} +1 -1
- package/core/built/admin/assets/{source-icon-_ubRDbg_.js → source-icon-BeaRxUn9.js} +1 -1
- package/core/built/admin/assets/{stats-BQZMGMvn.js → stats-Bjj_kmJ1.js} +1 -1
- package/core/built/admin/assets/{stats-view-Doei-qCX.js → stats-view-BxPbW6K1.js} +1 -1
- package/core/built/admin/assets/{step-1-IKiLBPoC.js → step-1-B1e3-1F8.js} +1 -1
- package/core/built/admin/assets/{step-2-jJ41PsDN.js → step-2-C5WaeBJo.js} +1 -1
- package/core/built/admin/assets/{step-3-CaqFAEqf.js → step-3-B46iVPwu.js} +2 -2
- package/core/built/admin/assets/{switch-t-WyENU2.js → switch-z5Y6Uzt6.js} +1 -1
- package/core/built/admin/assets/{table-DxUL78jU.js → table-CB-4_HBJ.js} +1 -1
- package/core/built/admin/assets/tabs-CCEgKYzq.js +1 -0
- package/core/built/admin/assets/{tags-CNujGwkT.js → tags-Cg2VJGmG.js} +1 -1
- package/core/built/admin/assets/{tiers-BgYLWRJv.js → tiers-DYsa4DCo.js} +1 -1
- package/core/built/admin/assets/{toggle-group-BeEpQ69p.js → toggle-group-b3IlxvhV.js} +1 -1
- package/core/built/admin/assets/{topic-filter-Bb5MOD-1.js → topic-filter-DAFT87tQ.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-Cds6XDjg.js → use-analytics-data-S0FTd43s.js} +1 -1
- package/core/built/admin/assets/use-filter-params-Gpoq2E1L.js +1 -0
- package/core/built/admin/assets/{use-growth-stats-D8uf98-G.js → use-growth-stats-CIaiHOVb.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-CRtgyuXB.js → use-post-referrers-Cq2vBPrA.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-DbKYX50v.js → use-reply-chain-data-DBDRtGu-.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-DC01kpo0.js → use-simple-pagination-CjYNxdwe.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-C7bQRJWw.js → use-tinybird-query-falHAtuK.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-Az4tam7_.js → use-tinybird-token-D7-8ND1y.js} +1 -1
- package/core/built/admin/assets/{users-CcSCQFcQ.js → users-Dkv8NQZe.js} +1 -1
- package/core/built/admin/assets/{virtual-list-Dhuh9fGx.js → virtual-list-CvnC9zF2.js} +1 -1
- package/core/built/admin/assets/web-B_5GZRvH.js +1 -0
- package/core/built/admin/assets/web-jFYWCKDn.js +2 -0
- package/core/built/admin/index.html +9 -9
- package/core/frontend/services/proxy.js +7 -8
- package/core/frontend/services/routing/router-manager.js +14 -14
- package/core/frontend/services/routing/static-routes-router.js +8 -10
- package/core/server/api/endpoints/automation-action-links.js +68 -0
- package/core/server/api/endpoints/automation-action-links.ts +46 -0
- package/core/server/api/endpoints/index.js +4 -0
- package/core/server/api/endpoints/settings.js +0 -2
- package/core/server/api/endpoints/utils/serializers/output/utils/url.js +11 -1
- package/core/server/data/migrations/versions/6.55/2026-07-27-11-25-44-add-to-hash-to-redirects.js +51 -0
- package/core/server/data/migrations/versions/6.55/2026-07-28-12-08-00-remove-routes-hash-from-settings.js +5 -0
- package/core/server/data/schema/commands.js +5 -0
- package/core/server/data/schema/default-settings/default-settings.json +0 -4
- package/core/server/data/schema/schema.js +5 -1
- package/core/server/services/automations/automations-api.js +15 -0
- package/core/server/services/automations/automations-api.ts +19 -0
- package/core/server/services/automations/automations-repository.ts +18 -0
- package/core/server/services/automations/database-automations-repository.js +109 -26
- package/core/server/services/automations/database-automations-repository.ts +110 -7
- package/core/server/services/automations/database-date.js +10 -0
- package/core/server/services/automations/database-date.ts +5 -0
- package/core/server/services/automations/poll.js +5 -2
- package/core/server/services/automations/poll.ts +7 -2
- package/core/server/services/automations/scheduler-poll-time.js +40 -0
- package/core/server/services/automations/scheduler-poll-time.ts +38 -0
- package/core/server/services/automations/service.js +15 -3
- package/core/server/services/automations/service.ts +16 -3
- package/core/server/services/comments/comments-service-emails.js +10 -7
- package/core/server/services/comments/comments-service.js +9 -4
- package/core/server/services/email-rendering/partials/email-wrapper.hbs +3 -1
- package/core/server/services/email-service/email-service-wrapper.js +6 -1
- package/core/server/services/email-service/email-templates/partials/styles.hbs +8 -0
- package/core/server/services/email-service/newsletter-email-event-storage.js +41 -10
- package/core/server/services/lib/dynamic-redirect-manager.js +12 -3
- package/core/server/services/link-redirection/link-redirect-repository.js +41 -4
- package/core/server/services/link-redirection/link-redirect.js +2 -0
- package/core/server/services/link-redirection/link-redirect.ts +3 -1
- package/core/server/services/link-redirection/link-redirects-service.js +38 -2
- package/core/server/services/link-tracking/click-event.js +3 -0
- package/core/server/services/link-tracking/index.js +38 -6
- package/core/server/services/link-tracking/link-click-repository.js +30 -9
- package/core/server/services/link-tracking/link-click-tracking-service.js +44 -3
- package/core/server/services/member-attribution/outbound-link-tagger.js +3 -1
- package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +19 -4
- package/core/server/services/member-welcome-emails/service.js +11 -3
- package/core/server/services/members/import-export/csv/formula.js +16 -0
- package/core/server/services/members/import-export/csv/formula.ts +14 -0
- package/core/server/services/members/import-export/csv/index.js +3 -1
- package/core/server/services/members/import-export/csv/index.ts +1 -0
- package/core/server/services/members/import-export/import/completion-email.js +22 -3
- package/core/server/services/members/import-export/import/completion-email.ts +29 -7
- package/core/server/services/members/import-export/import/importer.js +25 -4
- package/core/server/services/members/import-export/import/importer.ts +42 -4
- package/core/server/services/members/import-export/index.js +9 -0
- package/core/server/services/members/import-export/index.ts +20 -1
- package/core/server/services/members/service.js +8 -1
- package/core/server/services/members-custom-fields/values-service.js +19 -7
- package/core/server/services/members-custom-fields/values-service.ts +19 -7
- package/core/server/services/mentions/service.js +5 -1
- package/core/server/services/route-settings/activation-bridge.js +59 -49
- package/core/server/services/route-settings/activation-bridge.ts +91 -51
- package/core/server/services/route-settings/dynamic-routing-service.js +22 -51
- package/core/server/services/route-settings/index.js +1 -16
- package/core/server/services/settings/settings-service.js +0 -18
- package/core/server/services/url/url-service-facade.js +1 -1
- package/core/server/services/url/url-service-facade.ts +8 -1
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/package.json +7 -7
- package/pnpm-lock.yaml +8 -8
- package/pnpm-workspace.yaml +5 -4
- package/core/built/admin/assets/Growth-BTP1LthE.js +0 -3
- package/core/built/admin/assets/Newsletters-CSEX0myc.js +0 -1
- package/core/built/admin/assets/Overview-BjACEsJF.js +0 -1
- package/core/built/admin/assets/Web-CxxxjCcv.js +0 -2
- package/core/built/admin/assets/activitypub/layout-CziktJXo.js +0 -1
- package/core/built/admin/assets/audience-X_XsZivI.js +0 -1
- package/core/built/admin/assets/comments-CvyXxOyN.js +0 -1
- package/core/built/admin/assets/constants-c-JNF9c2.js +0 -1
- package/core/built/admin/assets/domain-CPNXKVOV.js +0 -1
- package/core/built/admin/assets/editor-BOG2YPUm.js +0 -8
- package/core/built/admin/assets/field-D0XISCK2.js +0 -1
- package/core/built/admin/assets/filters-bXkkWro8.js +0 -1
- package/core/built/admin/assets/gift-link-modal-su8MsRdg.js +0 -1
- package/core/built/admin/assets/growth-Ba6S5FQX.js +0 -1
- package/core/built/admin/assets/index-D8PbzuT9.css +0 -1
- package/core/built/admin/assets/index-ukW_0Uie.js +0 -67
- package/core/built/admin/assets/label-picker-Uq91atex.js +0 -1
- package/core/built/admin/assets/labels-Cyayfd-e.js +0 -1
- package/core/built/admin/assets/layout-WUcz6tu1.js +0 -1
- package/core/built/admin/assets/member-custom-fields-NCzrMuba.js +0 -1
- package/core/built/admin/assets/member-detail-DPWDtYnN.js +0 -1
- package/core/built/admin/assets/members-B9rO7qr4.js +0 -10
- package/core/built/admin/assets/modals-CxRaan1J.js +0 -31
- package/core/built/admin/assets/moderation-D8zon60E.js +0 -1
- package/core/built/admin/assets/multi-select-combobox-1wYm3nle.js +0 -1
- package/core/built/admin/assets/newsletter-DbYMRxRS.js +0 -1
- package/core/built/admin/assets/note-PiWPnnrh.js +0 -1
- package/core/built/admin/assets/notifications-DEgqqB9G.js +0 -1
- package/core/built/admin/assets/onboarding-route-BzF6HeVn.js +0 -1
- package/core/built/admin/assets/overview-DixLeIqY.js +0 -1
- package/core/built/admin/assets/post-analytics-header-DsMoLx0R.js +0 -1
- package/core/built/admin/assets/post-analytics-provider-BJCPLMIb.js +0 -1
- package/core/built/admin/assets/preferences-D7-j6A4C.js +0 -5
- package/core/built/admin/assets/profile-Dl69QPp0.js +0 -1
- package/core/built/admin/assets/settings-BbmC_Iqq.js +0 -4
- package/core/built/admin/assets/social-urls-BPL1nADp.js +0 -8
- package/core/built/admin/assets/tabs-BQcJQ1ke.js +0 -1
- package/core/built/admin/assets/use-filter-params-DjXkhO54.js +0 -1
- package/core/built/admin/assets/web-LtHNiNBV.js +0 -1
- package/core/server/services/route-settings/default-settings-manager.js +0 -63
- package/core/server/services/route-settings/route-settings.js +0 -170
- package/core/server/services/route-settings/settings-loader.js +0 -60
- package/core/server/services/route-settings/settings-path-manager.js +0 -47
- 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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
+
}
|