ghost 6.57.0 → 6.57.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tryghost-adapter-base-scheduling-0.2.1.tgz +0 -0
- package/components/tryghost-kg-converters-1.2.4.tgz +0 -0
- package/components/tryghost-kg-default-cards-10.3.4.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.1.5.tgz +0 -0
- package/components/tryghost-kg-default-transforms-1.3.3.tgz +0 -0
- package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
- package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
- package/core/built/admin/assets/{about-C7O-AgRZ.js → about-BNojRaxf.js} +1 -1
- package/core/built/admin/assets/{account-migration-Cv_ol1ko.js → account-migration-DMS8JsM-.js} +1 -1
- package/core/built/admin/assets/{add-integration-modal-m9Dv6f77.js → add-integration-modal-DU9ghGLS.js} +1 -1
- package/core/built/admin/assets/{add-newsletter-modal-CPSVZL2H.js → add-newsletter-modal-DFxuQrYu.js} +1 -1
- package/core/built/admin/assets/{add-recommendation-modal-Ci9Z5yat.js → add-recommendation-modal-D-3LOrdd.js} +1 -1
- package/core/built/admin/assets/{analytics-DqSCVV8U.js → analytics-DTIMRhWw.js} +1 -1
- package/core/built/admin/assets/{announcement-bar-modal-DU3avV7d.js → announcement-bar-modal-DGCcezB6.js} +1 -1
- package/core/built/admin/assets/{api-keys-BK0LylyK.js → api-keys-Dx4NeNpK.js} +1 -1
- package/core/built/admin/assets/{audience-DTJtXXWY.js → audience-DkknNqhZ.js} +1 -1
- package/core/built/admin/assets/{automations-dtQ_MRZ7.js → automations-DvSU7QhW.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-B8Es3AuV.js → bluesky-sharing-yZ9A_jL8.js} +1 -1
- package/core/built/admin/assets/{breadcrumb-tg5zL--z.js → breadcrumb-CjOH7LCU.js} +1 -1
- package/core/built/admin/assets/{chart-helpers-CZElTGYL.js → chart-helpers-DGxvWnBT.js} +1 -1
- package/core/built/admin/assets/{checkbox-Cyo-x7mC.js → checkbox-BSUcnNq_.js} +1 -1
- package/core/built/admin/assets/{chunk.873.b8fc05a8ab877fb819ed.js → chunk.424.4a27628efdade15c2dab.js} +3 -3
- package/core/built/admin/assets/chunk.524.dce1c51e0bc30fb2718e.js +25 -0
- package/core/built/admin/assets/{chunk.211.6f7328d63c41536451b3.js → chunk.571.40f093ed6d883993714e.js} +5 -5
- package/core/built/admin/assets/chunk.582.3a430d5c6a744fd475b6.js +27 -0
- package/core/built/admin/assets/{code-editor-view-Dq1rQ4tS.js → code-editor-view-BcyKABrG.js} +1 -1
- package/core/built/admin/assets/{color-picker-field-BonBgxhU.js → color-picker-field-8NoSnhNI.js} +1 -1
- package/core/built/admin/assets/{color-utils-BsrwXuDz.js → color-utils-Dzeeuj1F.js} +1 -1
- package/core/built/admin/assets/{combobox-uX5r_paI.js → combobox-5rdgDrDV.js} +1 -1
- package/core/built/admin/assets/{command-DxwBO2pF.js → command-BOYMbZa_.js} +1 -1
- package/core/built/admin/assets/{comments-Dn-RO-nU.js → comments-BhNM5CYl.js} +1 -1
- package/core/built/admin/assets/{confirmation-modal-B6D6mk6R.js → confirmation-modal-CYD7x3UQ.js} +1 -1
- package/core/built/admin/assets/{content-api-modal-BxQ-7T6u.js → content-api-modal-BtW4Z1-1.js} +1 -1
- package/core/built/admin/assets/{copy-field-BAxL4Zqn.js → copy-field-CEp_XieW.js} +1 -1
- package/core/built/admin/assets/{custom-integration-modal---d_l7lc.js → custom-integration-modal-bk3lNEuQ.js} +1 -1
- package/core/built/admin/assets/{date-range-select-PB2n1HZf.js → date-range-select-B3Ij1Rqg.js} +1 -1
- package/core/built/admin/assets/{debounce-Dg2fW5Rg.js → debounce-Cx5B_9OI.js} +1 -1
- package/core/built/admin/assets/{design-and-theme-modal-DYRT207a.js → design-and-theme-modal-B2xYKS1I.js} +3 -3
- package/core/built/admin/assets/{dirty-confirm-dialog-BnDJ39ao.js → dirty-confirm-dialog-YE9L21gY.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-iMHrkyV3.js → disabled-sources-indicator-uIzbWL7K.js} +1 -1
- package/core/built/admin/assets/{dist-D5vhMdXf.js → dist-ARN6XTcR.js} +1 -1
- package/core/built/admin/assets/{dist-cFzKLG7y.js → dist-Bg8bSvLA.js} +1 -1
- package/core/built/admin/assets/{dist-WeOIvSYM.js → dist-CsvXCJSe.js} +1 -1
- package/core/built/admin/assets/{domain-BR8FIdjD.js → domain-DzrOAPY8.js} +1 -1
- package/core/built/admin/assets/{dropzone-hNxmUvwD.js → dropzone-CzhP33wa.js} +1 -1
- package/core/built/admin/assets/{edit-profile-CENrkY0l.js → edit-profile-oufs4R4n.js} +1 -1
- package/core/built/admin/assets/{editor-B18D0lm8.js → editor-BBKrunMK.js} +3 -3
- package/core/built/admin/assets/{embed-signup-form-modal-BsCYUMvQ.js → embed-signup-form-modal-Ch3s6lqA.js} +1 -1
- package/core/built/admin/assets/{explore-BmrnljWw.js → explore-CA-R01Br.js} +1 -1
- package/core/built/admin/assets/{feed-Dy4li6i8.js → feed-ClC8_Gld.js} +1 -1
- package/core/built/admin/assets/{field-Dh_lwk13.js → field-B3OPenbP.js} +1 -1
- package/core/built/admin/assets/{filter-sources-D2zDuJH5.js → filter-sources-CiPLnG9A.js} +1 -1
- package/core/built/admin/assets/{filters-3JBQcNtJ.js → filters-Bz0HsRie.js} +1 -1
- package/core/built/admin/assets/{filters-CFt_BDe4.js → filters-yFvybRMe.js} +1 -1
- package/core/built/admin/assets/{first-promoter-modal-D6Rjxugw.js → first-promoter-modal-BC0UpYCD.js} +1 -1
- package/core/built/admin/assets/{gift-link-modal-zMtlJhLQ.js → gift-link-modal-BKRuqo-k.js} +1 -1
- package/core/built/admin/assets/{global-data-provider-B949LDXJ.js → global-data-provider-RzW6Bnlf.js} +1 -1
- package/core/built/admin/assets/{growth-CFws67_i.js → growth-ClNAldfL.js} +1 -1
- package/core/built/admin/assets/{growth-C38Xtp8D.js → growth-Dz8OSG06.js} +1 -1
- package/core/built/admin/assets/{history-modal-ILAOkbxH.js → history-modal-DIVuQ5Ct.js} +1 -1
- package/core/built/admin/assets/{html-field-9a9Id_2q.js → html-field-CVZ2sMxw.js} +1 -1
- package/core/built/admin/assets/{images-CrQKVc9Q.js → images-Ok8FFJ3A.js} +1 -1
- package/core/built/admin/assets/{inbox-BrSRzyvz.js → inbox-CMUVI6qb.js} +1 -1
- package/core/built/admin/assets/{index-BHy2DZX-.js → index-DUXM0NlS.js} +2 -2
- package/core/built/admin/assets/{input-group-6vEPvX9S.js → input-group-2XU-iuh_.js} +1 -1
- package/core/built/admin/assets/{invite-user-modal-5xDCEw90.js → invite-user-modal-DrZ7xxrZ.js} +1 -1
- package/core/built/admin/assets/{koenig-editor-base-BWLEB9Vf.js → koenig-editor-base-BPj4S760.js} +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +9254 -9251
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +124 -124
- package/core/built/admin/assets/{koenig-lexical-Cj62r_y9.js → koenig-lexical-Cdjv-m3_.js} +98 -98
- package/core/built/admin/assets/{kpi-card-IHDo8Q56.js → kpi-card-J8Mj1sDX.js} +1 -1
- package/core/built/admin/assets/{kpi-card-DOUnkZcN.js → kpi-card-awos0BMH.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-CjUdECAK.js → kpi-helpers-SEhvH-8E.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-X3DkOYbI.js → kpi-tabs-R6BjsKhd.js} +1 -1
- package/core/built/admin/assets/{label-Dq6QaOuS.js → label-BMC3dzLI.js} +1 -1
- package/core/built/admin/assets/{label-picker-8YlhS2U3.js → label-picker-Bnlru5tO.js} +1 -1
- package/core/built/admin/assets/layout-yR9rlvEw.js +1 -0
- package/core/built/admin/assets/{limit-modal-BRPsit_B.js → limit-modal-COmLevuY.js} +1 -1
- package/core/built/admin/assets/{member-custom-fields-CmvsuuV_.js → member-custom-fields-BvFKEpxJ.js} +1 -1
- package/core/built/admin/assets/{member-detail-DCvRrkmJ.js → member-detail-CWdPAe3p.js} +1 -1
- package/core/built/admin/assets/{members-DxbSKngD.js → members-Bnt0ih2R.js} +1 -1
- package/core/built/admin/assets/{moderation-Da5lhhWh.js → moderation-mPjYHgZW.js} +1 -1
- package/core/built/admin/assets/{multi-select-combobox-CYdkH7KQ.js → multi-select-combobox-CsoyHAiX.js} +1 -1
- package/core/built/admin/assets/{navigation-modal-BIoTwtMy.js → navigation-modal-D_c0badj.js} +1 -1
- package/core/built/admin/assets/{newsletter-BptlVoJm.js → newsletter-DR49NyGS.js} +1 -1
- package/core/built/admin/assets/{newsletter-detail-modal-BIvxqE0N.js → newsletter-detail-modal-B45pL1pb.js} +1 -1
- package/core/built/admin/assets/{newsletter-emails-Bf8IZLsP.js → newsletter-emails-B-OLfrvr.js} +1 -1
- package/core/built/admin/assets/{newsletters-BgrIZk0V.js → newsletters-DGs5BaKH.js} +1 -1
- package/core/built/admin/assets/{note-DYcLOmDb.js → note-CSVsUKBm.js} +1 -1
- package/core/built/admin/assets/{notifications-B2SPFIV9.js → notifications-DV9FRtDw.js} +1 -1
- package/core/built/admin/assets/{offers-container-modal-BZnZITMw.js → offers-container-modal-B0hFS8nk.js} +1 -1
- package/core/built/admin/assets/{onboarding-route-DgdfxkQh.js → onboarding-route--jL5KLPD.js} +1 -1
- package/core/built/admin/assets/{overview-Dt2_mR6U.js → overview-B-ZPNqMo.js} +1 -1
- package/core/built/admin/assets/{overview-BYQJXSog.js → overview-D96iwoJi.js} +1 -1
- package/core/built/admin/assets/{page-header-CpmxJj5B.js → page-header-DAvDYKO9.js} +1 -1
- package/core/built/admin/assets/{pintura-modal-B4MuBuuO.js → pintura-modal-jiUCxX0Q.js} +1 -1
- package/core/built/admin/assets/{portal-frame-BF0NBIji.js → portal-frame-D4D4nEO6.js} +1 -1
- package/core/built/admin/assets/{portal-modal-DMVbm0oy.js → portal-modal-BLFdYeDH.js} +1 -1
- package/core/built/admin/assets/{post-analytics-CXWBOGyP.js → post-analytics-BhAtiFJp.js} +1 -1
- package/core/built/admin/assets/{post-analytics-header-ChzjXVPt.js → post-analytics-header-DK27VjJm.js} +1 -1
- package/core/built/admin/assets/{post-analytics-provider-Cgw4U_fw.js → post-analytics-provider-DGg0FUqs.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-B9Vmk_XM.js → post-share-modal-DtFAC132.js} +1 -1
- package/core/built/admin/assets/{preferences-BfzJmnsd.js → preferences-CuWLISHa.js} +1 -1
- package/core/built/admin/assets/{preview-modal-Dw_HtD6S.js → preview-modal-DctYx20m.js} +1 -1
- package/core/built/admin/assets/{profile-BT5vtv9b.js → profile-BsKYYtuL.js} +1 -1
- package/core/built/admin/assets/{radio-group-I7ou7PoJ.js → radio-group-CI5_Kf_d.js} +1 -1
- package/core/built/admin/assets/{select-D9oU1IoM.js → select-CkXqNJDJ.js} +1 -1
- package/core/built/admin/assets/settings-B4vM-rgV.js +4 -0
- package/core/built/admin/assets/{settings-CpvHWix7.js → settings-ZCj-W56o.js} +1 -1
- package/core/built/admin/assets/{settings-breadcrumbs-K0USP2nw.js → settings-breadcrumbs-DIq31FwA.js} +1 -1
- package/core/built/admin/assets/{share-modal-ClIyPzc5.js → share-modal-BNj14g79.js} +1 -1
- package/core/built/admin/assets/{slack-modal-1XXqOfiw.js → slack-modal-C6SpMQ-q.js} +1 -1
- package/core/built/admin/assets/{sort-button-CdBsm40v.js → sort-button-B-k54ow2.js} +1 -1
- package/core/built/admin/assets/{source-icon-CEYmVLXR.js → source-icon-Cej3sZM6.js} +1 -1
- package/core/built/admin/assets/{stats-D8nZCTy5.js → stats-B2KW8E2y.js} +1 -1
- package/core/built/admin/assets/{stats-view-CugFVf0g.js → stats-view-CVzU2-p5.js} +1 -1
- package/core/built/admin/assets/{step-1-DOYjrORn.js → step-1-Dbn1iIMU.js} +1 -1
- package/core/built/admin/assets/{step-2-vA6RsarH.js → step-2-BahnSuN6.js} +1 -1
- package/core/built/admin/assets/{step-3-BtIBxMId.js → step-3-CW04e2ZJ.js} +1 -1
- package/core/built/admin/assets/{stripe-button-UFP4QRXd.js → stripe-button-BkBZqaGG.js} +1 -1
- package/core/built/admin/assets/{stripe-connect-modal-C_L3uM6n.js → stripe-connect-modal-CYDDrSJm.js} +1 -1
- package/core/built/admin/assets/{table-TMCgCQK7.js → table-BtLcEi1M.js} +1 -1
- package/core/built/admin/assets/{tabs-Dd6oOKbv.js → tabs-BaWRfg8j.js} +1 -1
- package/core/built/admin/assets/{tag-detail-D3l-UJPR.js → tag-detail-CmqiC32L.js} +1 -1
- package/core/built/admin/assets/{tags-WdKQGWR8.js → tags-BbygUvNB.js} +1 -1
- package/core/built/admin/assets/{testimonials-modal-CqWp-xXJ.js → testimonials-modal--bdzppyh.js} +1 -1
- package/core/built/admin/assets/{tier-detail-modal-BkcR2u0A.js → tier-detail-modal-BZmAsIy4.js} +1 -1
- package/core/built/admin/assets/{toggle-group-Guorw93z.js → toggle-group-CVl-Xigr.js} +1 -1
- package/core/built/admin/assets/{topic-filter-CbgoWIHn.js → topic-filter-BoRHRbL1.js} +1 -1
- package/core/built/admin/assets/{transistor-modal-DuNnnpal.js → transistor-modal-CBPDqEGy.js} +1 -1
- package/core/built/admin/assets/{unsplash-modal-y2bVwTHH.js → unsplash-modal-CixoRAO2.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-B-b4wNSC.js → use-analytics-data-BiWvcf1u.js} +1 -1
- package/core/built/admin/assets/{use-check-theme-limit-error-m09J38Mq.js → use-check-theme-limit-error-B14lZukR.js} +1 -1
- package/core/built/admin/assets/use-feature-flag-BmJoc5WN.js +1 -0
- package/core/built/admin/assets/{use-filter-params-BnbBDc7T.js → use-filter-params-ChoV72Hw.js} +1 -1
- package/core/built/admin/assets/{use-growth-stats-DPU5rt3W.js → use-growth-stats-Dom8xVzo.js} +1 -1
- package/core/built/admin/assets/{use-limiter-BkN3E4gw.js → use-limiter-Dwg8ivgQ.js} +1 -1
- package/core/built/admin/assets/{use-pintura-config-C_z8HTG2.js → use-pintura-config-Bj4QmiVk.js} +1 -1
- package/core/built/admin/assets/{use-pintura-editor-DNgbkhGp.js → use-pintura-editor-DvrslmPE.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-C2mXyrp-.js → use-post-referrers-pxIHTXFs.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-DaK8q2qQ.js → use-reply-chain-data-CmFe0N6F.js} +1 -1
- package/core/built/admin/assets/{use-setting-group-YlYcoNyP.js → use-setting-group-Cmp_sy-d.js} +1 -1
- package/core/built/admin/assets/{use-settings-navigation-Bf1g1rEh.js → use-settings-navigation-BmROPrls.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-D9ckKo5-.js → use-simple-pagination-D5rL3YwO.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-GSa32hVw.js → use-tinybird-query-DP2dMaq7.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-CKLOZFsw.js → use-tinybird-token-DrK_skPQ.js} +1 -1
- package/core/built/admin/assets/{use-url-input-z-p0Kewm.js → use-url-input-ChrUZBFf.js} +1 -1
- package/core/built/admin/assets/{user-detail-modal-BMxOA4Sv.js → user-detail-modal-XODpOqNC.js} +1 -1
- package/core/built/admin/assets/{virtual-list-RHeEL05L.js → virtual-list-Dnoafqbk.js} +1 -1
- package/core/built/admin/assets/{web-xJtcwMy4.js → web-D71mjB2r.js} +1 -1
- package/core/built/admin/assets/{web-Bw2QMO5u.js → web-z71lA1Y3.js} +1 -1
- package/core/built/admin/assets/{zapier-modal-BP2f4-vY.js → zapier-modal-Bp507Um7.js} +1 -1
- package/core/built/admin/index.html +5 -5
- package/core/frontend/web/middleware/admin-toolbar.js +1 -1
- package/core/frontend/web/middleware/serve-indexnow-key.js +1 -7
- package/core/server/api/endpoints/index.js +2 -2
- package/core/server/api/endpoints/themes.js +6 -1
- package/core/server/models/base/plugins/overrides.js +7 -5
- package/core/server/services/adapter-manager/adapter-paths.js +16 -12
- package/core/server/services/adapter-manager/adapter-paths.ts +17 -12
- package/core/server/services/audience-feedback/audience-feedback-controller.js +10 -1
- package/core/server/services/audience-feedback/feedback-repository.js +4 -1
- package/core/server/services/auth/session/session-service.js +18 -3
- package/core/server/services/automations/database-automations-repository.js +5 -5
- package/core/server/services/automations/database-automations-repository.ts +12 -12
- package/core/server/services/automations/database-date.js +52 -3
- package/core/server/services/automations/database-date.ts +23 -2
- package/core/server/services/email-analytics/email-analytics-service-wrapper.js +1 -1
- package/core/server/services/email-analytics/email-analytics-service.js +3 -2
- package/core/server/services/email-analytics/index.js +2 -3
- package/core/server/services/email-analytics/index.ts +1 -2
- package/core/server/services/email-analytics/lib/queries.js +91 -124
- package/core/server/services/email-analytics/lib/queries.ts +341 -0
- package/core/server/services/indexnow-ping/index.js +0 -2
- package/core/server/services/indexnow-ping/index.ts +0 -2
- package/core/server/services/indexnow-ping/indexnow-ping-service.js +5 -7
- package/core/server/services/indexnow-ping/indexnow-ping-service.ts +7 -13
- package/core/server/services/members/members-api/repositories/member-repository.js +51 -0
- package/core/server/services/slack-notifications/slack-notifications.js +2 -2
- package/core/server/services/slack-ping/index.js +1 -1
- package/core/server/services/slack-ping/index.ts +1 -1
- package/core/server/services/slack-ping/slack-ping-service.js +1 -0
- package/core/server/services/slack-ping/slack-ping-service.ts +1 -0
- package/core/server/services/stats/members-stats-service.js +0 -7
- package/core/server/services/themes/storage.js +19 -2
- package/core/server/services/webhooks/serialize.js +10 -1
- package/core/server/web/api/endpoints/admin/routes.js +2 -2
- package/core/shared/custom-theme-settings-cache/custom-theme-settings-bread-service.js +4 -0
- package/core/shared/custom-theme-settings-cache/custom-theme-settings-service.js +39 -0
- package/core/shared/labs.js +1 -1
- package/package.json +11 -11
- package/pnpm-lock.yaml +41 -41
- package/pnpm-workspace.yaml +18 -18
- package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
- package/components/tryghost-kg-converters-1.2.3.tgz +0 -0
- package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.1.4.tgz +0 -0
- package/core/built/admin/assets/chunk.524.cd5498dd4e195f679266.js +0 -25
- package/core/built/admin/assets/chunk.582.6f92ec1f03734bf2ea3f.js +0 -27
- package/core/built/admin/assets/layout-TMj4P-vv.js +0 -1
- package/core/built/admin/assets/settings-CfZFbyu_.js +0 -4
- package/core/built/admin/assets/use-feature-flag-ud0Qz8vt.js +0 -1
- package/core/server/services/email-analytics/jobs/update-member-email-analytics/index.js +0 -14
- /package/core/built/admin/assets/{chunk.211.6f7328d63c41536451b3.js.LICENSE.txt → chunk.571.40f093ed6d883993714e.js.LICENSE.txt} +0 -0
- /package/core/server/api/endpoints/{gift-reminders.js → gifts.js} +0 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import debugFactory from '@tryghost/debug';
|
|
3
|
+
// @ts-expect-error This module lacks type definitions.
|
|
4
|
+
import db from '../../../data/db';
|
|
5
|
+
import logging from '@tryghost/logging';
|
|
6
|
+
import ObjectID from 'bson-objectid';
|
|
7
|
+
|
|
8
|
+
const debug = debugFactory('services:email-analytics');
|
|
9
|
+
|
|
10
|
+
const MIN_EMAIL_COUNT_FOR_OPEN_RATE = 5;
|
|
11
|
+
|
|
12
|
+
type EmailAnalyticsJobName = string;
|
|
13
|
+
type EmailAnalyticsEvent = 'delivered' | 'opened' | 'failed';
|
|
14
|
+
type CursorSeed = {
|
|
15
|
+
tableName: string;
|
|
16
|
+
eventColumns: Partial<Record<EmailAnalyticsEvent, string>>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a job in the jobs table if it does not already exist.
|
|
21
|
+
* @param jobName - The name of the job to create.
|
|
22
|
+
*/
|
|
23
|
+
async function createJobIfNotExists(jobName: EmailAnalyticsJobName): Promise<void> {
|
|
24
|
+
await db.knex('jobs').insert({
|
|
25
|
+
id: new ObjectID().toHexString(),
|
|
26
|
+
name: jobName,
|
|
27
|
+
started_at: new Date(),
|
|
28
|
+
created_at: new Date(),
|
|
29
|
+
status: 'started'
|
|
30
|
+
}).onConflict('name').ignore();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const queries = {
|
|
34
|
+
/**
|
|
35
|
+
* Retrieves the timestamp of the last seen event for the specified email analytics events.
|
|
36
|
+
* @param jobName - The name of the job to update.
|
|
37
|
+
* @param events - The email analytics events to consider.
|
|
38
|
+
* @param cursorSeed - Recipient table and timestamp columns to read the initial cursor from. Used when the job has no stored timestamp yet.
|
|
39
|
+
* @returns The timestamp of the last seen event, or null if no events are found.
|
|
40
|
+
*/
|
|
41
|
+
async getLastEventTimestamp(
|
|
42
|
+
jobName: EmailAnalyticsJobName,
|
|
43
|
+
events: EmailAnalyticsEvent[],
|
|
44
|
+
cursorSeed: CursorSeed
|
|
45
|
+
): Promise<Date | null> {
|
|
46
|
+
const startDate = new Date();
|
|
47
|
+
|
|
48
|
+
let timestamps: (Date | string | null)[] = [];
|
|
49
|
+
const lastJobRunTimestamp = await this.getLastJobRunTimestamp(jobName);
|
|
50
|
+
|
|
51
|
+
if (lastJobRunTimestamp) {
|
|
52
|
+
debug(`Using job data for ${jobName}`);
|
|
53
|
+
timestamps = [lastJobRunTimestamp];
|
|
54
|
+
} else {
|
|
55
|
+
debug(`Job data not found for ${jobName}, using ${cursorSeed.tableName} data`);
|
|
56
|
+
logging.info(`Job data not found for ${jobName}, using ${cursorSeed.tableName} data`);
|
|
57
|
+
|
|
58
|
+
for (const event of events) {
|
|
59
|
+
const columnName = cursorSeed.eventColumns[event];
|
|
60
|
+
if (!columnName) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const row = await db.knex(cursorSeed.tableName)
|
|
64
|
+
.select(db.knex.raw('MAX(??) as maxTimestamp', [columnName]))
|
|
65
|
+
.first();
|
|
66
|
+
timestamps.push(row.maxTimestamp);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await createJobIfNotExists(jobName);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Convert string dates to Date objects for SQLite compatibility
|
|
73
|
+
const normalizedTimestamps = timestamps.map(date => {
|
|
74
|
+
if (!date) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return date instanceof Date ? date : new Date(date);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const lastSeenEventTimestamp = _.max(normalizedTimestamps) ?? null;
|
|
81
|
+
debug(`getLastEventTimestamp: finished in ${Date.now() - startDate.getTime()}ms`);
|
|
82
|
+
|
|
83
|
+
return lastSeenEventTimestamp;
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Retrieves the job data for the specified job name.
|
|
88
|
+
* @param jobName - The name of the job to retrieve data for.
|
|
89
|
+
* @returns The job data, or null if no job data is found.
|
|
90
|
+
*/
|
|
91
|
+
async getJobData(jobName: EmailAnalyticsJobName) {
|
|
92
|
+
return await db.knex('jobs').select('finished_at', 'started_at', 'metadata').where('name', jobName).first();
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Retrieves the timestamp of the last job run for the specified job name.
|
|
97
|
+
* @param jobName - The name of the job to retrieve the last run timestamp for.
|
|
98
|
+
* @returns The timestamp of the last job run, or null if no job data is found.
|
|
99
|
+
*/
|
|
100
|
+
async getLastJobRunTimestamp(jobName: EmailAnalyticsJobName): Promise<Date | null> {
|
|
101
|
+
const jobData = await this.getJobData(jobName);
|
|
102
|
+
return jobData ? jobData.finished_at || jobData.started_at : null;
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Sets the timestamp of the last seen event for the specified email analytics events.
|
|
107
|
+
* @param jobName - The name of the job to update.
|
|
108
|
+
* @param field - The field to update.
|
|
109
|
+
* @param date - The timestamp of the last seen event.
|
|
110
|
+
* @description
|
|
111
|
+
* Updates the `finished_at` or `started_at` column of the specified job in the `jobs` table with the provided timestamp.
|
|
112
|
+
* This is used to keep track of the last time the job was run to avoid expensive queries following reboot.
|
|
113
|
+
*/
|
|
114
|
+
async setJobTimestamp(jobName: EmailAnalyticsJobName, field: 'finished' | 'started', date: Date): Promise<void> {
|
|
115
|
+
// Convert string dates to Date objects for SQLite compatibility
|
|
116
|
+
try {
|
|
117
|
+
debug(`Setting ${field} timestamp for job ${jobName} to ${date}`);
|
|
118
|
+
const updateField = field === 'finished' ? 'finished_at' : 'started_at';
|
|
119
|
+
const status = field === 'finished' ? 'finished' : 'started';
|
|
120
|
+
const result = await db.knex('jobs').update({[updateField]: date, updated_at: new Date(), status: status}).where('name', jobName);
|
|
121
|
+
if (result === 0) {
|
|
122
|
+
await db.knex('jobs').insert({
|
|
123
|
+
id: new ObjectID().toHexString(),
|
|
124
|
+
name: jobName,
|
|
125
|
+
[updateField]: date.toISOString(), // force to iso string for sqlite
|
|
126
|
+
updated_at: date.toISOString(), // force to iso string for sqlite
|
|
127
|
+
status: status
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
} catch (err: unknown) {
|
|
131
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
132
|
+
debug(`Error setting ${field} timestamp for job ${jobName}: ${message}`);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Retrieves and parses the metadata JSON for the specified job.
|
|
138
|
+
* @param jobName - The name of the job.
|
|
139
|
+
* @returns The parsed metadata object, or null.
|
|
140
|
+
*/
|
|
141
|
+
async getJobMetadata(jobName: EmailAnalyticsJobName): Promise<object | null> {
|
|
142
|
+
try {
|
|
143
|
+
const row = await db.knex('jobs').select('metadata').where('name', jobName).first();
|
|
144
|
+
if (row && row.metadata) {
|
|
145
|
+
return JSON.parse(row.metadata);
|
|
146
|
+
}
|
|
147
|
+
} catch (err: unknown) {
|
|
148
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
149
|
+
logging.error(`Error reading metadata for job ${jobName}: ${message}`);
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Writes metadata JSON for the specified job.
|
|
156
|
+
* @param jobName - The name of the job.
|
|
157
|
+
* @param metadata - The metadata to store, or null to clear.
|
|
158
|
+
*/
|
|
159
|
+
async setJobMetadata(jobName: EmailAnalyticsJobName, metadata: object | null): Promise<void> {
|
|
160
|
+
try {
|
|
161
|
+
const value = metadata ? JSON.stringify(metadata) : null;
|
|
162
|
+
await db.knex.transaction(async (trx: typeof db.knex) => {
|
|
163
|
+
const result = await trx('jobs').update({metadata: value, updated_at: new Date()}).where('name', jobName);
|
|
164
|
+
if (result === 0 && metadata) {
|
|
165
|
+
await trx('jobs').insert({
|
|
166
|
+
id: new ObjectID().toHexString(),
|
|
167
|
+
name: jobName,
|
|
168
|
+
metadata: value,
|
|
169
|
+
created_at: new Date(),
|
|
170
|
+
status: 'queued'
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
} catch (err: unknown) {
|
|
175
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
176
|
+
logging.error(`Error setting metadata for job ${jobName}: ${message}`);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Sets the status of the specified email analytics job.
|
|
182
|
+
* @param jobName - The name of the job to update.
|
|
183
|
+
* @param status - The new status of the job.
|
|
184
|
+
* @description
|
|
185
|
+
* Updates the `status` column of the specified job in the `jobs` table with the provided status.
|
|
186
|
+
* This is used to keep track of the current state of the job.
|
|
187
|
+
*/
|
|
188
|
+
async setJobStatus(jobName: EmailAnalyticsJobName, status: 'started' | 'finished' | 'failed'): Promise<void> {
|
|
189
|
+
debug(`Setting status for job ${jobName} to ${status}`);
|
|
190
|
+
try {
|
|
191
|
+
const result = await db.knex('jobs')
|
|
192
|
+
.update({
|
|
193
|
+
status: status,
|
|
194
|
+
updated_at: new Date()
|
|
195
|
+
})
|
|
196
|
+
.where('name', jobName);
|
|
197
|
+
|
|
198
|
+
if (result === 0) {
|
|
199
|
+
await db.knex('jobs').insert({
|
|
200
|
+
id: new ObjectID().toHexString(),
|
|
201
|
+
name: jobName,
|
|
202
|
+
status: status,
|
|
203
|
+
created_at: new Date(),
|
|
204
|
+
updated_at: new Date()
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
} catch (err: unknown) {
|
|
208
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
209
|
+
debug(`Error setting status for job ${jobName}: ${message}`);
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
async aggregateEmailStats(emailId: string, updateOpenedCount: boolean): Promise<void> {
|
|
215
|
+
const [deliveredCount] = await db.knex('email_recipients').count('id as count').whereRaw('email_id = ? AND delivered_at IS NOT NULL', [emailId]);
|
|
216
|
+
const [failedCount] = await db.knex('email_recipients').count('id as count').whereRaw('email_id = ? AND failed_at IS NOT NULL', [emailId]);
|
|
217
|
+
|
|
218
|
+
const updateData: Record<string, string | number> = {
|
|
219
|
+
delivered_count: deliveredCount.count,
|
|
220
|
+
failed_count: failedCount.count
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
if (updateOpenedCount) {
|
|
224
|
+
const [openedCount] = await db.knex('email_recipients').count('id as count').whereRaw('email_id = ? AND opened_at IS NOT NULL', [emailId]);
|
|
225
|
+
updateData.opened_count = openedCount.count;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await db.knex('emails').update(updateData).where('id', emailId);
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
async aggregateMemberStats(memberId: string): Promise<void> {
|
|
232
|
+
const {trackedEmailCount} = await db.knex('email_recipients')
|
|
233
|
+
.select(db.knex.raw('COUNT(email_recipients.id) as trackedEmailCount'))
|
|
234
|
+
.leftJoin('emails', 'email_recipients.email_id', 'emails.id')
|
|
235
|
+
.where('email_recipients.member_id', memberId)
|
|
236
|
+
.where('emails.track_opens', true)
|
|
237
|
+
.first() || {};
|
|
238
|
+
|
|
239
|
+
const [emailCount] = await db.knex('email_recipients').count('id as count').whereRaw('member_id = ?', [memberId]);
|
|
240
|
+
const [emailOpenedCount] = await db.knex('email_recipients').count('id as count').whereRaw('member_id = ? AND opened_at IS NOT NULL', [memberId]);
|
|
241
|
+
|
|
242
|
+
const updateQuery: Record<string, string | number> = {
|
|
243
|
+
email_count: emailCount.count,
|
|
244
|
+
email_opened_count: emailOpenedCount.count
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (trackedEmailCount >= MIN_EMAIL_COUNT_FOR_OPEN_RATE) {
|
|
248
|
+
updateQuery.email_open_rate = Math.round(emailOpenedCount.count / trackedEmailCount * 100);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
await db.knex('members')
|
|
252
|
+
.update(updateQuery)
|
|
253
|
+
.where('id', memberId);
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
async aggregateMemberStatsBatch(memberIds: string[]): Promise<void> {
|
|
257
|
+
if (!memberIds || memberIds.length === 0) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Batch query to get stats for all members at once
|
|
262
|
+
const stats = await db.knex('email_recipients')
|
|
263
|
+
.leftJoin('emails', 'emails.id', 'email_recipients.email_id')
|
|
264
|
+
.select(
|
|
265
|
+
'email_recipients.member_id',
|
|
266
|
+
db.knex.raw('COUNT(email_recipients.id) as email_count'),
|
|
267
|
+
db.knex.raw('SUM(CASE WHEN email_recipients.opened_at IS NOT NULL THEN 1 ELSE 0 END) as email_opened_count'),
|
|
268
|
+
db.knex.raw('SUM(CASE WHEN emails.track_opens = 1 THEN 1 ELSE 0 END) as tracked_count')
|
|
269
|
+
)
|
|
270
|
+
.whereIn('email_recipients.member_id', memberIds)
|
|
271
|
+
.groupBy('email_recipients.member_id');
|
|
272
|
+
|
|
273
|
+
// Build update data for each member
|
|
274
|
+
const memberStatsMap = new Map<string, {email_count: number; email_opened_count: number; email_open_rate: number | null}>();
|
|
275
|
+
for (const stat of stats) {
|
|
276
|
+
const emailOpenRate = stat.tracked_count >= MIN_EMAIL_COUNT_FOR_OPEN_RATE
|
|
277
|
+
? Math.round((stat.email_opened_count / stat.tracked_count) * 100)
|
|
278
|
+
: null;
|
|
279
|
+
|
|
280
|
+
memberStatsMap.set(stat.member_id, {
|
|
281
|
+
email_count: stat.email_count,
|
|
282
|
+
email_opened_count: stat.email_opened_count,
|
|
283
|
+
email_open_rate: emailOpenRate
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Build CASE statements for batch update
|
|
288
|
+
const emailCountCases: string[] = [];
|
|
289
|
+
const emailOpenedCountCases: string[] = [];
|
|
290
|
+
const emailOpenRateCases: string[] = [];
|
|
291
|
+
const emailCountBindings: (string | number)[] = [];
|
|
292
|
+
const emailOpenedCountBindings: (string | number)[] = [];
|
|
293
|
+
const emailOpenRateBindings: (string | number)[] = [];
|
|
294
|
+
|
|
295
|
+
for (const memberId of memberIds) {
|
|
296
|
+
const memberStats = memberStatsMap.get(memberId) || {
|
|
297
|
+
email_count: 0,
|
|
298
|
+
email_opened_count: 0,
|
|
299
|
+
email_open_rate: null
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
emailCountCases.push(`WHEN ? THEN ?`);
|
|
303
|
+
emailCountBindings.push(memberId, memberStats.email_count);
|
|
304
|
+
|
|
305
|
+
emailOpenedCountCases.push(`WHEN ? THEN ?`);
|
|
306
|
+
emailOpenedCountBindings.push(memberId, memberStats.email_opened_count);
|
|
307
|
+
|
|
308
|
+
if (memberStats.email_open_rate !== null) {
|
|
309
|
+
emailOpenRateCases.push(`WHEN ? THEN ?`);
|
|
310
|
+
emailOpenRateBindings.push(memberId, memberStats.email_open_rate);
|
|
311
|
+
} else {
|
|
312
|
+
emailOpenRateCases.push(`WHEN ? THEN NULL`);
|
|
313
|
+
emailOpenRateBindings.push(memberId);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Combine bindings in the order they appear in the SQL statement:
|
|
318
|
+
// 1. All bindings for email_count CASE statement
|
|
319
|
+
// 2. All bindings for email_opened_count CASE statement
|
|
320
|
+
// 3. All bindings for email_open_rate CASE statement
|
|
321
|
+
// 4. Member IDs for the WHERE IN clause
|
|
322
|
+
const bindings = [
|
|
323
|
+
...emailCountBindings,
|
|
324
|
+
...emailOpenedCountBindings,
|
|
325
|
+
...emailOpenRateBindings,
|
|
326
|
+
...memberIds
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
// Execute batched update with CASE statements
|
|
330
|
+
await db.knex.raw(`
|
|
331
|
+
UPDATE members
|
|
332
|
+
SET
|
|
333
|
+
email_count = CASE id ${emailCountCases.join(' ')} END,
|
|
334
|
+
email_opened_count = CASE id ${emailOpenedCountCases.join(' ')} END,
|
|
335
|
+
email_open_rate = CASE id ${emailOpenRateCases.join(' ')} END
|
|
336
|
+
WHERE id IN (${memberIds.map(() => '?').join(',')})
|
|
337
|
+
`, bindings);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
export type Queries = typeof queries;
|
|
@@ -11,7 +11,6 @@ class IndexNowPingServiceWrapper {
|
|
|
11
11
|
// Wire up all the dependencies
|
|
12
12
|
const settingsCache = require('../../../shared/settings-cache');
|
|
13
13
|
const config = require('../../../shared/config');
|
|
14
|
-
const labs = require('../../../shared/labs');
|
|
15
14
|
const urlService = require('../url');
|
|
16
15
|
const urlUtils = require('../../../shared/url-utils').default;
|
|
17
16
|
const request = require('@tryghost/request');
|
|
@@ -20,7 +19,6 @@ class IndexNowPingServiceWrapper {
|
|
|
20
19
|
this.service = new indexnow_ping_service_1.IndexNowPingService({
|
|
21
20
|
settingsCache,
|
|
22
21
|
config,
|
|
23
|
-
labs,
|
|
24
22
|
urlService,
|
|
25
23
|
urlUtils,
|
|
26
24
|
request,
|
|
@@ -12,7 +12,6 @@ class IndexNowPingServiceWrapper {
|
|
|
12
12
|
// Wire up all the dependencies
|
|
13
13
|
const settingsCache = require('../../../shared/settings-cache');
|
|
14
14
|
const config = require('../../../shared/config');
|
|
15
|
-
const labs = require('../../../shared/labs');
|
|
16
15
|
const urlService = require('../url');
|
|
17
16
|
const urlUtils = require('../../../shared/url-utils').default;
|
|
18
17
|
const request = require('@tryghost/request');
|
|
@@ -22,7 +21,6 @@ class IndexNowPingServiceWrapper {
|
|
|
22
21
|
this.service = new IndexNowPingService({
|
|
23
22
|
settingsCache,
|
|
24
23
|
config,
|
|
25
|
-
labs,
|
|
26
24
|
urlService,
|
|
27
25
|
urlUtils,
|
|
28
26
|
request,
|
|
@@ -31,17 +31,15 @@ const constants_1 = require("./constants");
|
|
|
31
31
|
class IndexNowPingService {
|
|
32
32
|
settingsCache;
|
|
33
33
|
config;
|
|
34
|
-
labs;
|
|
35
34
|
urlService;
|
|
36
35
|
urlUtils;
|
|
37
36
|
request;
|
|
38
37
|
logging;
|
|
39
38
|
events;
|
|
40
39
|
listener;
|
|
41
|
-
constructor({ settingsCache, config,
|
|
40
|
+
constructor({ settingsCache, config, urlService, urlUtils, request, logging, events }) {
|
|
42
41
|
this.settingsCache = settingsCache;
|
|
43
42
|
this.config = config;
|
|
44
|
-
this.labs = labs;
|
|
45
43
|
this.urlService = urlService;
|
|
46
44
|
this.urlUtils = urlUtils;
|
|
47
45
|
this.request = request;
|
|
@@ -68,6 +66,10 @@ class IndexNowPingService {
|
|
|
68
66
|
* Ping IndexNow with a URL.
|
|
69
67
|
*/
|
|
70
68
|
async ping(post) {
|
|
69
|
+
// Skip if in development
|
|
70
|
+
if (this.config.get('env') === 'development') {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
71
73
|
// Skip pages - only ping for posts
|
|
72
74
|
if (post.type === 'page') {
|
|
73
75
|
return;
|
|
@@ -80,10 +82,6 @@ class IndexNowPingService {
|
|
|
80
82
|
if (this.config.isPrivacyDisabled('useIndexNow')) {
|
|
81
83
|
return;
|
|
82
84
|
}
|
|
83
|
-
// Skip if IndexNow is not enabled in labs
|
|
84
|
-
if (!this.labs.isSet('indexnow')) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
85
|
// Don't ping for the default posts
|
|
88
86
|
if (post.slug && constants_1.defaultPostSlugs.indexOf(post.slug) > -1) {
|
|
89
87
|
return;
|
|
@@ -36,13 +36,10 @@ type SettingsCache = {
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
type Config = {
|
|
39
|
+
get(key: string): unknown;
|
|
39
40
|
isPrivacyDisabled(key: string): boolean;
|
|
40
41
|
};
|
|
41
42
|
|
|
42
|
-
type Labs = {
|
|
43
|
-
isSet(flag: string): boolean;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
43
|
type UrlService = {
|
|
47
44
|
getUrlForResource(resource: Record<string, unknown>, options: {absolute: boolean}): string | null;
|
|
48
45
|
};
|
|
@@ -84,7 +81,6 @@ type Post = {
|
|
|
84
81
|
export type IndexNowPingServiceDeps = {
|
|
85
82
|
settingsCache: SettingsCache;
|
|
86
83
|
config: Config;
|
|
87
|
-
labs: Labs;
|
|
88
84
|
urlService: UrlService;
|
|
89
85
|
urlUtils: UrlUtils;
|
|
90
86
|
request: RequestFn;
|
|
@@ -95,7 +91,6 @@ export type IndexNowPingServiceDeps = {
|
|
|
95
91
|
export class IndexNowPingService {
|
|
96
92
|
settingsCache: SettingsCache;
|
|
97
93
|
config: Config;
|
|
98
|
-
labs: Labs;
|
|
99
94
|
urlService: UrlService;
|
|
100
95
|
urlUtils: UrlUtils;
|
|
101
96
|
request: RequestFn;
|
|
@@ -103,10 +98,9 @@ export class IndexNowPingService {
|
|
|
103
98
|
events: ModelEvents;
|
|
104
99
|
listener: ModelEventListener;
|
|
105
100
|
|
|
106
|
-
constructor({settingsCache, config,
|
|
101
|
+
constructor({settingsCache, config, urlService, urlUtils, request, logging, events}: IndexNowPingServiceDeps) {
|
|
107
102
|
this.settingsCache = settingsCache;
|
|
108
103
|
this.config = config;
|
|
109
|
-
this.labs = labs;
|
|
110
104
|
this.urlService = urlService;
|
|
111
105
|
this.urlUtils = urlUtils;
|
|
112
106
|
this.request = request;
|
|
@@ -137,6 +131,11 @@ export class IndexNowPingService {
|
|
|
137
131
|
* Ping IndexNow with a URL.
|
|
138
132
|
*/
|
|
139
133
|
async ping(post: Post): Promise<void> {
|
|
134
|
+
// Skip if in development
|
|
135
|
+
if (this.config.get('env') === 'development') {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
140
139
|
// Skip pages - only ping for posts
|
|
141
140
|
if (post.type === 'page') {
|
|
142
141
|
return;
|
|
@@ -152,11 +151,6 @@ export class IndexNowPingService {
|
|
|
152
151
|
return;
|
|
153
152
|
}
|
|
154
153
|
|
|
155
|
-
// Skip if IndexNow is not enabled in labs
|
|
156
|
-
if (!this.labs.isSet('indexnow')) {
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
154
|
// Don't ping for the default posts
|
|
161
155
|
if (post.slug && defaultPostSlugs.indexOf(post.slug) > -1) {
|
|
162
156
|
return;
|
|
@@ -1256,6 +1256,11 @@ module.exports = class MemberRepository {
|
|
|
1256
1256
|
};
|
|
1257
1257
|
let eventData = {};
|
|
1258
1258
|
|
|
1259
|
+
// A cancellation (or reactivation) changes no `members` column, so the member
|
|
1260
|
+
// model event would be suppressed by `wasChanged()`. Remember the pre-update
|
|
1261
|
+
// subscription so the event can be marked and carry its prior state.
|
|
1262
|
+
let subscriptionBeforeCancelFlagChange = null;
|
|
1263
|
+
|
|
1259
1264
|
const stripeCustomerSubscriptionModelShouldBeDeleted = stripeSubscriptionData.metadata && !!stripeSubscriptionData.metadata.ghost_migrated_to && stripeSubscriptionData.status === 'canceled';
|
|
1260
1265
|
if (stripeCustomerSubscriptionModelShouldBeDeleted) {
|
|
1261
1266
|
logging.warn(`Subscription ${subscriptionData.subscription_id} is marked for deletion, skipping linking.`);
|
|
@@ -1300,6 +1305,10 @@ module.exports = class MemberRepository {
|
|
|
1300
1305
|
this.dispatchEvent(offerRedemptionEvent, options);
|
|
1301
1306
|
}
|
|
1302
1307
|
|
|
1308
|
+
if (stripeCustomerSubscriptionModel.get('cancel_at_period_end') !== updatedStripeCustomerSubscriptionModel.get('cancel_at_period_end')) {
|
|
1309
|
+
subscriptionBeforeCancelFlagChange = stripeCustomerSubscriptionModel;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1303
1312
|
if (stripeCustomerSubscriptionModel.get('mrr') !== updatedStripeCustomerSubscriptionModel.get('mrr') || stripeCustomerSubscriptionModel.get('plan_id') !== updatedStripeCustomerSubscriptionModel.get('plan_id') || stripeCustomerSubscriptionModel.get('status') !== updatedStripeCustomerSubscriptionModel.get('status') || stripeCustomerSubscriptionModel.get('cancel_at_period_end') !== updatedStripeCustomerSubscriptionModel.get('cancel_at_period_end')) {
|
|
1304
1313
|
const originalMrrDelta = stripeCustomerSubscriptionModel.get('mrr');
|
|
1305
1314
|
const updatedMrrDelta = updatedStripeCustomerSubscriptionModel.get('mrr');
|
|
@@ -1550,6 +1559,48 @@ module.exports = class MemberRepository {
|
|
|
1550
1559
|
updatedMember = await this._Member.edit({status: status}, {...options, id: data.id});
|
|
1551
1560
|
}
|
|
1552
1561
|
|
|
1562
|
+
// Cancelling (or reactivating) touches no `members` column, so mark the
|
|
1563
|
+
// subscription relation as the change and carry its prior state. This lets the
|
|
1564
|
+
// webhook payload express before/after — see `services/webhooks/serialize.js`.
|
|
1565
|
+
//
|
|
1566
|
+
// No explicit `emitChange` is needed, and adding one would emit twice: inside a
|
|
1567
|
+
// transaction `emitChange` queues onto the `committed` handler and defers its
|
|
1568
|
+
// `wasChanged()` check to commit time (see `models/base/plugins/events.js`), so
|
|
1569
|
+
// the event already queued by `_Member.edit` above is still pending and setting
|
|
1570
|
+
// `_changed` here is what lets it through. The e2e test asserting exactly one
|
|
1571
|
+
// `member.edited` per cancellation guards this.
|
|
1572
|
+
if (subscriptionBeforeCancelFlagChange && updatedMember) {
|
|
1573
|
+
// Price/tier relations are needed for the serialized subscription shape
|
|
1574
|
+
// to match the Admin API member resource (price, tier, plan)
|
|
1575
|
+
await updatedMember.load([
|
|
1576
|
+
'stripeSubscriptions',
|
|
1577
|
+
'stripeSubscriptions.stripePrice',
|
|
1578
|
+
'stripeSubscriptions.stripePrice.stripeProduct',
|
|
1579
|
+
'stripeSubscriptions.stripePrice.stripeProduct.product'
|
|
1580
|
+
], options);
|
|
1581
|
+
await subscriptionBeforeCancelFlagChange.load([
|
|
1582
|
+
'stripePrice',
|
|
1583
|
+
'stripePrice.stripeProduct',
|
|
1584
|
+
'stripePrice.stripeProduct.product'
|
|
1585
|
+
], options);
|
|
1586
|
+
updatedMember._changed = {
|
|
1587
|
+
...updatedMember._changed,
|
|
1588
|
+
stripeSubscriptions: {
|
|
1589
|
+
cancel_at_period_end: subscriptionBeforeCancelFlagChange.get('cancel_at_period_end')
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
// `previous` must be the full collection — a member can have multiple
|
|
1593
|
+
// subscriptions — with only the changed one swapped for its prior state
|
|
1594
|
+
updatedMember._previousRelations = {
|
|
1595
|
+
...updatedMember._previousRelations,
|
|
1596
|
+
stripeSubscriptions: {
|
|
1597
|
+
models: updatedMember.related('stripeSubscriptions').models.map((subscription) => {
|
|
1598
|
+
return subscription.id === subscriptionBeforeCancelFlagChange.id ? subscriptionBeforeCancelFlagChange : subscription;
|
|
1599
|
+
})
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1553
1604
|
const newMemberProductIds = memberProducts.map(product => product.id);
|
|
1554
1605
|
const oldMemberProductIds = oldMemberProducts.map(product => product.id);
|
|
1555
1606
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const got = require('got').default;
|
|
2
1
|
const validator = require('@tryghost/validator');
|
|
3
2
|
const errors = require('@tryghost/errors');
|
|
3
|
+
const externalRequest = require('../../lib/request-external');
|
|
4
4
|
const ghostVersion = require('@tryghost/version');
|
|
5
5
|
const moment = require('moment');
|
|
6
6
|
|
|
@@ -178,7 +178,7 @@ class SlackNotifications {
|
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
return await
|
|
181
|
+
return await externalRequest.post(url, requestOptions);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
/**
|
|
@@ -12,7 +12,7 @@ class SlackPingServiceWrapper {
|
|
|
12
12
|
const { blogIcon } = require('../../lib/image');
|
|
13
13
|
const events = require('../../lib/common/events');
|
|
14
14
|
const logging = require('@tryghost/logging');
|
|
15
|
-
const request = require('
|
|
15
|
+
const request = require('../../lib/request-external');
|
|
16
16
|
const settingsCache = require('../../../shared/settings-cache');
|
|
17
17
|
const urlService = require('../url');
|
|
18
18
|
const urlUtils = require('../../../shared/url-utils').default;
|
|
@@ -13,7 +13,7 @@ class SlackPingServiceWrapper {
|
|
|
13
13
|
const {blogIcon} = require('../../lib/image');
|
|
14
14
|
const events = require('../../lib/common/events');
|
|
15
15
|
const logging = require('@tryghost/logging');
|
|
16
|
-
const request = require('
|
|
16
|
+
const request = require('../../lib/request-external');
|
|
17
17
|
const settingsCache = require('../../../shared/settings-cache');
|
|
18
18
|
const urlService = require('../url');
|
|
19
19
|
const urlUtils = require('../../../shared/url-utils').default;
|
|
@@ -111,13 +111,6 @@ class MembersStatsService {
|
|
|
111
111
|
const startDateMoment = moment.utc(startDate).startOf('day');
|
|
112
112
|
const endDateMoment = moment.utc(today).startOf('day');
|
|
113
113
|
|
|
114
|
-
// Create a map of events by date for fast lookup
|
|
115
|
-
const eventsMap = new Map();
|
|
116
|
-
rows.forEach((row) => {
|
|
117
|
-
const date = moment(row.date).format('YYYY-MM-DD');
|
|
118
|
-
eventsMap.set(date, row);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
114
|
// Sort rows chronologically to calculate historical totals
|
|
122
115
|
rows.sort((a, b) => moment(a.date).valueOf() - moment(b.date).valueOf());
|
|
123
116
|
|