ghost 6.8.0 → 6.9.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 (68) hide show
  1. package/components/tryghost-i18n-6.9.0.tgz +0 -0
  2. package/core/built/admin/assets/activitypub/activitypub.js +2 -2
  3. package/core/built/admin/assets/activitypub/{index-BbINZU9U.mjs → index-B29oZuTp.mjs} +2 -2
  4. package/core/built/admin/assets/activitypub/{index-C3M839De.mjs → index-C19nEXqT.mjs} +7998 -7923
  5. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-Dh1am4P6.mjs → CodeEditorView-BNKxdfRt.mjs} +2 -2
  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-DqbTDHzA.mjs → index-B-_a183c.mjs} +2 -2
  8. package/core/built/admin/assets/admin-x-settings/{index-Hwd1HJ0-.mjs → index-CjRGpMVv.mjs} +2 -2
  9. package/core/built/admin/assets/admin-x-settings/{index-SxxA3jSX.mjs → index-Q0XmL0KU.mjs} +5 -5
  10. package/core/built/admin/assets/admin-x-settings/{modals-yhgQVHax.mjs → modals-omgXN6i-.mjs} +31 -27
  11. package/core/built/admin/assets/{chunk.524.0bee64e8bac52bb41823.js → chunk.524.774a2df444e2ffde4942.js} +7 -7
  12. package/core/built/admin/assets/{chunk.582.83c6478c40d90ef19d6b.js → chunk.582.ca4f05f3c39fda05b54c.js} +8 -8
  13. package/core/built/admin/assets/{ghost-7556359b8bd4ec08a6c23890b04bb56e.js → ghost-94d0fbb20e8e880fa9ba144cf26ab050.js} +27 -27
  14. package/core/built/admin/assets/ghost-dark-6c9cfa9c364e28c57e5983f68ec6f2fc.css +1 -0
  15. package/core/built/admin/assets/ghost-f724c1d53f5402f78a2d8cf8beb7c716.css +1 -0
  16. package/core/built/admin/assets/posts/posts.js +1 -1
  17. package/core/built/admin/assets/stats/stats.js +1 -1
  18. package/core/built/admin/index.html +4 -4
  19. package/core/frontend/public/robots.txt +1 -0
  20. package/core/frontend/web/middleware/index.js +0 -1
  21. package/core/frontend/web/routers/serve-favicon.js +56 -0
  22. package/core/frontend/web/site.js +2 -1
  23. package/core/server/data/tinybird/endpoints/api_kpis.pipe +0 -3
  24. package/core/server/data/tinybird/endpoints/api_top_locations.pipe +0 -3
  25. package/core/server/data/tinybird/endpoints/api_top_pages.pipe +0 -5
  26. package/core/server/data/tinybird/pipes/filtered_sessions.pipe +0 -3
  27. package/core/server/data/tinybird/scripts/analytics-generator.js +106 -2
  28. package/core/server/data/tinybird/tests/api_kpis.yaml +0 -36
  29. package/core/server/data/tinybird/tests/api_top_locations.yaml +2 -31
  30. package/core/server/data/tinybird/tests/api_top_pages.yaml +1 -30
  31. package/core/server/data/tinybird/tests/api_top_sources.yaml +2 -41
  32. package/core/server/data/tinybird/tests/api_top_utm_campaigns.yaml +3 -33
  33. package/core/server/data/tinybird/tests/api_top_utm_contents.yaml +3 -39
  34. package/core/server/data/tinybird/tests/api_top_utm_mediums.yaml +3 -34
  35. package/core/server/data/tinybird/tests/api_top_utm_sources.yaml +3 -43
  36. package/core/server/data/tinybird/tests/api_top_utm_terms.yaml +3 -36
  37. package/core/server/services/email-address/EmailAddressService.js +4 -1
  38. package/core/server/services/email-address/EmailAddressService.ts +5 -1
  39. package/core/server/services/email-analytics/EmailAnalyticsService.js +70 -16
  40. package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +6 -0
  41. package/core/server/services/email-analytics/jobs/update-member-email-analytics/index.js +4 -3
  42. package/core/server/services/email-analytics/lib/queries.js +84 -0
  43. package/core/server/services/email-service/EmailEventProcessor.js +113 -13
  44. package/core/server/services/email-service/EmailEventStorage.js +191 -26
  45. package/core/server/services/email-service/EmailRenderer.js +10 -3
  46. package/core/server/services/email-service/SendingService.js +1 -1
  47. package/core/server/services/lib/MailgunClient.js +2 -1
  48. package/core/server/services/member-welcome-emails/jobs/index.js +2 -2
  49. package/core/server/services/members/members-api/repositories/MemberRepository.js +2 -1
  50. package/core/server/services/tinybird/TinybirdService.js +0 -3
  51. package/core/server/web/admin/app.js +3 -1
  52. package/core/server/web/admin/controller.js +2 -1
  53. package/core/shared/config/defaults.json +2 -1
  54. package/core/shared/config/helpers.js +13 -0
  55. package/core/shared/url-utils.js +4 -2
  56. package/package.json +7 -7
  57. package/tsconfig.tsbuildinfo +1 -1
  58. package/yarn.lock +162 -109
  59. package/components/tryghost-i18n-6.8.0.tgz +0 -0
  60. package/core/built/admin/assets/ghost-ca67b9eb701b867ae2a2fdd76cebdc17.css +0 -1
  61. package/core/built/admin/assets/ghost-dark-a5c3c5101d50a0af1f7b828ee387846d.css +0 -1
  62. package/core/frontend/web/middleware/serve-favicon.js +0 -72
  63. package/core/server/data/tinybird/endpoints/api_top_browsers.pipe +0 -54
  64. package/core/server/data/tinybird/endpoints/api_top_devices.pipe +0 -53
  65. package/core/server/data/tinybird/endpoints/api_top_os.pipe +0 -53
  66. package/core/server/data/tinybird/tests/api_top_browsers.yaml +0 -98
  67. package/core/server/data/tinybird/tests/api_top_devices.yaml +0 -75
  68. package/core/server/data/tinybird/tests/api_top_os.yaml +0 -80
@@ -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.8%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%227f93a89e16%22%2C%22activitypubFilename%22%3A%22activitypub.js%22%2C%22activitypubHash%22%3A%2251c307dd9b%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%22feacf15ba2%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%22e0aadbd487%22%2C%22activitypubRemoteConfigUrl%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.9%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%22a4b3b38319%22%2C%22activitypubFilename%22%3A%22activitypub.js%22%2C%22activitypubHash%22%3A%2238e5073e2a%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%22460f25b871%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%22c820ebd57d%22%2C%22activitypubRemoteConfigUrl%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-ca67b9eb701b867ae2a2fdd76cebdc17.css" title="light">
31
+ <link integrity="" rel="stylesheet" href="assets/ghost-f724c1d53f5402f78a2d8cf8beb7c716.css" title="light">
32
32
 
33
33
 
34
34
  </head>
@@ -49,7 +49,7 @@
49
49
 
50
50
  <script src="assets/vendor-aed0068cf9b67d042dd23a6343545b7b.js"></script>
51
51
  <script src="assets/chunk.397.a720333cfffc99c47e71.js"></script>
52
- <script src="assets/chunk.524.0bee64e8bac52bb41823.js"></script>
53
- <script src="assets/ghost-7556359b8bd4ec08a6c23890b04bb56e.js"></script>
52
+ <script src="assets/chunk.524.774a2df444e2ffde4942.js"></script>
53
+ <script src="assets/ghost-94d0fbb20e8e880fa9ba144cf26ab050.js"></script>
54
54
  </body>
55
55
  </html>
@@ -5,3 +5,4 @@ Disallow: /email/
5
5
  Disallow: /members/api/comments/counts/
6
6
  Disallow: /r/
7
7
  Disallow: /webmentions/receive/
8
+ Disallow: /.ghost/analytics/api/
@@ -4,7 +4,6 @@ module.exports = {
4
4
  frontendCaching: require('./frontend-caching'),
5
5
  handleImageSizes: require('./handle-image-sizes'),
6
6
  redirectGhostToAdmin: require('./redirect-ghost-to-admin'),
7
- serveFavicon: require('./serve-favicon'),
8
7
  servePublicFile: require('./serve-public-file'),
9
8
  staticTheme: require('./static-theme')
10
9
  };
@@ -0,0 +1,56 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const config = require('../../../shared/config');
5
+ const {blogIcon} = require('../../../server/lib/image');
6
+ const urlUtils = require('../../../shared/url-utils');
7
+ const settingsCache = require('../../../shared/settings-cache');
8
+
9
+ const buildContentResponse = (ext, buf) => {
10
+ return {
11
+ headers: {
12
+ 'Content-Type': `image/${ext}`,
13
+ 'Content-Length': buf.length,
14
+ ETag: `"${crypto.createHash('md5').update(buf, 'utf8').digest('hex')}"`,
15
+ 'Cache-Control': `public, max-age=${config.get('caching:favicon:maxAge')}`
16
+ },
17
+ body: buf
18
+ };
19
+ };
20
+
21
+ // Handles requests to /favicon.ico, /favicon.png, /favicon.jpg, /favicon.jpeg
22
+ module.exports = function serveFavicon(siteApp) {
23
+ siteApp.get(/^\/favicon\.(ico|png|jpe?g)$/i, (req, res, next) => {
24
+ // CASE: favicon is default
25
+ // confusing: if you upload an icon, it's same logic as storing images
26
+ // we store as /content/images, because this is the url path images get requested via the browser
27
+ // we are using an express route to skip /content/images and the result is a image path
28
+ // based on config.getContentPath('images') + req.path
29
+ // in this case we don't use path rewrite, that's why we have to make it manually
30
+ const filePath = blogIcon.getIconPath();
31
+ const originalExtension = path.extname(filePath).toLowerCase();
32
+ const requestedExtension = path.extname(req.path).toLowerCase();
33
+
34
+ // CASE: custom favicon exists, load it from local file storage
35
+ if (settingsCache.get('icon')) {
36
+ // Always redirect to the icon path, which is never favicon.xxx
37
+ return res.redirect(302, blogIcon.getIconUrl());
38
+ }
39
+
40
+ // CASE: always redirect to .ico for default icon
41
+ if (originalExtension !== requestedExtension) {
42
+ return res.redirect(302, urlUtils.urlFor({relativeUrl: '/favicon.ico'}));
43
+ }
44
+
45
+ fs.readFile(filePath, (err, buf) => {
46
+ if (err) {
47
+ return next(err);
48
+ }
49
+
50
+ const content = buildContentResponse('x-icon', buf);
51
+
52
+ res.writeHead(200, content.headers);
53
+ res.end(content.body);
54
+ });
55
+ });
56
+ };
@@ -9,6 +9,7 @@ const config = require('../../shared/config');
9
9
  const storage = require('../../server/adapters/storage');
10
10
  const urlUtils = require('../../shared/url-utils');
11
11
  const sitemapHandler = require('../services/sitemap/handler');
12
+ const serveFavicon = require('./routers/serve-favicon');
12
13
  const themeEngine = require('../services/theme-engine');
13
14
  const themeMiddleware = themeEngine.middleware;
14
15
  const membersService = require('../../server/services/members');
@@ -62,7 +63,7 @@ module.exports = function setupSiteApp(routerConfig) {
62
63
  // Static content/assets
63
64
  // @TODO make sure all of these have a local 404 error handler
64
65
  // Favicon
65
- siteApp.use(mw.serveFavicon());
66
+ serveFavicon(siteApp);
66
67
 
67
68
  // Serve sitemap.xsl file
68
69
  siteApp.use(mw.servePublicFile('static', 'sitemap.xsl', 'text/xsl', config.get('caching:sitemapXSL:maxAge')));
@@ -140,9 +140,6 @@ SQL >
140
140
  {% if defined(date_to) %} and toDate(toTimezone(timestamp, {{String(timezone, 'Etc/UTC', description="Site timezone", required=True)}})) <= {{ Date(date_to) }} {% else %} and toDate(toTimezone(timestamp, {{String(timezone, 'Etc/UTC', description="Site timezone", required=True)}})) <= today() {% end %}
141
141
  {% end %}
142
142
  {% if defined(member_status) %} and member_status IN {{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }} {% end %}
143
- {% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %}
144
- {% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %}
145
- {% if defined(os) %} and os = {{ String(os, description="Operating system to filter on", required=False) }} {% end %}
146
143
  {% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %}
147
144
  {% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %}
148
145
  {% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %}
@@ -39,9 +39,6 @@ SQL >
39
39
  )
40
40
  )
41
41
  {% end %}
42
- {% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %}
43
- {% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %}
44
- {% if defined(os) %} and os = {{ String(os, description="Operating system to filter on", required=False) }} {% end %}
45
42
  {% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %}
46
43
  {% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %}
47
44
  {% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %}
@@ -40,11 +40,6 @@ SQL >
40
40
  )
41
41
  )
42
42
  {% end %}
43
- {% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %}
44
- {% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %}
45
- {% if defined(os) %} and os = {{ String(os, description="Operating system to filter on", required=False) }} {% end %}
46
- -- we do filtering on source in the filtered_sessions pipe
47
- # --{% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %}
48
43
  {% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %}
49
44
  {% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %}
50
45
  {% if defined(post_uuid) %} and post_uuid = {{ String(post_uuid, description="Post UUID to filter on", required=False) }} {% end %}
@@ -20,9 +20,6 @@ SQL >
20
20
  )
21
21
  )
22
22
  {% end %}
23
- {% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %}
24
- {% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %}
25
- {% if defined(os) %} and os = {{ String(os, description="Operating system to filter on", required=False) }} {% end %}
26
23
  {% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %}
27
24
  {% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %}
28
25
  {% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %}
@@ -111,6 +111,59 @@ class AnalyticsEventGenerator {
111
111
  this.locales = [
112
112
  'en-US', 'en-GB', 'es-ES', 'fr-FR', 'de-DE', 'it-IT', 'pt-BR', 'ja-JP', 'ko-KR', 'zh-CN'
113
113
  ];
114
+
115
+ // UTM parameters for realistic campaign tracking
116
+ this.utmSources = [
117
+ {value: 'google', weight: 25},
118
+ {value: 'facebook', weight: 15},
119
+ {value: 'twitter', weight: 12},
120
+ {value: 'linkedin', weight: 10},
121
+ {value: 'reddit', weight: 8},
122
+ {value: 'newsletter', weight: 15},
123
+ {value: 'email', weight: 10},
124
+ {value: 'bluesky', weight: 5}
125
+ ];
126
+
127
+ this.utmMediums = [
128
+ {value: 'cpc', weight: 30}, // Paid search
129
+ {value: 'social', weight: 25}, // Social media
130
+ {value: 'email', weight: 20}, // Email campaigns
131
+ {value: 'organic', weight: 15}, // Organic search
132
+ {value: 'referral', weight: 10} // Referral traffic
133
+ ];
134
+
135
+ this.utmCampaigns = [
136
+ {value: 'spring_sale_2024', weight: 15},
137
+ {value: 'product_launch', weight: 12},
138
+ {value: 'weekly_newsletter', weight: 20},
139
+ {value: 'summer_promotion', weight: 10},
140
+ {value: 'black_friday', weight: 8},
141
+ {value: 'holiday_special', weight: 8},
142
+ {value: 'retargeting', weight: 12},
143
+ {value: 'brand_awareness', weight: 10},
144
+ {value: 'lead_generation', weight: 5}
145
+ ];
146
+
147
+ this.utmContents = [
148
+ {value: 'header_cta', weight: 20},
149
+ {value: 'sidebar_banner', weight: 15},
150
+ {value: 'footer_link', weight: 10},
151
+ {value: 'inline_text', weight: 15},
152
+ {value: 'hero_button', weight: 20},
153
+ {value: 'popup_form', weight: 10},
154
+ {value: 'video_thumbnail', weight: 10}
155
+ ];
156
+
157
+ this.utmTerms = [
158
+ {value: 'ghost_cms', weight: 15},
159
+ {value: 'blogging_platform', weight: 12},
160
+ {value: 'content_management', weight: 10},
161
+ {value: 'publishing_software', weight: 8},
162
+ {value: 'newsletter_tool', weight: 10},
163
+ {value: 'membership_site', weight: 8},
164
+ {value: 'headless_cms', weight: 7},
165
+ {value: 'open_source_blog', weight: 5}
166
+ ];
114
167
 
115
168
  // Weighted distributions based on production data
116
169
  this.memberStatusWeights = [
@@ -497,6 +550,39 @@ class AnalyticsEventGenerator {
497
550
  randomChoice(array) {
498
551
  return array[Math.floor(Math.random() * array.length)];
499
552
  }
553
+
554
+ /**
555
+ * Generate UTM parameters for an event
556
+ * Returns null for ~50% of events (organic/direct traffic without UTM)
557
+ */
558
+ generateUtmParameters() {
559
+ // 50% of events have no UTM parameters (organic/direct traffic)
560
+ if (Math.random() < 0.5) {
561
+ return null;
562
+ }
563
+
564
+ const utmParams = {
565
+ utm_source: this.weightedChoice(this.utmSources),
566
+ utm_medium: this.weightedChoice(this.utmMediums)
567
+ };
568
+
569
+ // 80% of UTM events include a campaign
570
+ if (Math.random() < 0.8) {
571
+ utmParams.utm_campaign = this.weightedChoice(this.utmCampaigns);
572
+ }
573
+
574
+ // 40% of UTM events include content
575
+ if (Math.random() < 0.4) {
576
+ utmParams.utm_content = this.weightedChoice(this.utmContents);
577
+ }
578
+
579
+ // 30% of UTM events include term (mainly for paid search)
580
+ if (Math.random() < 0.3 && utmParams.utm_medium === 'cpc') {
581
+ utmParams.utm_term = this.weightedChoice(this.utmTerms);
582
+ }
583
+
584
+ return utmParams;
585
+ }
500
586
 
501
587
  /**
502
588
  * Format timestamp to match the schema format
@@ -533,7 +619,19 @@ class AnalyticsEventGenerator {
533
619
 
534
620
  // Generate referrerSource for meta field
535
621
  const referrerSource = this.referrerSourceMap[referrer] || referrer;
536
-
622
+
623
+ // Generate UTM parameters
624
+ const utmParams = this.generateUtmParameters();
625
+
626
+ // Build href with UTM parameters if present
627
+ let href = `${this.baseUrl}${content.pathname}`;
628
+ if (utmParams) {
629
+ const utmQueryString = Object.entries(utmParams)
630
+ .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
631
+ .join('&');
632
+ href = `${href}?${utmQueryString}`;
633
+ }
634
+
537
635
  const payload = {
538
636
  site_uuid: this.siteUuid,
539
637
  member_uuid: memberUuid,
@@ -545,11 +643,16 @@ class AnalyticsEventGenerator {
545
643
  location: this.weightedChoice(this.locationWeights),
546
644
  referrer: referrer,
547
645
  pathname: content.pathname,
548
- href: `${this.baseUrl}${content.pathname}`,
646
+ href: href,
549
647
  meta: {
550
648
  referrerSource: referrerSource
551
649
  }
552
650
  };
651
+
652
+ // Add UTM parameters to payload if present
653
+ if (utmParams) {
654
+ Object.assign(payload, utmParams);
655
+ }
553
656
 
554
657
  return {
555
658
  timestamp: this.formatTimestamp(timestamp),
@@ -591,6 +694,7 @@ class AnalyticsEventGenerator {
591
694
  console.log(`📈 Traffic pattern: Moderate growth over time with realistic seasonal patterns`);
592
695
  console.log(`⏰ Includes realistic daily/weekly patterns (weekdays > weekends, business hours > nights)`);
593
696
  console.log(`🔗 Using production-based referrer patterns (Google, Reddit, Bluesky, newsletters, etc.)`);
697
+ console.log(`🎯 ~50% of events include UTM tracking parameters (source, medium, campaign, content, term)`);
594
698
 
595
699
  for (let i = 0; i < numEvents; i++) {
596
700
  events.push(this.generateEvent(i, numEvents));
@@ -11,30 +11,6 @@
11
11
  {"date":"2100-01-06","visits":2,"pageviews":2,"bounce_rate":1,"avg_session_sec":0}
12
12
  {"date":"2100-01-07","visits":2,"pageviews":2,"bounce_rate":1,"avg_session_sec":0}
13
13
 
14
- - name: Filtered by browser - Chrome
15
- description: Filtered by browser - Chrome
16
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&browser=chrome
17
- expected_result: |
18
- {"date":"2100-01-01","visits":1,"pageviews":2,"bounce_rate":0,"avg_session_sec":1111}
19
- {"date":"2100-01-02","visits":0,"pageviews":0,"bounce_rate":0,"avg_session_sec":0}
20
- {"date":"2100-01-03","visits":1,"pageviews":3,"bounce_rate":0,"avg_session_sec":1115}
21
- {"date":"2100-01-04","visits":3,"pageviews":7,"bounce_rate":0.33,"avg_session_sec":572}
22
- {"date":"2100-01-05","visits":1,"pageviews":3,"bounce_rate":0,"avg_session_sec":493}
23
- {"date":"2100-01-06","visits":1,"pageviews":1,"bounce_rate":1,"avg_session_sec":0}
24
- {"date":"2100-01-07","visits":0,"pageviews":0,"bounce_rate":0,"avg_session_sec":0}
25
-
26
- - name: Filtered by device - desktop
27
- description: Filtered by device - desktop
28
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&device=desktop
29
- expected_result: |
30
- {"date":"2100-01-01","visits":2,"pageviews":4,"bounce_rate":0,"avg_session_sec":870.5}
31
- {"date":"2100-01-02","visits":1,"pageviews":3,"bounce_rate":0,"avg_session_sec":1027}
32
- {"date":"2100-01-03","visits":3,"pageviews":7,"bounce_rate":0,"avg_session_sec":3333}
33
- {"date":"2100-01-04","visits":3,"pageviews":7,"bounce_rate":0.33,"avg_session_sec":572}
34
- {"date":"2100-01-05","visits":2,"pageviews":5,"bounce_rate":0,"avg_session_sec":308}
35
- {"date":"2100-01-06","visits":2,"pageviews":2,"bounce_rate":1,"avg_session_sec":0}
36
- {"date":"2100-01-07","visits":2,"pageviews":2,"bounce_rate":1,"avg_session_sec":0}
37
-
38
14
  - name: Filtered by location - UK
39
15
  description: Filtered by location - UK
40
16
  parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&location=GB
@@ -47,18 +23,6 @@
47
23
  {"date":"2100-01-06","visits":1,"pageviews":1,"bounce_rate":1,"avg_session_sec":0}
48
24
  {"date":"2100-01-07","visits":0,"pageviews":0,"bounce_rate":0,"avg_session_sec":0}
49
25
 
50
- - name: Filtered by OS - Windows
51
- description: Filtered by OS - Windows
52
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&os=windows
53
- expected_result: |
54
- {"date":"2100-01-01","visits":2,"pageviews":4,"bounce_rate":0,"avg_session_sec":870.5}
55
- {"date":"2100-01-02","visits":1,"pageviews":3,"bounce_rate":0,"avg_session_sec":1027}
56
- {"date":"2100-01-03","visits":3,"pageviews":7,"bounce_rate":0,"avg_session_sec":3333}
57
- {"date":"2100-01-04","visits":3,"pageviews":7,"bounce_rate":0.33,"avg_session_sec":572}
58
- {"date":"2100-01-05","visits":2,"pageviews":5,"bounce_rate":0,"avg_session_sec":308}
59
- {"date":"2100-01-06","visits":1,"pageviews":1,"bounce_rate":1,"avg_session_sec":0}
60
- {"date":"2100-01-07","visits":2,"pageviews":2,"bounce_rate":1,"avg_session_sec":0}
61
-
62
26
  - name: Filtered by pathname - /about/
63
27
  description: Filtered by pathname - /about/
64
28
  parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&pathname=%2Fabout%2F
@@ -9,40 +9,12 @@
9
9
  {"location":"ES","visits":2}
10
10
  {"location":"DE","visits":1}
11
11
 
12
- - name: Filtered by browser - Chrome
13
- description: Filtered by browser - Chrome
14
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&browser=chrome
15
- expected_result: |
16
- {"location":"GB","visits":5}
17
- {"location":"DE","visits":1}
18
- {"location":"ES","visits":1}
19
-
20
- - name: Filtered by device - desktop
21
- description: Filtered by device - desktop
22
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop
23
- expected_result: |
24
- {"location":"GB","visits":7}
25
- {"location":"US","visits":3}
26
- {"location":"FR","visits":2}
27
- {"location":"ES","visits":2}
28
- {"location":"DE","visits":1}
29
-
30
12
  - name: Filtered by location - UK
31
13
  description: Filtered by location - UK
32
14
  parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&location=GB
33
15
  expected_result: |
34
16
  {"location":"GB","visits":8}
35
17
 
36
- - name: Filtered by OS - Windows
37
- description: Filtered by OS - Windows
38
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&os=windows
39
- expected_result: |
40
- {"location":"GB","visits":7}
41
- {"location":"US","visits":3}
42
- {"location":"ES","visits":2}
43
- {"location":"FR","visits":1}
44
- {"location":"DE","visits":1}
45
-
46
18
  - name: Filtered by pathname - /about/
47
19
  description: Filtered by pathname - /about/
48
20
  parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&pathname=%2Fabout%2F
@@ -98,8 +70,7 @@
98
70
 
99
71
  - name: Test with multiple filters combined
100
72
  description: Test with multiple filters combined
101
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop&browser=firefox
73
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&source=bing.com&pathname=%2Fabout%2F
102
74
  expected_result: |
103
- {"location":"US","visits":3}
104
- {"location":"FR","visits":1}
75
+ {"location":"DE","visits":1}
105
76
  {"location":"GB","visits":1}
@@ -8,23 +8,6 @@
8
8
  {"post_uuid":"","pathname":"\/","visits":7}
9
9
  {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
10
10
 
11
- - name: Filtered by browser - Chrome
12
- description: Filtered by browser - Chrome
13
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&browser=chrome
14
- expected_result: |
15
- {"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":6}
16
- {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":3}
17
- {"post_uuid":"","pathname":"\/","visits":3}
18
-
19
- - name: Filtered by device - desktop
20
- description: Filtered by device - desktop
21
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop
22
- expected_result: |
23
- {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
24
- {"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":8}
25
- {"post_uuid":"","pathname":"\/","visits":7}
26
- {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
27
-
28
11
  - name: Filtered by location - UK
29
12
  description: Filtered by location - UK
30
13
  parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&location=GB
@@ -33,15 +16,6 @@
33
16
  {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":4}
34
17
  {"post_uuid":"","pathname":"\/","visits":4}
35
18
 
36
- - name: Filtered by OS - Windows
37
- description: Filtered by OS - Windows
38
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&os=windows
39
- expected_result: |
40
- {"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":8}
41
- {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":7}
42
- {"post_uuid":"","pathname":"\/","visits":7}
43
- {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
44
-
45
19
  - name: Filtered by pathname - /about/
46
20
  description: Filtered by pathname - /about/
47
21
  parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&pathname=%2Fabout%2F
@@ -90,12 +64,9 @@
90
64
 
91
65
  - name: Test with multiple filters combined
92
66
  description: Test with multiple filters combined
93
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop&browser=firefox
67
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&source=bing.com&pathname=%2Fabout%2F
94
68
  expected_result: |
95
- {"post_uuid":"","pathname":"\/","visits":3}
96
69
  {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":2}
97
- {"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":1}
98
- {"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
99
70
 
100
71
  - name: Test with post_type - post
101
72
  description: Test with post_type - post
@@ -13,28 +13,6 @@
13
13
  {"source":"petty-queen.com","visits":1}
14
14
  {"source":"my-ghost-site.com","visits":1}
15
15
 
16
- - name: Filtered by browser - Chrome
17
- description: Filtered by browser - Chrome
18
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&browser=chrome
19
- expected_result: |
20
- {"source":"","visits":4}
21
- {"source":"bing.com","visits":1}
22
- {"source":"search.yahoo.com","visits":1}
23
- {"source":"baidu.com","visits":1}
24
-
25
- - name: Filtered by device - desktop
26
- description: Filtered by device - desktop
27
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop
28
- expected_result: |
29
- {"source":"","visits":6}
30
- {"source":"bing.com","visits":2}
31
- {"source":"search.yahoo.com","visits":2}
32
- {"source":"google.com","visits":1}
33
- {"source":"baidu.com","visits":1}
34
- {"source":"wilted-tick.com","visits":1}
35
- {"source":"duckduckgo.com","visits":1}
36
- {"source":"my-ghost-site.com","visits":1}
37
-
38
16
  - name: Filtered by location - UK
39
17
  description: Filtered by location - UK
40
18
  parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&location=GB
@@ -46,19 +24,6 @@
46
24
  {"source":"baidu.com","visits":1}
47
25
  {"source":"petty-queen.com","visits":1}
48
26
 
49
- - name: Filtered by OS - Windows
50
- description: Filtered by OS - Windows
51
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&os=windows
52
- expected_result: |
53
- {"source":"","visits":5}
54
- {"source":"bing.com","visits":2}
55
- {"source":"search.yahoo.com","visits":2}
56
- {"source":"google.com","visits":1}
57
- {"source":"baidu.com","visits":1}
58
- {"source":"wilted-tick.com","visits":1}
59
- {"source":"duckduckgo.com","visits":1}
60
- {"source":"my-ghost-site.com","visits":1}
61
-
62
27
  - name: Filtered by pathname - /about/
63
28
  description: Filtered by pathname - /about/
64
29
  parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&pathname=%2Fabout%2F
@@ -118,13 +83,9 @@
118
83
 
119
84
  - name: Test with multiple filters combined
120
85
  description: Test with multiple filters combined
121
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop&browser=firefox
86
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&source=bing.com&pathname=%2Fabout%2F
122
87
  expected_result: |
123
- {"source":"google.com","visits":1}
124
- {"source":"search.yahoo.com","visits":1}
125
- {"source":"wilted-tick.com","visits":1}
126
- {"source":"duckduckgo.com","visits":1}
127
- {"source":"my-ghost-site.com","visits":1}
88
+ {"source":"bing.com","visits":2}
128
89
 
129
90
  - name: Filtered by utm_source - google
130
91
  description: Filtered by utm_source - google
@@ -10,25 +10,6 @@
10
10
  {"utm_campaign":"retention_q4","visits":1}
11
11
  {"utm_campaign":"newsletter_weekly","visits":1}
12
12
 
13
- - name: Filtered by browser - Chrome
14
- description: Filtered by browser - Chrome
15
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&browser=chrome
16
- expected_result: |
17
- {"utm_campaign":"brand_awareness","visits":2}
18
- {"utm_campaign":"summer_sale_2024","visits":1}
19
- {"utm_campaign":"retention_q4","visits":1}
20
-
21
- - name: Filtered by device - desktop
22
- description: Filtered by device - desktop
23
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop
24
- expected_result: |
25
- {"utm_campaign":"brand_awareness","visits":2}
26
- {"utm_campaign":"holiday_promo","visits":2}
27
- {"utm_campaign":"product_launch","visits":2}
28
- {"utm_campaign":"summer_sale_2024","visits":1}
29
- {"utm_campaign":"retention_q4","visits":1}
30
- {"utm_campaign":"newsletter_weekly","visits":1}
31
-
32
13
  - name: Filtered by location - UK
33
14
  description: Filtered by location - UK
34
15
  parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&location=GB
@@ -38,17 +19,6 @@
38
19
  {"utm_campaign":"product_launch","visits":1}
39
20
  {"utm_campaign":"newsletter_weekly","visits":1}
40
21
 
41
- - name: Filtered by OS - Windows
42
- description: Filtered by OS - Windows
43
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&os=windows
44
- expected_result: |
45
- {"utm_campaign":"brand_awareness","visits":2}
46
- {"utm_campaign":"holiday_promo","visits":2}
47
- {"utm_campaign":"product_launch","visits":2}
48
- {"utm_campaign":"summer_sale_2024","visits":1}
49
- {"utm_campaign":"retention_q4","visits":1}
50
- {"utm_campaign":"newsletter_weekly","visits":1}
51
-
52
22
  - name: Filtered by pathname - /about/
53
23
  description: Filtered by pathname - /about/
54
24
  parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&pathname=%2Fabout%2F
@@ -103,7 +73,7 @@
103
73
 
104
74
  - name: Test with multiple filters combined
105
75
  description: Test with multiple filters combined
106
- parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop&browser=firefox
76
+ parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&source=bing.com&pathname=%2Fabout%2F
107
77
  expected_result: |
108
- {"utm_campaign":"holiday_promo","visits":2}
109
- {"utm_campaign":"product_launch","visits":2}
78
+ {"utm_campaign":"retention_q4","visits":1}
79
+ {"utm_campaign":"newsletter_weekly","visits":1}