ghost 6.17.1 → 6.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tryghost-i18n-6.18.0.tgz +0 -0
- package/components/{tryghost-parse-email-address-6.17.1.tgz → tryghost-parse-email-address-6.18.0.tgz} +0 -0
- package/core/built/admin/assets/{PolarAngleAxis-DVj7Sw2n.js → PolarAngleAxis-Dod0DwfL.js} +1 -1
- package/core/built/admin/assets/{_baseAssignValue-CrCkbXur.js → _baseAssignValue-DnkbkowM.js} +1 -1
- package/core/built/admin/assets/{a-large-small-BLquRSNr.js → a-large-small-C5mgFBRg.js} +1 -1
- package/core/built/admin/assets/activitypub/activitypub.js +1 -1
- package/core/built/admin/assets/activitypub/{at-sign-Fsk3x72r.mjs → at-sign-BR2C2gdz.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{avatar-flipboard-C10JfFS_.mjs → avatar-flipboard-CJV3KhLU.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{bluesky-sharing-C1xeGSL6.mjs → bluesky-sharing-DY4KCGPZ.mjs} +4 -4
- package/core/built/admin/assets/activitypub/{copy-C1fElSkQ.mjs → copy-CBFwbAdE.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{deleted-feed-item-Bun4tY2_.mjs → deleted-feed-item-DOm9GxTQ.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{edit-profile-CYh00FZ7.mjs → edit-profile-CcbnZWkM.mjs} +3 -3
- package/core/built/admin/assets/activitypub/{feed-BxUqmcN9.mjs → feed-BHiZU0Q_.mjs} +4 -4
- package/core/built/admin/assets/activitypub/{hash-CNgwAx-U.mjs → hash-Csjloy5t.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-DqNqII4a.mjs → inbox-DGKbBux9.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{index-CONoLlDU.mjs → index-Bt_qFJNY.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{index--Q6orQkb.mjs → index-CCYuVHjm.mjs} +21 -21
- package/core/built/admin/assets/activitypub/{index-jhjmoHwu.mjs → index-CTV39jCH.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{index-Dvh9q3jy.mjs → index-ClNx7qzi.mjs} +4 -4
- package/core/built/admin/assets/activitypub/{index-C9pnotJK.mjs → index-D6Y6ywyl.mjs} +6 -6
- package/core/built/admin/assets/activitypub/{index-BueIufRq.mjs → index-YLvIz6pJ.mjs} +5 -5
- package/core/built/admin/assets/activitypub/{index-C3KJXzZE.mjs → index-tWANcGjH.mjs} +7 -7
- package/core/built/admin/assets/activitypub/{index-CJJXnqq1.mjs → index-vVqFEPIX.mjs} +3 -3
- package/core/built/admin/assets/activitypub/{moderation-CYhwUFi2.mjs → moderation-CUGqRfT1.mjs} +3 -3
- package/core/built/admin/assets/activitypub/{note-COVa8CMw.mjs → note-DMy39JHG.mjs} +4 -4
- package/core/built/admin/assets/activitypub/{reply-BHpKVBxx.mjs → reply-BboFcDsv.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{separator-DP7q5sFH.mjs → separator-DBvIXQnV.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{settings-3n7zo_3K.mjs → settings-D4qRhs9E.mjs} +3 -3
- package/core/built/admin/assets/activitypub/{step-1-BmUukywZ.mjs → step-1-C-ubwwIT.mjs} +3 -3
- package/core/built/admin/assets/activitypub/{step-2-C--I3xxp.mjs → step-2-Dsd2XY1_.mjs} +5 -5
- package/core/built/admin/assets/activitypub/{step-3-0Deh5N9c.mjs → step-3-BHz3ow0t.mjs} +7 -7
- package/core/built/admin/assets/activitypub/{tabs-D_vmoLBo.mjs → tabs-s_P4XeyF.mjs} +3 -3
- package/core/built/admin/assets/activitypub/{topic-filter-DJMrhH-c.mjs → topic-filter-CCf15CR-.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
- package/core/built/admin/assets/admin-x-settings/{code-editor-view-BzvYs1fs.mjs → code-editor-view-C9yQgDwn.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-BXChUyr6.mjs → index-Cb0MgPGv.mjs} +30 -19
- package/core/built/admin/assets/admin-x-settings/{index-DYEF3NiM.mjs → index-DGoxlHIt.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-DdMo_pvD.mjs → index-jzFpi950.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-Ck3AHDx3.mjs → modals-CCI0FGRh.mjs} +13 -12
- package/core/built/admin/assets/{at-sign-Cj4_ORaa.js → at-sign-Bz-SU-S_.js} +1 -1
- package/core/built/admin/assets/{audience-ACTBvUSt.js → audience-DmVdEmIe.js} +1 -1
- package/core/built/admin/assets/{avatar-flipboard-BeKL4BEF.js → avatar-flipboard-C7sxDVEM.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-CP7ej5rz.js → bluesky-sharing-DJniSF6N.js} +1 -1
- package/core/built/admin/assets/{chart-B6jk72FT.js → chart-mYz3IJwm.js} +1 -1
- package/core/built/admin/assets/{chunk.524.6c12997c856012ea06f5.js → chunk.524.d2c4d1c78e72685d04c9.js} +6 -6
- package/core/built/admin/assets/{chunk.582.a0abd5b93eb1bc50335a.js → chunk.582.a3333324fa5a373a6148.js} +8 -8
- package/core/built/admin/assets/{code-editor-view-CVRtsOBM.js → code-editor-view-BIQWFJ01.js} +1 -1
- package/core/built/admin/assets/comments-B_-hdjc6.js +1 -0
- package/core/built/admin/assets/{copy-YhtJexiS.js → copy-Bpq3uhnh.js} +1 -1
- package/core/built/admin/assets/{data-list-BPSXm3ej.js → data-list-BYNMbRIq.js} +1 -1
- package/core/built/admin/assets/{deleted-feed-item-D8BHDEHh.js → deleted-feed-item-B_AwUKZy.js} +1 -1
- package/core/built/admin/assets/{edit-profile-FrkkXMG7.js → edit-profile-DZo2ZcOu.js} +1 -1
- package/core/built/admin/assets/{empty-indicator-SsCyCd50.js → empty-indicator-Bn5wG9-T.js} +1 -1
- package/core/built/admin/assets/{en-ZWZBSdFY.js → en-D4zIrMLN.js} +1 -1
- package/core/built/admin/assets/{feed-BvVJCTy8.js → feed-Bgq4RarU.js} +1 -1
- package/core/built/admin/assets/{filters-BupJ4mUj.js → filters-CpVwu1Gk.js} +1 -1
- package/core/built/admin/assets/{gh-chart-Dp9VXeON.js → gh-chart-Br7NXn_b.js} +1 -1
- package/core/built/admin/assets/{ghost-78b420974adc056bf1c9570275a46535.css → ghost-3c48d37b32f4fcd7eb15cfd739905a03.css} +1 -1
- package/core/built/admin/assets/{ghost-a9268074d0a9b02198594a05ecd7a626.js → ghost-98a3c5fd6235ebd344594f491cb6d17b.js} +241 -246
- package/core/built/admin/assets/{ghost-dark-12abca67e79c76fe6bfed6cb81ba2a53.css → ghost-dark-0100fe3ec7b4c3f6d8d043c904254a63.css} +1 -1
- package/core/built/admin/assets/{growth-CRDHOGrZ.js → growth-CiPDnWxJ.js} +1 -1
- package/core/built/admin/assets/{hash-CPAX2PwN.js → hash-Bc1e38oH.js} +1 -1
- package/core/built/admin/assets/{inbox-DuT2edmx.js → inbox-BcZNBNhk.js} +1 -1
- package/core/built/admin/assets/{index-CQP2hNBo.js → index-BFEx_ouC.js} +1 -1
- package/core/built/admin/assets/{index-ByYZ5TiM.js → index-B_7QsC5T.js} +1 -1
- package/core/built/admin/assets/{index-BX3kQzlo.js → index-Bm5ZeLtt.js} +1 -1
- package/core/built/admin/assets/{index-Dk-Wh_Fw.js → index-BpcL7RmI.js} +2 -2
- package/core/built/admin/assets/{index--Nwo3958.js → index-Brg4tecQ.js} +1 -1
- package/core/built/admin/assets/{index-exFCQBCk.js → index-CMTzNTew.js} +29 -29
- package/core/built/admin/assets/{index-8ECl7z0l.js → index-CTfJflJ2.js} +1 -1
- package/core/built/admin/assets/{index-B2oDZJ9B.js → index-Cn32t_gv.js} +1 -1
- package/core/built/admin/assets/{index-BwUQiVrX.js → index-CsidX7Si.js} +1 -1
- package/core/built/admin/assets/{index-DnY4rls4.js → index-D212VvOz.js} +1 -1
- package/core/built/admin/assets/{index-bl5YmBtG.js → index-D67LJ_H4.js} +1 -1
- package/core/built/admin/assets/{index-BPYePamp.js → index-DU3bv9jz.js} +1 -1
- package/core/built/admin/assets/{index-CjzFwwKZ.js → index-Dv13wKfX.js} +1 -1
- package/core/built/admin/assets/{index-B1rQYn0h.js → index-ETsoQLbU.js} +1 -1
- package/core/built/admin/assets/{index-CeFAaI2v.js → index-ZYDRMgcT.js} +1 -1
- package/core/built/admin/assets/{koenig-lexical-eRS1y7PS.js → koenig-lexical-DZUWzN0P.js} +1 -1
- package/core/built/admin/assets/{kpi-card-DShFKtte.js → kpi-card-DENsK2xK.js} +1 -1
- package/core/built/admin/assets/{kpis-Bxyr_zYY.js → kpis-CDrs2iS1.js} +1 -1
- package/core/built/admin/assets/{label-hgdYQhmm.js → label-BznQtEEo.js} +1 -1
- package/core/built/admin/assets/{links-5NNDFN65.js → links-lpAC3T1p.js} +1 -1
- package/core/built/admin/assets/{lucide-react-f58FO8MB.js → lucide-react-CBigk-fq.js} +1 -1
- package/core/built/admin/assets/{main-layout-BDz_2UZs.js → main-layout-_SYQRjIl.js} +1 -1
- package/core/built/admin/assets/{message-square-text-6dTKbQKA.js → message-square-text-cV8O_qKq.js} +1 -1
- package/core/built/admin/assets/{minus-D2C19Fiu.js → minus-NvnQTlW7.js} +1 -1
- package/core/built/admin/assets/{modals-DzetQgXf.js → modals-XRSkribf.js} +3 -3
- package/core/built/admin/assets/{moderation-87QsKbyf.js → moderation-BQp1GEWG.js} +1 -1
- package/core/built/admin/assets/{newsletter-BhI9Lgkn.js → newsletter-foM6KNNV.js} +1 -1
- package/core/built/admin/assets/{newsletters-adLgF_Re.js → newsletters-BexdXUhn.js} +1 -1
- package/core/built/admin/assets/{note-BbPR57Yw.js → note-DuaUGOeZ.js} +1 -1
- package/core/built/admin/assets/{overview-CqyRlIVM.js → overview-DgKBNqyc.js} +1 -1
- package/core/built/admin/assets/{pagemenu-DlQF5jN4.js → pagemenu-CZyroidv.js} +1 -1
- package/core/built/admin/assets/{post-analytics-BudH7GCA.js → post-analytics-DLK2SOSQ.js} +1 -1
- package/core/built/admin/assets/{post-analytics-context-CxuzZluw.js → post-analytics-context-CF7C67-0.js} +1 -1
- package/core/built/admin/assets/{post-analytics-header-BV_8onIF.js → post-analytics-header-7GtJCx0W.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-CnM21JFK.js → post-share-modal-D8R7DUZP.js} +1 -1
- package/core/built/admin/assets/posts/{comments-CHEqbZPq.mjs → comments-Bn_vn_sb.mjs} +434 -407
- package/core/built/admin/assets/posts/{dialog-VFomJYla.mjs → dialog-CKPcMrj7.mjs} +3 -3
- package/core/built/admin/assets/posts/{empty-indicator-CT9qQsMO.mjs → empty-indicator-kkrhP6K_.mjs} +2 -2
- package/core/built/admin/assets/posts/{filters-BXCfSKKS.mjs → filters-DHsxxy0F.mjs} +6 -6
- package/core/built/admin/assets/posts/{growth-xMitvyvt.mjs → growth-D7ACh-oR.mjs} +12 -12
- package/core/built/admin/assets/posts/{heading-DIulQwgD.mjs → heading-BrKVbOxA.mjs} +2 -2
- package/core/built/admin/assets/posts/{hooks-BiBMDYOA.mjs → hooks-BBEMuLiW.mjs} +3 -3
- package/core/built/admin/assets/posts/{index-uYyMBANY.mjs → index-C0rRguZx.mjs} +9 -9
- package/core/built/admin/assets/posts/{kpis-DK6j0CJA.mjs → kpis-1o524OIK.mjs} +10 -10
- package/core/built/admin/assets/posts/{links-CBiYZsyk.mjs → links-JpeATx1f.mjs} +4 -4
- package/core/built/admin/assets/posts/{loading-indicator-BPPeKpVb.mjs → loading-indicator-BHAmSf8j.mjs} +3 -3
- package/core/built/admin/assets/posts/{main-layout-f-PcNn-6.mjs → main-layout-BTItAOQE.mjs} +2 -2
- package/core/built/admin/assets/posts/{newsletter-CSOyAfvO.mjs → newsletter-CQdsCEHv.mjs} +13 -13
- package/core/built/admin/assets/posts/{overview-Dve3Uxt4.mjs → overview-sYh4JN1D.mjs} +15 -15
- package/core/built/admin/assets/posts/{post-analytics-CPKxH7yg.mjs → post-analytics-DtLt2SWx.mjs} +6 -6
- package/core/built/admin/assets/posts/{post-analytics-context-B5zQoflZ.mjs → post-analytics-context-CB1yM9fk.mjs} +4 -4
- package/core/built/admin/assets/posts/{post-analytics-header-BBxuaptK.mjs → post-analytics-header-BYaWuL9W.mjs} +9 -9
- package/core/built/admin/assets/posts/{post-share-modal-D_iMXvnI.mjs → post-share-modal-V0HTOBaf.mjs} +4 -4
- package/core/built/admin/assets/posts/{posts-DW6ne0od.mjs → posts-DeQT3knv.mjs} +2 -2
- package/core/built/admin/assets/posts/posts.js +1 -1
- package/core/built/admin/assets/posts/{search-CVlPqGMA.mjs → search-CLjC37AT.mjs} +2 -2
- package/core/built/admin/assets/posts/{separator-YrJkDGVP.mjs → separator-KNoTIaJx.mjs} +5 -5
- package/core/built/admin/assets/posts/{sheet-T5GWxxrn.mjs → sheet-DBg_SCDt.mjs} +3 -3
- package/core/built/admin/assets/posts/{skeleton-qnvlkDy-.mjs → skeleton-DvsoolWu.mjs} +3 -3
- package/core/built/admin/assets/posts/{source-icon-DBU0eZwS.mjs → source-icon-OmRaTU2G.mjs} +3 -3
- package/core/built/admin/assets/posts/{stats-udwJVZP8.mjs → stats-BC2DzntY.mjs} +4 -4
- package/core/built/admin/assets/posts/{table-Cu0BZfBU.mjs → table-CxX9OKAj.mjs} +2 -2
- package/core/built/admin/assets/posts/{tabs-B-RaoD3I.mjs → tabs-B1jw7cBi.mjs} +10 -10
- package/core/built/admin/assets/posts/{tags-DSDGzArh.mjs → tags-BBilTo1a.mjs} +11 -11
- package/core/built/admin/assets/posts/{tags-DByfrDKO.mjs → tags-BitrLT_j.mjs} +2 -2
- package/core/built/admin/assets/posts/{use-infinite-virtual-scroll-B7dD2nWW.mjs → use-infinite-virtual-scroll-DaijA1ao.mjs} +3 -3
- package/core/built/admin/assets/posts/{web-B9wvEtJ_.mjs → web-CKmyC4Xj.mjs} +15 -15
- package/core/built/admin/assets/{posts-C5zAbpdG.js → posts-CL9UDYoW.js} +1 -1
- package/core/built/admin/assets/{repeat-BbSnXois.js → repeat-DgH39UKE.js} +1 -1
- package/core/built/admin/assets/{reply-CuCJJfZj.js → reply-DAaNxiy8.js} +1 -1
- package/core/built/admin/assets/{select-Bv-dkHeU.js → select-Cor2wFXT.js} +1 -1
- package/core/built/admin/assets/{settings-Cit2-dRQ.js → settings-BeumESEN.js} +5 -5
- package/core/built/admin/assets/{settings-wlK_oNsg.js → settings-xRx917Gj.js} +1 -1
- package/core/built/admin/assets/{sort-button-Dmyldyzm.js → sort-button-BNW3i4Lb.js} +1 -1
- package/core/built/admin/assets/{source-icon-DjyV6RS6.js → source-icon-DvDuzw73.js} +1 -1
- package/core/built/admin/assets/{sprout-jOQR37se.js → sprout-C3cc0c-K.js} +1 -1
- package/core/built/admin/assets/{square-IrYxuAJL.js → square-tZp0_n7e.js} +1 -1
- package/core/built/admin/assets/stats/{audience-Dfo7BmNu.mjs → audience-BWqU7WWT.mjs} +3 -3
- package/core/built/admin/assets/stats/{index-DUWoiDE_.mjs → index-CUuQaROI.mjs} +5 -5
- package/core/built/admin/assets/stats/{index-Yo58temM.mjs → index-CcCyLMxL.mjs} +7 -7
- package/core/built/admin/assets/stats/{index-h0rsyxDz.mjs → index-D5mlMG4l.mjs} +8 -8
- package/core/built/admin/assets/stats/{index-_vL3Zubi.mjs → index-DXU2rE9t.mjs} +5 -5
- package/core/built/admin/assets/stats/{index-Banm1wtA.mjs → index-K7ASx7EG.mjs} +6 -6
- package/core/built/admin/assets/stats/{sort-button-i6PS-TUn.mjs → sort-button-CELUx6Zp.mjs} +3 -3
- package/core/built/admin/assets/stats/{stats-CjepXEWS.mjs → stats-d_u_in4l.mjs} +2 -2
- package/core/built/admin/assets/stats/stats.js +2 -2
- package/core/built/admin/assets/stats/{tabs-Q20S1oup.mjs → tabs-3wLZsy0v.mjs} +3 -3
- package/core/built/admin/assets/stats/{url-helpers-DrGoeEH1.mjs → url-helpers-Drq3xg0l.mjs} +4 -4
- package/core/built/admin/assets/stats/{use-growth-stats-BnffY2W3.mjs → use-growth-stats-28Sr42va.mjs} +3 -3
- package/core/built/admin/assets/{stats-C4LbW8IV.js → stats-2Jelnn-Q.js} +1 -1
- package/core/built/admin/assets/{stats-view-BwaLDryw.js → stats-view-CESy8ELH.js} +1 -1
- package/core/built/admin/assets/{step-1-C0QApeYD.js → step-1-DrqdolAh.js} +1 -1
- package/core/built/admin/assets/{step-2-h0b6Njyb.js → step-2-DmEpKck5.js} +1 -1
- package/core/built/admin/assets/{step-3-CaM-Hz3F.js → step-3-Bus-0o0n.js} +1 -1
- package/core/built/admin/assets/{table-P4FGxGxN.js → table-BQUcKHfm.js} +1 -1
- package/core/built/admin/assets/{tabs-Cj65mCsJ.js → tabs-BmdL0X4U.js} +1 -1
- package/core/built/admin/assets/{tags-Dnv6zTag.js → tags-CLxXZlOO.js} +1 -1
- package/core/built/admin/assets/{tags-CDvEkvxr.js → tags-EchqlZUJ.js} +1 -1
- package/core/built/admin/assets/{tiers-Y3OCOT03.js → tiers-nCGyTly9.js} +1 -1
- package/core/built/admin/assets/{toggle-group-kZZo_vdJ.js → toggle-group-CM5uf7J1.js} +1 -1
- package/core/built/admin/assets/{topic-filter-Cn1_Z9I7.js → topic-filter-LTRvZ8aU.js} +1 -1
- package/core/built/admin/assets/{trash-ln90Rlgi.js → trash-u5BxolyH.js} +1 -1
- package/core/built/admin/assets/{url-helpers-B5_oMSPb.js → url-helpers-D41fEt51.js} +1 -1
- package/core/built/admin/assets/{use-growth-stats-DQsJauHH.js → use-growth-stats-BJ0O9ewi.js} +1 -1
- package/core/built/admin/assets/{use-infinite-virtual-scroll-4OIpSNMH.js → use-infinite-virtual-scroll-APZWciOk.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-BedA-MMT.js → use-simple-pagination-DVRHeaAR.js} +1 -1
- package/core/built/admin/assets/{user-round-check-CYI147XP.js → user-round-check-B6j98D6d.js} +1 -1
- package/core/built/admin/assets/{wallet-cards-xpilOd6G.js → wallet-cards-KmOh29LP.js} +1 -1
- package/core/built/admin/assets/{web-VPMnLvnh.js → web-Cclotbnz.js} +1 -1
- package/core/built/admin/index.html +5 -5
- package/core/frontend/meta/asset-url.js +126 -15
- package/core/frontend/services/asset-hash/index.js +73 -0
- package/core/frontend/services/theme-engine/active.js +4 -2
- package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -0
- package/core/server/data/migrations/versions/6.17/2026-02-04-10-42-20-offers-nullable-product-id.js +11 -0
- package/core/server/data/migrations/versions/6.18/2026-02-04-10-42-20-offers-nullable-product-id.js +4 -0
- package/core/server/data/schema/schema.js +1 -1
- package/core/server/data/tinybird/datasources/_mv_daily_pages.datasource +13 -0
- package/core/server/data/tinybird/endpoints/api_top_pages_v3.pipe +113 -0
- package/core/server/data/tinybird/pipes/mv_daily_pages.pipe +23 -0
- package/core/server/data/tinybird/scripts/benchmark-top-pages.sh +186 -0
- package/core/server/data/tinybird/scripts/compare-top-pages.sh +262 -0
- package/core/server/data/tinybird/tests/api_top_pages_v3.yaml +112 -0
- package/core/server/models/automated-email.js +34 -0
- package/core/server/services/email-address/email-address-service-wrapper.js +3 -0
- package/core/server/services/email-address/email-address-service.js +11 -0
- package/core/server/services/email-address/email-address-service.ts +13 -0
- package/core/server/services/koenig/node-renderers/transistor-renderer.js +3 -8
- package/core/server/services/lib/member-signup-contexts.js +23 -0
- package/core/server/services/lib/member-signup-contexts.ts +22 -0
- package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +13 -7
- package/core/server/services/member-welcome-emails/service.js +8 -12
- package/core/server/services/members/api.js +3 -1
- package/core/server/services/members/members-api/controllers/router-controller.js +19 -2
- package/core/server/services/members/members-api/members-api.js +4 -2
- package/core/server/services/members/members-api/repositories/member-repository.js +1 -1
- package/core/server/services/members/members-api/services/payments-service.js +5 -0
- package/core/server/services/oembed/oembed-service.js +1 -1
- package/core/server/services/offers/application/offer-mapper.js +6 -7
- package/core/server/services/offers/application/offers-api.js +3 -3
- package/core/server/services/offers/domain/errors/index.js +2 -0
- package/core/server/services/offers/domain/models/offer.js +15 -3
- package/core/server/services/offers/offer-bookshelf-repository.js +4 -5
- package/core/server/services/stats/utils/tinybird.js +5 -3
- package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +17 -1
- package/core/server/services/stripe/stripe-service.js +8 -0
- package/core/shared/config/defaults.json +6 -0
- package/core/shared/labs.js +3 -3
- package/core/shared/url-utils.js +2 -1
- package/package.json +6 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +23 -2
- package/components/tryghost-i18n-6.17.1.tgz +0 -0
- package/core/built/admin/assets/comments-CxjhZH_T.js +0 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# benchmark-top-pages.sh - Compare performance of api_top_pages vs api_top_pages_v3
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./benchmark-top-pages.sh [iterations]
|
|
6
|
+
#
|
|
7
|
+
# Prerequisites: yarn dev:analytics
|
|
8
|
+
|
|
9
|
+
set -e
|
|
10
|
+
|
|
11
|
+
# Configuration
|
|
12
|
+
TB_HOST="${TB_HOST:-http://localhost:7181}"
|
|
13
|
+
ITERATIONS="${1:-5}"
|
|
14
|
+
|
|
15
|
+
# Get site_uuid and token from Docker
|
|
16
|
+
echo "============================================"
|
|
17
|
+
echo "Tinybird Top Pages Benchmark"
|
|
18
|
+
echo "============================================"
|
|
19
|
+
|
|
20
|
+
# Get token
|
|
21
|
+
TB_TOKEN=$(docker run --rm -v ghost-dev_shared-config:/config alpine cat /config/.env.tinybird 2>/dev/null | grep TINYBIRD_ADMIN_TOKEN | cut -d= -f2)
|
|
22
|
+
if [ -z "$TB_TOKEN" ]; then
|
|
23
|
+
echo "Error: Could not find Tinybird token. Is yarn dev:analytics running?"
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Get site_uuid - first try from analytics data, fallback to Ghost database
|
|
28
|
+
SITE_UUID=$(curl -s -H "Authorization: Bearer $TB_TOKEN" \
|
|
29
|
+
"${TB_HOST}/v0/sql?q=SELECT%20site_uuid%20FROM%20_mv_hits%20WHERE%20site_uuid%20!=%20'mock_site_uuid'%20LIMIT%201" | tr -d '\n')
|
|
30
|
+
if [ -z "$SITE_UUID" ] || [ "$SITE_UUID" = "" ]; then
|
|
31
|
+
# Fallback to Ghost database
|
|
32
|
+
SITE_UUID=$(docker exec ghost-dev-mysql mysql -uroot -proot ghost_dev -N -e "SELECT value FROM settings WHERE \`key\`='db_hash'" 2>/dev/null | tr -d '\r\n')
|
|
33
|
+
fi
|
|
34
|
+
if [ -z "$SITE_UUID" ]; then
|
|
35
|
+
echo "Error: Could not get site_uuid"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Calculate date range (last 30 days)
|
|
40
|
+
DATE_TO=$(date +%Y-%m-%d)
|
|
41
|
+
DATE_FROM=$(date -v-30d +%Y-%m-%d 2>/dev/null || date -d '30 days ago' +%Y-%m-%d)
|
|
42
|
+
TIMEZONE="Etc/UTC"
|
|
43
|
+
|
|
44
|
+
echo "Site UUID: $SITE_UUID"
|
|
45
|
+
echo "Date Range: $DATE_FROM to $DATE_TO"
|
|
46
|
+
echo "Iterations: $ITERATIONS"
|
|
47
|
+
echo "============================================"
|
|
48
|
+
echo ""
|
|
49
|
+
|
|
50
|
+
# Check data volume
|
|
51
|
+
echo "Checking data volume..."
|
|
52
|
+
DATA_COUNT=$(curl -s -H "Authorization: Bearer $TB_TOKEN" \
|
|
53
|
+
"${TB_HOST}/v0/pipes/api_top_pages.json?site_uuid=${SITE_UUID}&date_from=${DATE_FROM}&date_to=${DATE_TO}&timezone=${TIMEZONE}&limit=1" \
|
|
54
|
+
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('statistics',{}).get('rows_read', 'unknown'))" 2>/dev/null || echo "unknown")
|
|
55
|
+
echo "Rows in date range: ~${DATA_COUNT}"
|
|
56
|
+
echo ""
|
|
57
|
+
|
|
58
|
+
# Benchmark function
|
|
59
|
+
benchmark() {
|
|
60
|
+
local version=$1
|
|
61
|
+
local label=$2
|
|
62
|
+
local endpoint="api_top_pages"
|
|
63
|
+
|
|
64
|
+
if [ -n "$version" ]; then
|
|
65
|
+
endpoint="${endpoint}_${version}"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
local url="${TB_HOST}/v0/pipes/${endpoint}.json?site_uuid=${SITE_UUID}&date_from=${DATE_FROM}&date_to=${DATE_TO}&timezone=${TIMEZONE}"
|
|
69
|
+
|
|
70
|
+
echo "Testing: $label"
|
|
71
|
+
echo "Endpoint: $endpoint"
|
|
72
|
+
|
|
73
|
+
local times=()
|
|
74
|
+
local total_time=0
|
|
75
|
+
|
|
76
|
+
for i in $(seq 1 $ITERATIONS); do
|
|
77
|
+
# Use curl timing
|
|
78
|
+
local result=$(curl -s -w "%{time_total}" -o /tmp/tb_response.json \
|
|
79
|
+
-H "Authorization: Bearer $TB_TOKEN" \
|
|
80
|
+
"$url")
|
|
81
|
+
|
|
82
|
+
local time_ms=$(python3 -c "print(int(float('$result') * 1000))")
|
|
83
|
+
times+=($time_ms)
|
|
84
|
+
total_time=$((total_time + time_ms))
|
|
85
|
+
|
|
86
|
+
# Check for errors
|
|
87
|
+
local error=$(python3 -c "import json; d=json.load(open('/tmp/tb_response.json')); print(d.get('error',''))" 2>/dev/null)
|
|
88
|
+
if [ -n "$error" ]; then
|
|
89
|
+
echo " Iteration $i: ERROR - $error"
|
|
90
|
+
else
|
|
91
|
+
local stats=$(python3 -c "
|
|
92
|
+
import json
|
|
93
|
+
d=json.load(open('/tmp/tb_response.json'))
|
|
94
|
+
rows=len(d.get('data',[]))
|
|
95
|
+
stats=d.get('statistics',{})
|
|
96
|
+
rows_read=stats.get('rows_read',0)
|
|
97
|
+
print(f'{rows}|{rows_read}')" 2>/dev/null)
|
|
98
|
+
local rows=$(echo "$stats" | cut -d'|' -f1)
|
|
99
|
+
local rows_read=$(echo "$stats" | cut -d'|' -f2)
|
|
100
|
+
printf " Iteration %d: %4dms | %d results | %s rows scanned\n" "$i" "$time_ms" "$rows" "$rows_read"
|
|
101
|
+
fi
|
|
102
|
+
done
|
|
103
|
+
|
|
104
|
+
if [ ${#times[@]} -gt 0 ]; then
|
|
105
|
+
local avg=$((total_time / ${#times[@]}))
|
|
106
|
+
|
|
107
|
+
# Calculate min/max
|
|
108
|
+
local min=${times[0]}
|
|
109
|
+
local max=${times[0]}
|
|
110
|
+
for t in "${times[@]}"; do
|
|
111
|
+
[ $t -lt $min ] && min=$t
|
|
112
|
+
[ $t -gt $max ] && max=$t
|
|
113
|
+
done
|
|
114
|
+
|
|
115
|
+
# Get final stats for summary
|
|
116
|
+
local final_rows_read=$(python3 -c "import json; print(json.load(open('/tmp/tb_response.json')).get('statistics',{}).get('rows_read',0))" 2>/dev/null)
|
|
117
|
+
|
|
118
|
+
echo " ----------------------------------------"
|
|
119
|
+
printf " Avg: %dms | Min: %dms | Max: %dms | Rows: %s\n" "$avg" "$min" "$max" "$final_rows_read"
|
|
120
|
+
fi
|
|
121
|
+
echo ""
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Store results for comparison
|
|
125
|
+
V1_STATS=""
|
|
126
|
+
V3_STATS=""
|
|
127
|
+
|
|
128
|
+
run_benchmark() {
|
|
129
|
+
local version=$1
|
|
130
|
+
local label=$2
|
|
131
|
+
benchmark "$version" "$label"
|
|
132
|
+
|
|
133
|
+
# Capture stats from last run
|
|
134
|
+
local stats=$(python3 -c "
|
|
135
|
+
import json
|
|
136
|
+
d=json.load(open('/tmp/tb_response.json'))
|
|
137
|
+
s=d.get('statistics',{})
|
|
138
|
+
print(f\"{s.get('elapsed',0)*1000:.1f}|{s.get('rows_read',0)}\")" 2>/dev/null)
|
|
139
|
+
|
|
140
|
+
if [ "$version" = "" ]; then
|
|
141
|
+
V1_STATS="$stats"
|
|
142
|
+
else
|
|
143
|
+
V3_STATS="$stats"
|
|
144
|
+
fi
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Run benchmarks
|
|
148
|
+
run_benchmark "" "api_top_pages (unversioned)"
|
|
149
|
+
run_benchmark "v3" "api_top_pages_v3 (daily MV)"
|
|
150
|
+
|
|
151
|
+
# Summary comparison
|
|
152
|
+
echo "============================================"
|
|
153
|
+
echo "Performance Comparison Summary"
|
|
154
|
+
echo "============================================"
|
|
155
|
+
|
|
156
|
+
V1_TIME=$(echo "$V1_STATS" | cut -d'|' -f1)
|
|
157
|
+
V1_ROWS=$(echo "$V1_STATS" | cut -d'|' -f2)
|
|
158
|
+
V3_TIME=$(echo "$V3_STATS" | cut -d'|' -f1)
|
|
159
|
+
V3_ROWS=$(echo "$V3_STATS" | cut -d'|' -f2)
|
|
160
|
+
|
|
161
|
+
python3 << PYTHON_SUMMARY
|
|
162
|
+
v1_time = float("$V1_TIME")
|
|
163
|
+
v1_rows = int("$V1_ROWS")
|
|
164
|
+
v3_time = float("$V3_TIME")
|
|
165
|
+
v3_rows = int("$V3_ROWS")
|
|
166
|
+
|
|
167
|
+
print(f"api_top_pages: {v1_time:>8.1f}ms | {v1_rows:>12,} rows scanned")
|
|
168
|
+
print(f"api_top_pages_v3: {v3_time:>8.1f}ms | {v3_rows:>12,} rows scanned")
|
|
169
|
+
print()
|
|
170
|
+
|
|
171
|
+
if v1_time > 0 and v3_time > 0:
|
|
172
|
+
time_ratio = v1_time / v3_time
|
|
173
|
+
if time_ratio >= 1:
|
|
174
|
+
print(f"Speed: v3 is {time_ratio:.1f}x faster")
|
|
175
|
+
else:
|
|
176
|
+
print(f"Speed: v1 is {1/time_ratio:.1f}x faster")
|
|
177
|
+
|
|
178
|
+
if v1_rows > 0 and v3_rows > 0:
|
|
179
|
+
rows_ratio = v1_rows / v3_rows
|
|
180
|
+
print(f"Efficiency: v3 scans {rows_ratio:.1f}x fewer rows")
|
|
181
|
+
PYTHON_SUMMARY
|
|
182
|
+
|
|
183
|
+
echo "============================================"
|
|
184
|
+
|
|
185
|
+
# Cleanup
|
|
186
|
+
rm -f /tmp/tb_response.json
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# compare-top-pages.sh - Validate api_top_pages vs api_top_pages_v3 results
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./compare-top-pages.sh [-l limit] [-d days] [-t tolerance] [-v]
|
|
6
|
+
#
|
|
7
|
+
# Prerequisites: yarn dev:analytics
|
|
8
|
+
#
|
|
9
|
+
# Expected: v3 counts may be 1-2 lower for pages where sessions cross midnight
|
|
10
|
+
# at date boundaries. This is correct behavior - v1 over-counts by including
|
|
11
|
+
# page views outside the date range when sessions span multiple days.
|
|
12
|
+
|
|
13
|
+
set -e
|
|
14
|
+
|
|
15
|
+
# Default configuration
|
|
16
|
+
TB_HOST="${TB_HOST:-http://localhost:7181}"
|
|
17
|
+
LIMIT=50
|
|
18
|
+
DAYS=30
|
|
19
|
+
TOLERANCE=5
|
|
20
|
+
VERBOSE=false
|
|
21
|
+
|
|
22
|
+
# Parse arguments
|
|
23
|
+
while [[ $# -gt 0 ]]; do
|
|
24
|
+
case $1 in
|
|
25
|
+
-l|--limit)
|
|
26
|
+
LIMIT="$2"
|
|
27
|
+
shift 2
|
|
28
|
+
;;
|
|
29
|
+
-d|--days)
|
|
30
|
+
DAYS="$2"
|
|
31
|
+
shift 2
|
|
32
|
+
;;
|
|
33
|
+
-t|--tolerance)
|
|
34
|
+
TOLERANCE="$2"
|
|
35
|
+
shift 2
|
|
36
|
+
;;
|
|
37
|
+
-v|--verbose)
|
|
38
|
+
VERBOSE=true
|
|
39
|
+
shift
|
|
40
|
+
;;
|
|
41
|
+
-h|--help)
|
|
42
|
+
head -20 "$0" | tail -17
|
|
43
|
+
exit 0
|
|
44
|
+
;;
|
|
45
|
+
*)
|
|
46
|
+
echo "Unknown option: $1"
|
|
47
|
+
exit 1
|
|
48
|
+
;;
|
|
49
|
+
esac
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
echo "============================================"
|
|
53
|
+
echo "Tinybird Top Pages Comparison"
|
|
54
|
+
echo "============================================"
|
|
55
|
+
|
|
56
|
+
# Get token
|
|
57
|
+
TB_TOKEN=$(docker run --rm -v ghost-dev_shared-config:/config alpine cat /config/.env.tinybird 2>/dev/null | grep TINYBIRD_ADMIN_TOKEN | cut -d= -f2)
|
|
58
|
+
if [ -z "$TB_TOKEN" ]; then
|
|
59
|
+
echo "Error: Could not find Tinybird token. Is yarn dev:analytics running?"
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Get site_uuid from analytics data (check what's actually in the data)
|
|
64
|
+
SITE_UUID=$(curl -s -H "Authorization: Bearer $TB_TOKEN" \
|
|
65
|
+
"${TB_HOST}/v0/sql?q=SELECT%20site_uuid%20FROM%20_mv_hits%20WHERE%20site_uuid%20!=%20'mock_site_uuid'%20LIMIT%201" | tr -d '\n')
|
|
66
|
+
|
|
67
|
+
if [ -z "$SITE_UUID" ] || [ "$SITE_UUID" = "" ]; then
|
|
68
|
+
echo "Error: Could not get site_uuid from analytics data"
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# Calculate date range
|
|
73
|
+
DATE_TO=$(date +%Y-%m-%d)
|
|
74
|
+
DATE_FROM=$(date -v-${DAYS}d +%Y-%m-%d 2>/dev/null || date -d "${DAYS} days ago" +%Y-%m-%d)
|
|
75
|
+
TIMEZONE="Etc/UTC"
|
|
76
|
+
|
|
77
|
+
echo "Site UUID: $SITE_UUID"
|
|
78
|
+
echo "Date Range: $DATE_FROM to $DATE_TO"
|
|
79
|
+
echo "Limit: $LIMIT results"
|
|
80
|
+
echo "Tolerance: ${TOLERANCE}%"
|
|
81
|
+
echo "============================================"
|
|
82
|
+
echo ""
|
|
83
|
+
|
|
84
|
+
# Create temp files
|
|
85
|
+
V1_RESPONSE=$(mktemp)
|
|
86
|
+
V3_RESPONSE=$(mktemp)
|
|
87
|
+
|
|
88
|
+
cleanup() {
|
|
89
|
+
rm -f "$V1_RESPONSE" "$V3_RESPONSE"
|
|
90
|
+
}
|
|
91
|
+
trap cleanup EXIT
|
|
92
|
+
|
|
93
|
+
# Fetch from both endpoints
|
|
94
|
+
echo "Fetching from api_top_pages..."
|
|
95
|
+
curl -s -H "Authorization: Bearer $TB_TOKEN" \
|
|
96
|
+
"${TB_HOST}/v0/pipes/api_top_pages.json?site_uuid=${SITE_UUID}&date_from=${DATE_FROM}&date_to=${DATE_TO}&timezone=${TIMEZONE}&limit=${LIMIT}" \
|
|
97
|
+
> "$V1_RESPONSE"
|
|
98
|
+
|
|
99
|
+
echo "Fetching from api_top_pages_v3..."
|
|
100
|
+
curl -s -H "Authorization: Bearer $TB_TOKEN" \
|
|
101
|
+
"${TB_HOST}/v0/pipes/api_top_pages_v3.json?site_uuid=${SITE_UUID}&date_from=${DATE_FROM}&date_to=${DATE_TO}&timezone=${TIMEZONE}&limit=${LIMIT}" \
|
|
102
|
+
> "$V3_RESPONSE"
|
|
103
|
+
|
|
104
|
+
# Check for errors
|
|
105
|
+
V1_ERROR=$(python3 -c "import json; d=json.load(open('$V1_RESPONSE')); print(d.get('error',''))" 2>/dev/null)
|
|
106
|
+
V3_ERROR=$(python3 -c "import json; d=json.load(open('$V3_RESPONSE')); print(d.get('error',''))" 2>/dev/null)
|
|
107
|
+
|
|
108
|
+
if [ -n "$V1_ERROR" ]; then
|
|
109
|
+
echo "Error from api_top_pages: $V1_ERROR"
|
|
110
|
+
exit 1
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
if [ -n "$V3_ERROR" ]; then
|
|
114
|
+
echo "Error from api_top_pages_v3: $V3_ERROR"
|
|
115
|
+
exit 1
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
echo ""
|
|
119
|
+
|
|
120
|
+
# Compare results using Python
|
|
121
|
+
export V1_RESPONSE V3_RESPONSE TOLERANCE VERBOSE
|
|
122
|
+
python3 << 'PYTHON_SCRIPT'
|
|
123
|
+
import json
|
|
124
|
+
import sys
|
|
125
|
+
import os
|
|
126
|
+
|
|
127
|
+
v1_file = os.environ['V1_RESPONSE']
|
|
128
|
+
v3_file = os.environ['V3_RESPONSE']
|
|
129
|
+
tolerance = float(os.environ['TOLERANCE'])
|
|
130
|
+
verbose = os.environ['VERBOSE'] == 'true'
|
|
131
|
+
|
|
132
|
+
with open(v1_file) as f:
|
|
133
|
+
v1_data = json.load(f)
|
|
134
|
+
|
|
135
|
+
with open(v3_file) as f:
|
|
136
|
+
v3_data = json.load(f)
|
|
137
|
+
|
|
138
|
+
v1_results = {(r['pathname'], r['post_uuid']): r['visits'] for r in v1_data['data']}
|
|
139
|
+
v3_results = {(r['pathname'], r['post_uuid']): r['visits'] for r in v3_data['data']}
|
|
140
|
+
|
|
141
|
+
# Statistics
|
|
142
|
+
v1_stats = v1_data.get('statistics', {})
|
|
143
|
+
v3_stats = v3_data.get('statistics', {})
|
|
144
|
+
|
|
145
|
+
print("=== Performance Statistics ===")
|
|
146
|
+
print(f"api_top_pages: {v1_stats.get('elapsed', 0)*1000:.2f}ms | {v1_stats.get('rows_read', 0):,} rows read")
|
|
147
|
+
print(f"api_top_pages_v3: {v3_stats.get('elapsed', 0)*1000:.2f}ms | {v3_stats.get('rows_read', 0):,} rows read")
|
|
148
|
+
print()
|
|
149
|
+
|
|
150
|
+
# Combine all keys
|
|
151
|
+
all_keys = set(v1_results.keys()) | set(v3_results.keys())
|
|
152
|
+
|
|
153
|
+
differences = []
|
|
154
|
+
matches = []
|
|
155
|
+
only_in_v1 = []
|
|
156
|
+
only_in_v3 = []
|
|
157
|
+
|
|
158
|
+
for key in all_keys:
|
|
159
|
+
pathname, post_uuid = key
|
|
160
|
+
v1_val = v1_results.get(key)
|
|
161
|
+
v3_val = v3_results.get(key)
|
|
162
|
+
|
|
163
|
+
if v1_val is None:
|
|
164
|
+
only_in_v3.append((pathname, post_uuid, v3_val))
|
|
165
|
+
elif v3_val is None:
|
|
166
|
+
only_in_v1.append((pathname, post_uuid, v1_val))
|
|
167
|
+
else:
|
|
168
|
+
diff = abs(v1_val - v3_val)
|
|
169
|
+
diff_pct = (diff / max(v1_val, 1)) * 100
|
|
170
|
+
|
|
171
|
+
if diff > 0:
|
|
172
|
+
differences.append({
|
|
173
|
+
'pathname': pathname,
|
|
174
|
+
'post_uuid': post_uuid,
|
|
175
|
+
'v1': v1_val,
|
|
176
|
+
'v3': v3_val,
|
|
177
|
+
'diff': diff,
|
|
178
|
+
'diff_pct': diff_pct
|
|
179
|
+
})
|
|
180
|
+
else:
|
|
181
|
+
matches.append({
|
|
182
|
+
'pathname': pathname,
|
|
183
|
+
'post_uuid': post_uuid,
|
|
184
|
+
'visits': v1_val
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
# Sort differences by absolute difference
|
|
188
|
+
differences.sort(key=lambda x: x['diff'], reverse=True)
|
|
189
|
+
|
|
190
|
+
print("=== Comparison Results ===")
|
|
191
|
+
print(f"Total pages in v1: {len(v1_results)}")
|
|
192
|
+
print(f"Total pages in v3: {len(v3_results)}")
|
|
193
|
+
print(f"Exact matches: {len(matches)}")
|
|
194
|
+
print(f"Differences: {len(differences)}")
|
|
195
|
+
print(f"Only in v1: {len(only_in_v1)}")
|
|
196
|
+
print(f"Only in v3: {len(only_in_v3)}")
|
|
197
|
+
print()
|
|
198
|
+
|
|
199
|
+
# Check for significant differences
|
|
200
|
+
significant = [d for d in differences if d['diff_pct'] > tolerance]
|
|
201
|
+
|
|
202
|
+
if differences:
|
|
203
|
+
print("=== Differences Found ===")
|
|
204
|
+
print(f"{'Pathname':<60} {'V1':>8} {'V3':>8} {'Diff':>8} {'Diff%':>8}")
|
|
205
|
+
print("-" * 96)
|
|
206
|
+
|
|
207
|
+
for d in differences[:20]: # Show top 20 differences
|
|
208
|
+
pathname = d['pathname'][:58] if len(d['pathname']) > 58 else d['pathname']
|
|
209
|
+
marker = " *" if d['diff_pct'] > tolerance else ""
|
|
210
|
+
print(f"{pathname:<60} {d['v1']:>8} {d['v3']:>8} {d['diff']:>+8} {d['diff_pct']:>7.2f}%{marker}")
|
|
211
|
+
|
|
212
|
+
if len(differences) > 20:
|
|
213
|
+
print(f"... and {len(differences) - 20} more differences")
|
|
214
|
+
print()
|
|
215
|
+
print("(* = exceeds tolerance threshold)")
|
|
216
|
+
|
|
217
|
+
if only_in_v1:
|
|
218
|
+
print()
|
|
219
|
+
print("=== Pages only in api_top_pages (v1) ===")
|
|
220
|
+
for pathname, post_uuid, visits in only_in_v1[:10]:
|
|
221
|
+
print(f" {pathname:<60} visits: {visits}")
|
|
222
|
+
if len(only_in_v1) > 10:
|
|
223
|
+
print(f" ... and {len(only_in_v1) - 10} more")
|
|
224
|
+
|
|
225
|
+
if only_in_v3:
|
|
226
|
+
print()
|
|
227
|
+
print("=== Pages only in api_top_pages_v3 ===")
|
|
228
|
+
for pathname, post_uuid, visits in only_in_v3[:10]:
|
|
229
|
+
print(f" {pathname:<60} visits: {visits}")
|
|
230
|
+
if len(only_in_v3) > 10:
|
|
231
|
+
print(f" ... and {len(only_in_v3) - 10} more")
|
|
232
|
+
|
|
233
|
+
if verbose and matches:
|
|
234
|
+
print()
|
|
235
|
+
print("=== Exact Matches ===")
|
|
236
|
+
for m in matches[:20]:
|
|
237
|
+
print(f" {m['pathname']:<60} visits: {m['visits']}")
|
|
238
|
+
if len(matches) > 20:
|
|
239
|
+
print(f" ... and {len(matches) - 20} more matches")
|
|
240
|
+
|
|
241
|
+
print()
|
|
242
|
+
print("============================================")
|
|
243
|
+
|
|
244
|
+
# Check for unexpected differences (v3 should always be <= v1)
|
|
245
|
+
unexpected = [d for d in differences if d['v3'] > d['v1']]
|
|
246
|
+
|
|
247
|
+
# Exit status based on differences
|
|
248
|
+
if unexpected:
|
|
249
|
+
print(f"FAIL: {len(unexpected)} pages have v3 > v1 (unexpected - v3 should never exceed v1)")
|
|
250
|
+
sys.exit(1)
|
|
251
|
+
elif significant:
|
|
252
|
+
print(f"WARN: {len(significant)} pages exceed {tolerance}% tolerance (but v3 <= v1 as expected)")
|
|
253
|
+
print(" Small differences are expected due to cross-midnight session handling.")
|
|
254
|
+
sys.exit(0)
|
|
255
|
+
elif differences:
|
|
256
|
+
print(f"OK: {len(differences)} pages have minor differences (within {tolerance}% tolerance)")
|
|
257
|
+
print(" v3 counts are lower as expected (more accurate date filtering).")
|
|
258
|
+
sys.exit(0)
|
|
259
|
+
else:
|
|
260
|
+
print("PASS: All results match exactly!")
|
|
261
|
+
sys.exit(0)
|
|
262
|
+
PYTHON_SCRIPT
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
|
|
2
|
+
- name: Date range
|
|
3
|
+
description: All fixture data - uses pre-aggregated daily MV
|
|
4
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC
|
|
5
|
+
expected_result: |
|
|
6
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":9}
|
|
7
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
|
|
8
|
+
{"post_uuid":"","pathname":"\/","visits":7}
|
|
9
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
|
|
10
|
+
|
|
11
|
+
- name: Filtered by pathname - /about/
|
|
12
|
+
description: Filtered by pathname - /about/
|
|
13
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&pathname=%2Fabout%2F
|
|
14
|
+
expected_result: |
|
|
15
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
|
|
16
|
+
|
|
17
|
+
- name: Filtered by post_uuid - 06b1b0c9-fb53-4a15-a060-3db3fde7b1fc (/about/)
|
|
18
|
+
description: Filtered by post_uuid - 06b1b0c9-fb53-4a15-a060-3db3fde7b1fc (/about/)
|
|
19
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&post_uuid=06b1b0c9-fb53-4a15-a060-3db3fde7b1fc
|
|
20
|
+
expected_result: |
|
|
21
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
|
|
22
|
+
|
|
23
|
+
- name: Filtered by member status - paid
|
|
24
|
+
description: Filtered by member status - paid (includes comped)
|
|
25
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&member_status=paid
|
|
26
|
+
expected_result: |
|
|
27
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":3}
|
|
28
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":3}
|
|
29
|
+
{"post_uuid":"","pathname":"\/","visits":2}
|
|
30
|
+
|
|
31
|
+
- name: Filtered by member status - free
|
|
32
|
+
description: Filtered by member status - free
|
|
33
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&member_status=free
|
|
34
|
+
expected_result: |
|
|
35
|
+
{"post_uuid":"","pathname":"\/","visits":4}
|
|
36
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":3}
|
|
37
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":2}
|
|
38
|
+
|
|
39
|
+
- name: Filtered by member status - undefined
|
|
40
|
+
description: Filtered by member status - undefined
|
|
41
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&member_status=undefined
|
|
42
|
+
expected_result: |
|
|
43
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":4}
|
|
44
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":2}
|
|
45
|
+
{"post_uuid":"","pathname":"\/","visits":1}
|
|
46
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
|
|
47
|
+
|
|
48
|
+
- name: Filtered by timezone - America/Los_Angeles
|
|
49
|
+
description: Filtered by timezone - America/Los_Angeles (8 hours behind UTC)
|
|
50
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=America/Los_Angeles
|
|
51
|
+
expected_result: |
|
|
52
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":9}
|
|
53
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
|
|
54
|
+
{"post_uuid":"","pathname":"\/","visits":7}
|
|
55
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
|
|
56
|
+
|
|
57
|
+
- name: Test with post_type - post
|
|
58
|
+
description: Test with post_type - post
|
|
59
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&post_type=post
|
|
60
|
+
expected_result: |
|
|
61
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":9}
|
|
62
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
|
|
63
|
+
|
|
64
|
+
- name: Test with post_type - page
|
|
65
|
+
description: Test with post_type - page (includes empty post_type)
|
|
66
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&post_type=page
|
|
67
|
+
expected_result: |
|
|
68
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
|
|
69
|
+
{"post_uuid":"","pathname":"\/","visits":7}
|
|
70
|
+
|
|
71
|
+
- name: Test with multiple filters combined - member_status and pathname
|
|
72
|
+
description: Test combining member_status and pathname filters
|
|
73
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&member_status=paid&pathname=%2Fabout%2F
|
|
74
|
+
expected_result: |
|
|
75
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":3}
|
|
76
|
+
|
|
77
|
+
- name: Test with multiple filters combined - post_type and member_status
|
|
78
|
+
description: Test combining post_type and member_status filters
|
|
79
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&post_type=post&member_status=undefined
|
|
80
|
+
expected_result: |
|
|
81
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":4}
|
|
82
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
|
|
83
|
+
|
|
84
|
+
- name: Test pagination - skip
|
|
85
|
+
description: Test pagination with skip parameter
|
|
86
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&skip=1&limit=2
|
|
87
|
+
expected_result: |
|
|
88
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
|
|
89
|
+
{"post_uuid":"","pathname":"\/","visits":7}
|
|
90
|
+
|
|
91
|
+
- name: Test pagination - limit
|
|
92
|
+
description: Test pagination with limit parameter
|
|
93
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&limit=2
|
|
94
|
+
expected_result: |
|
|
95
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":9}
|
|
96
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
|
|
97
|
+
|
|
98
|
+
- name: Single day query
|
|
99
|
+
description: Test querying a single day
|
|
100
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-04&date_to=2100-01-04&timezone=Etc/UTC
|
|
101
|
+
expected_result: |
|
|
102
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":3}
|
|
103
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":2}
|
|
104
|
+
{"post_uuid":"","pathname":"\/","visits":1}
|
|
105
|
+
|
|
106
|
+
- name: Partial date range
|
|
107
|
+
description: Test querying a partial date range (first 3 days)
|
|
108
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-03&timezone=Etc/UTC
|
|
109
|
+
expected_result: |
|
|
110
|
+
{"post_uuid":"","pathname":"\/","visits":5}
|
|
111
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":3}
|
|
112
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":3}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const ghostBookshelf = require('./base');
|
|
2
|
+
const logging = require('@tryghost/logging');
|
|
2
3
|
const urlUtils = require('../../shared/url-utils');
|
|
3
4
|
const lexicalLib = require('../lib/lexical');
|
|
5
|
+
const {MEMBER_WELCOME_EMAIL_SLUGS} = require('../services/member-welcome-emails/constants');
|
|
6
|
+
|
|
7
|
+
const MEMBER_WELCOME_EMAIL_SLUG_SET = new Set(Object.values(MEMBER_WELCOME_EMAIL_SLUGS));
|
|
4
8
|
|
|
5
9
|
const AutomatedEmail = ghostBookshelf.Model.extend({
|
|
6
10
|
tableName: 'automated_emails',
|
|
@@ -33,6 +37,36 @@ const AutomatedEmail = ghostBookshelf.Model.extend({
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
return attrs;
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
onSaved(model) {
|
|
43
|
+
if (!model?.id) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const slug = model.get('slug');
|
|
48
|
+
|
|
49
|
+
if (!MEMBER_WELCOME_EMAIL_SLUG_SET.has(slug)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const previousStatus = model.previous('status');
|
|
54
|
+
const currentStatus = model.get('status');
|
|
55
|
+
const isNewModel = previousStatus === undefined;
|
|
56
|
+
const isEnableTransition = currentStatus === 'active' && (isNewModel || previousStatus === 'inactive');
|
|
57
|
+
const isDisableTransition = previousStatus === 'active' && currentStatus === 'inactive';
|
|
58
|
+
|
|
59
|
+
if (!isEnableTransition && !isDisableTransition) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
logging.info({
|
|
64
|
+
system: {
|
|
65
|
+
event: isEnableTransition ? 'welcome_email.enabled' : 'welcome_email.disabled',
|
|
66
|
+
automated_email_id: model.id,
|
|
67
|
+
slug
|
|
68
|
+
}
|
|
69
|
+
}, isEnableTransition ? 'Welcome email enabled' : 'Welcome email disabled');
|
|
36
70
|
}
|
|
37
71
|
});
|
|
38
72
|
|
|
@@ -33,6 +33,9 @@ class EmailAddressServiceWrapper {
|
|
|
33
33
|
getFallbackEmail: () => {
|
|
34
34
|
return config.get('hostSettings:managedEmail:fallbackAddress') || null;
|
|
35
35
|
},
|
|
36
|
+
getMembersSupportAddress: () => {
|
|
37
|
+
return settingsHelpers.getMembersSupportAddress();
|
|
38
|
+
},
|
|
36
39
|
isValidEmailAddress: (emailAddress) => {
|
|
37
40
|
return validator.isEmail(emailAddress);
|
|
38
41
|
}
|
|
@@ -12,12 +12,14 @@ class EmailAddressService {
|
|
|
12
12
|
#getDefaultEmail;
|
|
13
13
|
#getFallbackDomain;
|
|
14
14
|
#getFallbackEmail;
|
|
15
|
+
#getMembersSupportAddress;
|
|
15
16
|
#isValidEmailAddress;
|
|
16
17
|
constructor(dependencies) {
|
|
17
18
|
this.#getManagedEmailEnabled = dependencies.getManagedEmailEnabled;
|
|
18
19
|
this.#getSendingDomain = dependencies.getSendingDomain;
|
|
19
20
|
this.#getFallbackDomain = dependencies.getFallbackDomain;
|
|
20
21
|
this.#getDefaultEmail = dependencies.getDefaultEmail;
|
|
22
|
+
this.#getMembersSupportAddress = dependencies.getMembersSupportAddress;
|
|
21
23
|
this.#getFallbackEmail = () => {
|
|
22
24
|
const fallbackAddress = dependencies.getFallbackEmail();
|
|
23
25
|
if (!fallbackAddress) {
|
|
@@ -42,6 +44,15 @@ class EmailAddressService {
|
|
|
42
44
|
get fallbackEmail() {
|
|
43
45
|
return this.#getFallbackEmail();
|
|
44
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the actual sender address for member emails after DMARC transformation.
|
|
49
|
+
* On managed platforms, this may differ from the configured support address to ensure DMARC compliance.
|
|
50
|
+
*/
|
|
51
|
+
getMembersSupportAddress() {
|
|
52
|
+
const configuredAddress = this.#getMembersSupportAddress();
|
|
53
|
+
const transformed = this.getAddressFromString(configuredAddress);
|
|
54
|
+
return transformed.from.address;
|
|
55
|
+
}
|
|
45
56
|
getAddressFromString(from, replyTo) {
|
|
46
57
|
const parsedFrom = email_address_parser_js_1.default.parse(from);
|
|
47
58
|
const parsedReplyTo = replyTo ? email_address_parser_js_1.default.parse(replyTo) : undefined;
|