ghost 6.0.10 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/components/tryghost-i18n-6.2.0.tgz +0 -0
  2. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +1 -1
  3. package/core/built/admin/assets/admin-x-activitypub/{index-wBqnq7A5.mjs → index-DmCoswaX.mjs} +2 -2
  4. package/core/built/admin/assets/admin-x-activitypub/{index-XhNX0QuF.mjs → index-lT95Q15h.mjs} +8212 -8183
  5. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-BDBDWpWl.mjs → CodeEditorView-UxqLGRTu.mjs} +3 -3
  6. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  7. package/core/built/admin/assets/admin-x-settings/{index-o4Q9MNrB.mjs → index-8WxO2QXI.mjs} +3017 -2827
  8. package/core/built/admin/assets/admin-x-settings/{index-DsbJfrQ7.mjs → index-B5r0jdJS.mjs} +95 -95
  9. package/core/built/admin/assets/admin-x-settings/{index-BB7hgOf0.mjs → index-Co907MFn.mjs} +2 -2
  10. package/core/built/admin/assets/admin-x-settings/{index-BgCSf8S1.mjs → index-DD3HKlR3.mjs} +306 -315
  11. package/core/built/admin/assets/admin-x-settings/{modals-CCpr5VWU.mjs → modals-B7j9sxR4.mjs} +8799 -8807
  12. package/core/built/admin/assets/{chunk.397.e5d027e53a68dff31d76.js → chunk.397.d5e25bb9baf088f52499.js} +2 -2
  13. package/core/built/admin/assets/{chunk.524.695215c994f8cbf547d3.js → chunk.524.70595796c7b8c6003a2d.js} +7 -7
  14. package/core/built/admin/assets/{chunk.582.a949b80543caba37906c.js → chunk.582.d9b970b71da671ac1b7b.js} +8 -8
  15. package/core/built/admin/assets/{ghost-9c608430440a10746540adb7d2cd0f31.js → ghost-2066304fd0b166e1c16d397dd73ef7b2.js} +39 -35
  16. package/core/built/admin/assets/ghost-49475952d56ffe89bd47ab9d9c64ada8.css +1 -0
  17. package/core/built/admin/assets/ghost-dark-27877727751b91f03261d449d74e33b9.css +1 -0
  18. package/core/built/admin/assets/posts/posts.js +27400 -27361
  19. package/core/built/admin/assets/stats/stats.js +28701 -28674
  20. package/core/built/admin/index.html +5 -5
  21. package/core/frontend/public/member-attribution.min.js +1 -1
  22. package/core/server/api/endpoints/search-index.js +2 -2
  23. package/core/server/api/endpoints/stats.js +10 -4
  24. package/core/server/data/migrations/utils/schema.js +11 -6
  25. package/core/server/data/migrations/versions/6.1/2025-09-11-00-38-13-add-uuid-column-to-tokens.js +8 -0
  26. package/core/server/data/migrations/versions/6.1/2025-09-11-00-39-08-backfill-tokens-uuid.js +19 -0
  27. package/core/server/data/migrations/versions/6.1/2025-09-11-00-39-36-tokens-drop-nullable-uuid.js +4 -0
  28. package/core/server/data/migrations/versions/6.2/2025-09-30-14-28-09-add-utm-fields.js +24 -0
  29. package/core/server/data/schema/commands.js +21 -6
  30. package/core/server/data/schema/schema.js +25 -0
  31. package/core/server/data/tinybird/datasources/_mv_hits.datasource +7 -4
  32. package/core/server/data/tinybird/endpoints/api_top_utm_campaigns.pipe +2 -8
  33. package/core/server/data/tinybird/endpoints/api_top_utm_contents.pipe +2 -8
  34. package/core/server/data/tinybird/endpoints/api_top_utm_mediums.pipe +2 -8
  35. package/core/server/data/tinybird/endpoints/api_top_utm_sources.pipe +2 -8
  36. package/core/server/data/tinybird/endpoints/api_top_utm_terms.pipe +2 -8
  37. package/core/server/data/tinybird/fixtures/analytics_events.ndjson +11 -11
  38. package/core/server/data/tinybird/pipes/mv_hits.pipe +12 -2
  39. package/core/server/data/tinybird/pipes/mv_session_data.pipe +16 -6
  40. package/core/server/data/tinybird/tests/api_top_utm_campaigns.yaml +35 -34
  41. package/core/server/data/tinybird/tests/api_top_utm_contents.yaml +57 -48
  42. package/core/server/data/tinybird/tests/api_top_utm_mediums.yaml +40 -38
  43. package/core/server/data/tinybird/tests/api_top_utm_sources.yaml +59 -39
  44. package/core/server/data/tinybird/tests/api_top_utm_terms.yaml +55 -48
  45. package/core/server/models/single-use-token.js +1 -0
  46. package/core/server/services/email-service/EmailRenderer.js +1 -0
  47. package/core/server/services/email-service/email-templates/template.hbs +6 -0
  48. package/core/server/services/lib/MailgunClient.js +4 -3
  49. package/core/server/services/lib/magic-link/MagicLink.js +9 -9
  50. package/core/server/services/mail/GhostMailer.js +4 -1
  51. package/core/server/services/members/MembersConfigProvider.js +0 -15
  52. package/core/server/services/members/SingleUseTokenProvider.js +8 -8
  53. package/core/server/services/members/emails/signin.js +4 -4
  54. package/core/server/services/stats/MrrStatsService.js +10 -5
  55. package/core/server/services/stats/StatsService.js +2 -2
  56. package/core/shared/config/defaults.json +1 -1
  57. package/package.json +9 -9
  58. package/tsconfig.tsbuildinfo +1 -1
  59. package/yarn.lock +1076 -495
  60. package/components/tryghost-i18n-6.0.10.tgz +0 -0
  61. package/core/built/admin/assets/ghost-a7a53bf80dc45c37ae9c174a0d02a882.css +0 -1
  62. package/core/built/admin/assets/ghost-dark-6e0062029f988d8676e87f22d8e7f4a3.css +0 -1
  63. /package/core/built/admin/assets/{chunk.397.e5d027e53a68dff31d76.js.LICENSE.txt → chunk.397.d5e25bb9baf088f52499.js.LICENSE.txt} +0 -0
@@ -6,7 +6,7 @@
6
6
  <title>Ghost</title>
7
7
 
8
8
 
9
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%226.0%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%2237bd1e3e4d%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%22b272efcbc4%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%2258c4bfa71b%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%22336ecd7f7c%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%22a2e0991d78%22%2C%22adminXActivitypubRemoteConfigUrl%22%3A%22%2F.ghost%2Factivitypub%2Fstable%2Fclient-config%22%7D" />
9
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%226.2%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%2237bd1e3e4d%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%2213355f26c2%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%22d668621a75%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%22cf5b4df358%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%22f5dea8dc06%22%2C%22adminXActivitypubRemoteConfigUrl%22%3A%22%2F.ghost%2Factivitypub%2Fstable%2Fclient-config%22%7D" />
10
10
 
11
11
  <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1, minimal-ui, viewport-fit=cover" />
12
12
  <meta name="pinterest" content="nopin" />
@@ -28,7 +28,7 @@
28
28
  </style>
29
29
 
30
30
  <link integrity="" rel="stylesheet" href="assets/vendor-0ede59da8efb5e28fa929557f7ff7154.css">
31
- <link integrity="" rel="stylesheet" href="assets/ghost-a7a53bf80dc45c37ae9c174a0d02a882.css" title="light">
31
+ <link integrity="" rel="stylesheet" href="assets/ghost-49475952d56ffe89bd47ab9d9c64ada8.css" title="light">
32
32
 
33
33
 
34
34
  </head>
@@ -48,8 +48,8 @@
48
48
  <div id="ember-basic-dropdown-wormhole"></div>
49
49
 
50
50
  <script src="assets/vendor-aed0068cf9b67d042dd23a6343545b7b.js"></script>
51
- <script src="assets/chunk.397.e5d027e53a68dff31d76.js"></script>
52
- <script src="assets/chunk.524.695215c994f8cbf547d3.js"></script>
53
- <script src="assets/ghost-9c608430440a10746540adb7d2cd0f31.js"></script>
51
+ <script src="assets/chunk.397.d5e25bb9baf088f52499.js"></script>
52
+ <script src="assets/chunk.524.70595796c7b8c6003a2d.js"></script>
53
+ <script src="assets/ghost-2066304fd0b166e1c16d397dd73ef7b2.js"></script>
54
54
  </body>
55
55
  </html>
@@ -1 +1 @@
1
- "use strict";(()=>{var m=Object.defineProperty;var y=Object.getOwnPropertyDescriptor;var S=Object.getOwnPropertyNames;var R=Object.prototype.hasOwnProperty;var M=(e,r)=>()=>(e&&(r=e(e=0)),r);var _=(e,r)=>{for(var u in r)m(e,u,{get:r[u],enumerable:!0})},P=(e,r,u,t)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of S(r))!R.call(e,o)&&o!==u&&m(e,o,{get:()=>r[o],enumerable:!(t=y(r,o))||t.enumerable});return e};var T=e=>P(m({},"__esModule",{value:!0}),e);var g={};_(g,{getReferrer:()=>A,parseReferrerData:()=>h});function U(e){let r=e.get("ref"),u=e.get("source"),t=e.get("utm_source"),o=e.get("utm_medium"),i=e.get("utm_term"),s=e.get("utm_campaign"),c=e.get("utm_content");return{source:r||u||t||null,medium:o||null,url:window.document.referrer||null,utmSource:t||null,utmMedium:o||null,utmTerm:i||null,utmCampaign:s||null,utmContent:c||null}}function h(e){let r=new URL(e||window.location.href),u=r.searchParams;return r.hash&&r.hash.includes("#/portal")&&(u=new URL(r.href.replace("/#/portal","")).searchParams),U(u)}function C(e){let{source:r,medium:u,url:t}=e,o=r||u||t||null;if(o)try{let i=new URL(o).hostname,s=window.location.hostname;if(i===s)return null}catch{return o}return o}function A(e){let r=h(e);return C(r)}var d=M(()=>{"use strict"});var b=(d(),T(g)),D=b.parseReferrerData,E=b.getReferrer,p="ghost-history",I=24*60*60*1e3,w=15;(async function(){try{let e=window.sessionStorage,r=e.getItem(p),u=new Date().getTime(),t=[];if(r)try{t=JSON.parse(r)}catch(n){console.warn("[Member Attribution] Error while parsing history",n)}let o=t.findIndex(n=>{if(!n.time||typeof n.time!="number")return!1;let a=u-n.time;return!(isNaN(n.time)||a>I)});o>0?t.splice(0,o):o===-1&&(t=[]);let i;try{i=D(window.location.href)}catch(n){console.error("[Member Attribution] Parsing referrer failed",n),i={source:null,medium:null,url:null}}let s={referrerSource:i.source,referrerMedium:i.medium,utmSource:i.utmSource,utmMedium:i.utmMedium,utmCampaign:i.utmCampaign,utmTerm:i.utmTerm,utmContent:i.utmContent},c;try{c=E(window.location.href),!c&&i.url&&(c=i.url)}catch(n){console.error("[Member Attribution] Getting final referrer failed",n),c=i.url}try{let n=new URL(window.location.href),a=n.searchParams;a.get("attribution_id")&&a.get("attribution_type")&&(t.push({time:u,id:a.get("attribution_id"),type:a.get("attribution_type"),...s,referrerUrl:c}),a.delete("attribution_id"),a.delete("attribution_type"),n.search="?"+a.toString(),window.history.replaceState({},"",`${n.pathname}${n.search}${n.hash}`))}catch(n){console.error("[Member Attribution] Parsing attribution from querystring failed",n)}let l=window.location.pathname;if(t.length===0||t[t.length-1].path!==l)t.push({path:l,time:u,...s,referrerUrl:c});else if(t.length>0){let n=t[t.length-1];n.time=u,Object.entries(s).forEach(([a,f])=>{f&&(n[a]=f)}),c&&(n.referrerUrl=c)}t.length>w&&(t=t.slice(-w)),e.setItem(p,JSON.stringify(t))}catch(e){console.error("[Member Attribution] Failed with error",e)}})();})();
1
+ "use strict";(()=>{var m=Object.defineProperty;var y=Object.getOwnPropertyDescriptor;var S=Object.getOwnPropertyNames;var R=Object.prototype.hasOwnProperty;var M=(e,r)=>()=>(e&&(r=e(e=0)),r);var _=(e,r)=>{for(var u in r)m(e,u,{get:r[u],enumerable:!0})},P=(e,r,u,t)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of S(r))!R.call(e,o)&&o!==u&&m(e,o,{get:()=>r[o],enumerable:!(t=y(r,o))||t.enumerable});return e};var T=e=>P(m({},"__esModule",{value:!0}),e);var g={};_(g,{getReferrer:()=>A,parseReferrerData:()=>h});function U(e){let r=e.get("ref"),u=e.get("source"),t=e.get("utm_source"),o=e.get("utm_medium"),i=e.get("utm_term"),s=e.get("utm_campaign"),c=e.get("utm_content");return{source:r||u||t||null,medium:o||null,url:window.document.referrer||null,utmSource:t||null,utmMedium:o||null,utmTerm:i||null,utmCampaign:s||null,utmContent:c||null}}function h(e){let r=new URL(e||window.location.href),u=r.searchParams;return r.hash&&r.hash.includes("#/portal")&&(u=new URL(r.href.replace("/#/portal","")).searchParams),U(u)}function C(e){let{source:r,medium:u,url:t}=e,o=r||u||t||null;if(o)try{let i=new URL(o).hostname,s=window.location.hostname;if(i===s)return null}catch{return o}return o}function A(e){let r=h(e);return C(r)}var d=M(()=>{"use strict"});var b=(d(),T(g)),D=b.parseReferrerData,E=b.getReferrer,p="ghost-history",I=1440*60*1e3,w=15;(async function(){try{let e=window.sessionStorage,r=e.getItem(p),u=new Date().getTime(),t=[];if(r)try{t=JSON.parse(r)}catch(n){console.warn("[Member Attribution] Error while parsing history",n)}let o=t.findIndex(n=>{if(!n.time||typeof n.time!="number")return!1;let a=u-n.time;return!(isNaN(n.time)||a>I)});o>0?t.splice(0,o):o===-1&&(t=[]);let i;try{i=D(window.location.href)}catch(n){console.error("[Member Attribution] Parsing referrer failed",n),i={source:null,medium:null,url:null}}let s={referrerSource:i.source,referrerMedium:i.medium,utmSource:i.utmSource,utmMedium:i.utmMedium,utmCampaign:i.utmCampaign,utmTerm:i.utmTerm,utmContent:i.utmContent},c;try{c=E(window.location.href),!c&&i.url&&(c=i.url)}catch(n){console.error("[Member Attribution] Getting final referrer failed",n),c=i.url}try{let n=new URL(window.location.href),a=n.searchParams;a.get("attribution_id")&&a.get("attribution_type")&&(t.push({time:u,id:a.get("attribution_id"),type:a.get("attribution_type"),...s,referrerUrl:c}),a.delete("attribution_id"),a.delete("attribution_type"),n.search="?"+a.toString(),window.history.replaceState({},"",`${n.pathname}${n.search}${n.hash}`))}catch(n){console.error("[Member Attribution] Parsing attribution from querystring failed",n)}let l=window.location.pathname;if(t.length===0||t[t.length-1].path!==l)t.push({path:l,time:u,...s,referrerUrl:c});else if(t.length>0){let n=t[t.length-1];n.time=u,Object.entries(s).forEach(([a,f])=>{f&&(n[a]=f)}),c&&(n.referrerUrl=c)}t.length>w&&(t=t.slice(-w)),e.setItem(p,JSON.stringify(t))}catch(e){console.error("[Member Attribution] Failed with error",e)}})();})();
@@ -15,7 +15,7 @@ const controller = {
15
15
  },
16
16
  query() {
17
17
  const options = {
18
- filter: 'type:post',
18
+ filter: 'type:post+status:[draft,published,scheduled,sent]',
19
19
  limit: '10000',
20
20
  order: 'updated_at DESC',
21
21
  columns: ['id', 'url', 'title', 'status', 'published_at', 'visibility']
@@ -34,7 +34,7 @@ const controller = {
34
34
  },
35
35
  query() {
36
36
  const options = {
37
- filter: 'type:page',
37
+ filter: 'type:page+status:[draft,published,scheduled]',
38
38
  limit: '10000',
39
39
  order: 'updated_at DESC',
40
40
  columns: ['id', 'url', 'title', 'status', 'published_at', 'visibility']
@@ -35,14 +35,20 @@ const controller = {
35
35
  docName: 'members',
36
36
  method: 'browse'
37
37
  },
38
+ options: [
39
+ 'date_from'
40
+ ],
38
41
  cache: statsService.cache,
39
- generateCacheKeyData() {
42
+ generateCacheKeyData(frame) {
40
43
  return {
41
- method: 'mrr'
44
+ method: 'mrr',
45
+ options: frame.options
42
46
  };
43
47
  },
44
- async query() {
45
- return await statsService.api.getMRRHistory();
48
+ async query(frame) {
49
+ return await statsService.api.getMRRHistory({
50
+ dateFrom: frame?.options?.date_from
51
+ });
46
52
  }
47
53
  },
48
54
  subscriptions: {
@@ -11,7 +11,7 @@ const {createNonTransactionalMigration, createTransactionalMigration} = require(
11
11
  *
12
12
  * @returns {Migration}
13
13
  */
14
- function createAddColumnMigration(table, column, columnDefinition) {
14
+ function createAddColumnMigration(table, column, columnDefinition, options = {}) {
15
15
  return createNonTransactionalMigration(
16
16
  // up
17
17
  commands.createColumnMigration({
@@ -20,7 +20,8 @@ function createAddColumnMigration(table, column, columnDefinition) {
20
20
  dbIsInCorrectState: hasColumn => hasColumn === true,
21
21
  operation: commands.addColumn,
22
22
  operationVerb: 'Adding',
23
- columnDefinition
23
+ columnDefinition,
24
+ options
24
25
  }),
25
26
  // down
26
27
  commands.createColumnMigration({
@@ -29,7 +30,8 @@ function createAddColumnMigration(table, column, columnDefinition) {
29
30
  dbIsInCorrectState: hasColumn => hasColumn === false,
30
31
  operation: commands.dropColumn,
31
32
  operationVerb: 'Removing',
32
- columnDefinition
33
+ columnDefinition,
34
+ options
33
35
  })
34
36
  );
35
37
  }
@@ -41,7 +43,7 @@ function createAddColumnMigration(table, column, columnDefinition) {
41
43
  *
42
44
  * @returns {Migration}
43
45
  */
44
- function createDropColumnMigration(table, column, columnDefinition) {
46
+ function createDropColumnMigration(table, column, columnDefinition, options = {}) {
45
47
  return createNonTransactionalMigration(
46
48
  // up
47
49
  commands.createColumnMigration({
@@ -49,7 +51,9 @@ function createDropColumnMigration(table, column, columnDefinition) {
49
51
  column,
50
52
  dbIsInCorrectState: hasColumn => hasColumn === false,
51
53
  operation: commands.dropColumn,
52
- operationVerb: 'Removing'
54
+ operationVerb: 'Removing',
55
+ columnDefinition,
56
+ options
53
57
  }),
54
58
  // down
55
59
  commands.createColumnMigration({
@@ -58,7 +62,8 @@ function createDropColumnMigration(table, column, columnDefinition) {
58
62
  dbIsInCorrectState: hasColumn => hasColumn === true,
59
63
  operation: commands.addColumn,
60
64
  operationVerb: 'Adding',
61
- columnDefinition
65
+ columnDefinition,
66
+ options
62
67
  })
63
68
  );
64
69
  }
@@ -0,0 +1,8 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('tokens', 'uuid', {
4
+ type: 'string',
5
+ maxlength: 36,
6
+ nullable: true,
7
+ unique: true
8
+ });
@@ -0,0 +1,19 @@
1
+ const logging = require('@tryghost/logging');
2
+ const crypto = require('crypto');
3
+
4
+ const {createTransactionalMigration} = require('../../utils');
5
+
6
+ module.exports = createTransactionalMigration(
7
+ async function up(knex) {
8
+ const tokensWithoutUUID = await knex.select('id').from('tokens').whereNull('uuid');
9
+
10
+ logging.info(`Adding uuid field value to ${tokensWithoutUUID.length} tokens.`);
11
+
12
+ // eslint-disable-next-line no-restricted-syntax
13
+ for (const token of tokensWithoutUUID) {
14
+ await knex('tokens').update('uuid', crypto.randomUUID()).where('id', token.id);
15
+ }
16
+ },
17
+ // down is a no-op
18
+ async function down() {}
19
+ );
@@ -0,0 +1,4 @@
1
+ const {createDropNullableMigration} = require('../../utils');
2
+
3
+ module.exports = createDropNullableMigration('tokens', 'uuid');
4
+
@@ -0,0 +1,24 @@
1
+ const {combineNonTransactionalMigrations, createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = combineNonTransactionalMigrations(
4
+ // members_created_events
5
+ createAddColumnMigration('members_created_events', 'utm_source', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
6
+ createAddColumnMigration('members_created_events', 'utm_medium', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
7
+ createAddColumnMigration('members_created_events', 'utm_campaign', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
8
+ createAddColumnMigration('members_created_events', 'utm_term', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
9
+ createAddColumnMigration('members_created_events', 'utm_content', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
10
+
11
+ // members_subscription_created_events
12
+ createAddColumnMigration('members_subscription_created_events', 'utm_source', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
13
+ createAddColumnMigration('members_subscription_created_events', 'utm_medium', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
14
+ createAddColumnMigration('members_subscription_created_events', 'utm_campaign', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
15
+ createAddColumnMigration('members_subscription_created_events', 'utm_term', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
16
+ createAddColumnMigration('members_subscription_created_events', 'utm_content', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
17
+
18
+ // donation_payment_events
19
+ createAddColumnMigration('donation_payment_events', 'utm_source', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
20
+ createAddColumnMigration('donation_payment_events', 'utm_medium', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
21
+ createAddColumnMigration('donation_payment_events', 'utm_campaign', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
22
+ createAddColumnMigration('donation_payment_events', 'utm_term', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'}),
23
+ createAddColumnMigration('donation_payment_events', 'utm_content', {type: 'string', maxlength: 191, nullable: true}, {algorithm: 'auto'})
24
+ );
@@ -96,8 +96,10 @@ function dropNullable(tableName, column, transaction = db.knex) {
96
96
  * @param {string} column
97
97
  * @param {import('knex').Knex.Transaction} [transaction]
98
98
  * @param {object} columnSpec
99
+ * @param {object} [options]
100
+ * @param {'inplace'|'copy'|'auto'} [options.algorithm] - MySQL only
99
101
  */
100
- async function addColumn(tableName, column, transaction = db.knex, columnSpec) {
102
+ async function addColumn(tableName, column, transaction = db.knex, columnSpec, options = {}) {
101
103
  const addColumnBuilder = transaction.schema.table(tableName, function (table) {
102
104
  addTableColumn(tableName, table, column, columnSpec);
103
105
  });
@@ -114,7 +116,12 @@ async function addColumn(tableName, column, transaction = db.knex, columnSpec) {
114
116
 
115
117
  if (DatabaseInfo.isMySQL(transaction)) {
116
118
  // Guard against an ending semicolon
117
- sql = sql.replace(/;\s*$/, '') + ', algorithm=copy';
119
+ sql = sql.replace(/;\s*$/, '');
120
+ if (options?.algorithm !== 'auto') {
121
+ // default to copy if not specified
122
+ const algorithm = options?.algorithm || 'copy';
123
+ sql += `, algorithm=${algorithm}`;
124
+ }
118
125
  }
119
126
 
120
127
  await transaction.raw(sql);
@@ -126,8 +133,10 @@ async function addColumn(tableName, column, transaction = db.knex, columnSpec) {
126
133
  * @param {string} column
127
134
  * @param {import('knex').Knex} [transaction]
128
135
  * @param {object} [columnSpec]
136
+ * @param {object} [options]
137
+ * @param {'inplace'|'copy'|'auto'} [options.algorithm] - MySQL only
129
138
  */
130
- async function dropColumn(tableName, column, transaction = db.knex, columnSpec = {}) {
139
+ async function dropColumn(tableName, column, transaction = db.knex, columnSpec = {}, options = {}) {
131
140
  if (Object.prototype.hasOwnProperty.call(columnSpec, 'references')) {
132
141
  const [toTable, toColumn] = columnSpec.references.split('.');
133
142
  await dropForeign({fromTable: tableName, fromColumn: column, toTable, toColumn, constraintName: columnSpec.constraintName, transaction});
@@ -149,7 +158,12 @@ async function dropColumn(tableName, column, transaction = db.knex, columnSpec =
149
158
 
150
159
  if (DatabaseInfo.isMySQL(transaction)) {
151
160
  // Guard against an ending semicolon
152
- sql = sql.replace(/;\s*$/, '') + ', algorithm=copy';
161
+ sql = sql.replace(/;\s*$/, '');
162
+ if (options?.algorithm !== 'auto') {
163
+ // default to copy if not specified
164
+ const algorithm = options?.algorithm || 'copy';
165
+ sql += `, algorithm=${algorithm}`;
166
+ }
153
167
  }
154
168
 
155
169
  await transaction.raw(sql);
@@ -561,7 +575,8 @@ function createColumnMigration(...migrations) {
561
575
  dbIsInCorrectState,
562
576
  operation,
563
577
  operationVerb,
564
- columnDefinition
578
+ columnDefinition,
579
+ options
565
580
  } = migration;
566
581
 
567
582
  const hasColumn = await conn.schema.hasColumn(table, column);
@@ -571,7 +586,7 @@ function createColumnMigration(...migrations) {
571
586
  logging.warn(`${operationVerb} ${table}.${column} column - skipping as table is correct`);
572
587
  } else {
573
588
  logging.info(`${operationVerb} ${table}.${column} column`);
574
- await operation(table, column, conn, columnDefinition);
589
+ await operation(table, column, conn, columnDefinition, options);
575
590
  }
576
591
  }
577
592
 
@@ -525,6 +525,7 @@ module.exports = {
525
525
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
526
526
  created_at: {type: 'dateTime', nullable: false},
527
527
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
528
+ // attribution values from ghost-history (member attribution tracking script)
528
529
  attribution_id: {type: 'string', maxlength: 24, nullable: true, index: true},
529
530
  attribution_type: {
530
531
  type: 'string', maxlength: 50, nullable: true, validations: {
@@ -532,9 +533,16 @@ module.exports = {
532
533
  }
533
534
  },
534
535
  attribution_url: {type: 'string', maxlength: 2000, nullable: true},
536
+ // referrer values from browser, processed by our referrerParser library
535
537
  referrer_source: {type: 'string', maxlength: 191, nullable: true},
536
538
  referrer_medium: {type: 'string', maxlength: 191, nullable: true},
537
539
  referrer_url: {type: 'string', maxlength: 2000, nullable: true},
540
+ // raw values from URL query parameters
541
+ utm_source: {type: 'string', maxlength: 191, nullable: true},
542
+ utm_medium: {type: 'string', maxlength: 191, nullable: true},
543
+ utm_campaign: {type: 'string', maxlength: 191, nullable: true},
544
+ utm_term: {type: 'string', maxlength: 191, nullable: true},
545
+ utm_content: {type: 'string', maxlength: 191, nullable: true},
538
546
  source: {
539
547
  type: 'string', maxlength: 50, nullable: false, validations: {
540
548
  isIn: [['member', 'import', 'system', 'api', 'admin']]
@@ -705,6 +713,7 @@ module.exports = {
705
713
  created_at: {type: 'dateTime', nullable: false},
706
714
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
707
715
  subscription_id: {type: 'string', maxlength: 24, nullable: false, references: 'members_stripe_customers_subscriptions.id', cascadeDelete: true},
716
+ // attribution values from ghost-history (member attribution tracking script)
708
717
  attribution_id: {type: 'string', maxlength: 24, nullable: true, index: true},
709
718
  attribution_type: {
710
719
  type: 'string', maxlength: 50, nullable: true, validations: {
@@ -712,9 +721,16 @@ module.exports = {
712
721
  }
713
722
  },
714
723
  attribution_url: {type: 'string', maxlength: 2000, nullable: true},
724
+ // referrer values from browser, processed by our referrerParser library
715
725
  referrer_source: {type: 'string', maxlength: 191, nullable: true},
716
726
  referrer_medium: {type: 'string', maxlength: 191, nullable: true},
717
727
  referrer_url: {type: 'string', maxlength: 2000, nullable: true},
728
+ // raw values from URL query parameters
729
+ utm_source: {type: 'string', maxlength: 191, nullable: true},
730
+ utm_medium: {type: 'string', maxlength: 191, nullable: true},
731
+ utm_campaign: {type: 'string', maxlength: 191, nullable: true},
732
+ utm_term: {type: 'string', maxlength: 191, nullable: true},
733
+ utm_content: {type: 'string', maxlength: 191, nullable: true},
718
734
  batch_id: {type: 'string', maxlength: 24, nullable: true}
719
735
  },
720
736
  offer_redemptions: {
@@ -746,6 +762,7 @@ module.exports = {
746
762
  member_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'members.id', setNullDelete: true},
747
763
  amount: {type: 'integer', nullable: false},
748
764
  currency: {type: 'string', maxlength: 50, nullable: false},
765
+ // attribution values from ghost-history (member attribution tracking script)
749
766
  attribution_id: {type: 'string', maxlength: 24, nullable: true},
750
767
  attribution_type: {
751
768
  type: 'string', maxlength: 50, nullable: true, validations: {
@@ -753,9 +770,16 @@ module.exports = {
753
770
  }
754
771
  },
755
772
  attribution_url: {type: 'string', maxlength: 2000, nullable: true},
773
+ // referrer values from browser, processed by our referrerParser library
756
774
  referrer_source: {type: 'string', maxlength: 191, nullable: true},
757
775
  referrer_medium: {type: 'string', maxlength: 191, nullable: true},
758
776
  referrer_url: {type: 'string', maxlength: 2000, nullable: true},
777
+ // raw values from URL query parameters
778
+ utm_source: {type: 'string', maxlength: 191, nullable: true},
779
+ utm_medium: {type: 'string', maxlength: 191, nullable: true},
780
+ utm_campaign: {type: 'string', maxlength: 191, nullable: true},
781
+ utm_term: {type: 'string', maxlength: 191, nullable: true},
782
+ utm_content: {type: 'string', maxlength: 191, nullable: true},
759
783
  created_at: {type: 'dateTime', nullable: false},
760
784
  donation_message: {type: 'string', maxlength: 255, nullable: true} // https://docs.stripe.com/payments/checkout/custom-fields
761
785
  },
@@ -896,6 +920,7 @@ module.exports = {
896
920
  tokens: {
897
921
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
898
922
  token: {type: 'string', maxlength: 32, nullable: false, index: true},
923
+ uuid: {type: 'string', maxlength: 36, nullable: false, unique: true, validations: {isUUID: true}},
899
924
  data: {type: 'string', maxlength: 2000, nullable: true},
900
925
  created_at: {type: 'dateTime', nullable: false},
901
926
  updated_at: {type: 'dateTime', nullable: true},
@@ -1,5 +1,3 @@
1
-
2
-
3
1
  SCHEMA >
4
2
  `site_uuid` LowCardinality(String),
5
3
  `timestamp` DateTime,
@@ -16,8 +14,13 @@ SCHEMA >
16
14
  `href` String,
17
15
  `device` String,
18
16
  `os` String,
19
- `browser` String
17
+ `browser` String,
18
+ `utm_source` String,
19
+ `utm_medium` String,
20
+ `utm_campaign` String,
21
+ `utm_term` String,
22
+ `utm_content` String
20
23
 
21
24
  ENGINE "MergeTree"
22
25
  ENGINE_PARTITION_KEY "toYYYYMM(timestamp)"
23
- ENGINE_SORTING_KEY "site_uuid, timestamp, session_id"
26
+ ENGINE_SORTING_KEY "site_uuid, timestamp, session_id"
@@ -4,20 +4,14 @@ NODE top_utm_campaigns
4
4
  SQL >
5
5
  %
6
6
  select
7
- case
8
- when length(source) % 6 = 0 then 'summer_sale_2024'
9
- when length(source) % 6 = 1 then 'newsletter_weekly'
10
- when length(source) % 6 = 2 then 'product_launch'
11
- when length(source) % 6 = 3 then 'holiday_promo'
12
- when length(source) % 6 = 4 then 'brand_awareness'
13
- when length(source) % 6 = 5 then 'retention_q4'
14
- end as utm_campaign,
7
+ utm_campaign,
15
8
  count() as visits
16
9
  from mv_session_data sd
17
10
  inner join filtered_sessions fs
18
11
  on fs.session_id = sd.session_id
19
12
  where
20
13
  site_uuid = {{ String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True) }}
14
+ and utm_campaign != ''
21
15
  {% if defined(date_from) and day_diff(date_from, date_to) == 0 %}
22
16
  and toDate(toTimezone(first_pageview, {{String(timezone, 'Etc/UTC', description="Site timezone", required=True)}})) = {{ Date(date_from) }}
23
17
  {% else %}
@@ -4,20 +4,14 @@ NODE top_utm_content
4
4
  SQL >
5
5
  %
6
6
  select
7
- case
8
- when length(source) % 6 = 0 then 'hero-banner'
9
- when length(source) % 6 = 1 then 'sidebar-cta'
10
- when length(source) % 6 = 2 then 'footer-link'
11
- when length(source) % 6 = 3 then 'button-primary'
12
- when length(source) % 6 = 4 then 'text-link'
13
- when length(source) % 6 = 5 then 'nav-menu'
14
- end as utm_content,
7
+ utm_content,
15
8
  count() as visits
16
9
  from mv_session_data sd
17
10
  inner join filtered_sessions fs
18
11
  on fs.session_id = sd.session_id
19
12
  where
20
13
  site_uuid = {{ String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True) }}
14
+ and utm_content != ''
21
15
  {% if defined(date_from) and day_diff(date_from, date_to) == 0 %}
22
16
  and toDate(toTimezone(first_pageview, {{String(timezone, 'Etc/UTC', description="Site timezone", required=True)}})) = {{ Date(date_from) }}
23
17
  {% else %}
@@ -4,20 +4,14 @@ NODE top_utm_mediums
4
4
  SQL >
5
5
  %
6
6
  select
7
- case
8
- when length(source) % 6 = 0 then 'email'
9
- when length(source) % 6 = 1 then 'social'
10
- when length(source) % 6 = 2 then 'cpc'
11
- when length(source) % 6 = 3 then 'organic'
12
- when length(source) % 6 = 4 then 'referral'
13
- when length(source) % 6 = 5 then 'display'
14
- end as utm_medium,
7
+ utm_medium,
15
8
  count() as visits
16
9
  from mv_session_data sd
17
10
  inner join filtered_sessions fs
18
11
  on fs.session_id = sd.session_id
19
12
  where
20
13
  site_uuid = {{ String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True) }}
14
+ and utm_medium != ''
21
15
  {% if defined(date_from) and day_diff(date_from, date_to) == 0 %}
22
16
  and toDate(toTimezone(first_pageview, {{String(timezone, 'Etc/UTC', description="Site timezone", required=True)}})) = {{ Date(date_from) }}
23
17
  {% else %}
@@ -4,20 +4,14 @@ NODE top_utm_sources
4
4
  SQL >
5
5
  %
6
6
  select
7
- case
8
- when length(source) % 6 = 0 then 'google'
9
- when length(source) % 6 = 1 then 'facebook'
10
- when length(source) % 6 = 2 then 'twitter'
11
- when length(source) % 6 = 3 then 'linkedin'
12
- when length(source) % 6 = 4 then 'newsletter'
13
- when length(source) % 6 = 5 then 'instagram'
14
- end as utm_source,
7
+ utm_source,
15
8
  count() as visits
16
9
  from mv_session_data sd
17
10
  inner join filtered_sessions fs
18
11
  on fs.session_id = sd.session_id
19
12
  where
20
13
  site_uuid = {{ String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True) }}
14
+ and utm_source != ''
21
15
  {% if defined(date_from) and day_diff(date_from, date_to) == 0 %}
22
16
  and toDate(toTimezone(first_pageview, {{String(timezone, 'Etc/UTC', description="Site timezone", required=True)}})) = {{ Date(date_from) }}
23
17
  {% else %}
@@ -4,20 +4,14 @@ NODE top_utm_terms
4
4
  SQL >
5
5
  %
6
6
  select
7
- case
8
- when length(source) % 6 = 0 then 'ghost cms'
9
- when length(source) % 6 = 1 then 'headless publishing'
10
- when length(source) % 6 = 2 then 'content management'
11
- when length(source) % 6 = 3 then 'newsletter platform'
12
- when length(source) % 6 = 4 then 'blog software'
13
- when length(source) % 6 = 5 then 'membership site'
14
- end as utm_term,
7
+ utm_term,
15
8
  count() as visits
16
9
  from mv_session_data sd
17
10
  inner join filtered_sessions fs
18
11
  on fs.session_id = sd.session_id
19
12
  where
20
13
  site_uuid = {{ String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True) }}
14
+ and utm_term != ''
21
15
  {% if defined(date_from) and day_diff(date_from, date_to) == 0 %}
22
16
  and toDate(toTimezone(first_pageview, {{String(timezone, 'Etc/UTC', description="Site timezone", required=True)}})) = {{ Date(date_from) }}
23
17
  {% else %}