ghost 5.129.1 → 5.130.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 (58) hide show
  1. package/components/tryghost-i18n-5.130.0.tgz +0 -0
  2. package/core/boot.js +6 -5
  3. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +2 -2
  4. package/core/built/admin/assets/admin-x-activitypub/{index-CWqPqbZ6.mjs → index-BhgdXgH_.mjs} +2 -2
  5. package/core/built/admin/assets/admin-x-activitypub/{index-t8sCkPyJ.mjs → index-rDFm98Ub.mjs} +15557 -15454
  6. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-FMecMk4J.mjs → CodeEditorView-bO8i1M7l.mjs} +2 -2
  7. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +3 -3
  8. package/core/built/admin/assets/admin-x-settings/{index-CqcRQSMi.mjs → index-BeD9DTp3.mjs} +512 -490
  9. package/core/built/admin/assets/admin-x-settings/index-DIak5kz8.mjs +30462 -0
  10. package/core/built/admin/assets/admin-x-settings/{modals-CfWblo-N.mjs → modals-DLPpqlUq.mjs} +1212 -1210
  11. package/core/built/admin/assets/{chunk.524.5330e1e5569947d7b7f2.js → chunk.524.2443bfd380e6da0cbabd.js} +7 -7
  12. package/core/built/admin/assets/{chunk.582.f6a6d1826e91aafd496b.js → chunk.582.434476dff5ddc79ed054.js} +9 -9
  13. package/core/built/admin/assets/{chunk.383.09219fde42568dd42ed5.js → chunk.728.214803966b81ffdb1acd.js} +6957 -6441
  14. package/core/built/admin/assets/ghost-3d0ad0c58f433d5735532bf25d4fd423.css +1 -0
  15. package/core/built/admin/assets/{ghost-1bce1a4ebfdfc6f6f333a827f40f69a6.js → ghost-5d9c65b5c4ef960a664cd664b2616dea.js} +121 -125
  16. package/core/built/admin/assets/ghost-dark-f19869a3fd0ef48c525149b9c87e4241.css +1 -0
  17. package/core/built/admin/assets/posts/posts.js +6740 -6712
  18. package/core/built/admin/assets/stats/stats.js +13289 -13235
  19. package/core/built/admin/index.html +5 -5
  20. package/core/frontend/helpers/ghost_head.js +3 -4
  21. package/core/frontend/helpers/match.js +3 -0
  22. package/core/frontend/meta/get-meta.js +1 -1
  23. package/core/frontend/meta/schema.js +45 -24
  24. package/core/frontend/services/proxy.js +6 -0
  25. package/core/server/api/endpoints/utils/serializers/input/settings.js +3 -1
  26. package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.js +3 -1
  27. package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.js +3 -1
  28. package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
  29. package/core/server/data/migrations/versions/5.130/2025-07-11-14-14-54-add-explore-settings.js +16 -0
  30. package/core/server/data/schema/default-settings/default-settings.json +18 -0
  31. package/core/server/data/tinybird/ARCHITECTURE.md +420 -0
  32. package/core/server/data/tinybird/README.md +84 -0
  33. package/core/server/data/tinybird/scripts/configure-ghost.sh +65 -0
  34. package/core/server/services/activitypub/ActivityPubService.js +24 -4
  35. package/core/server/services/activitypub/ActivityPubService.ts +27 -7
  36. package/core/server/services/activitypub/ActivityPubServiceWrapper.js +11 -5
  37. package/core/server/services/email-service/EmailRenderer.js +11 -0
  38. package/core/server/services/email-service/email-templates/partials/styles.hbs +12 -7
  39. package/core/server/services/explore-ping/ExplorePingService.js +44 -33
  40. package/core/server/services/members/members-api/repositories/MemberRepository.js +1 -4
  41. package/core/server/services/public-config/config.js +4 -0
  42. package/core/server/services/settings/settings-service.js +4 -0
  43. package/core/server/services/settings-helpers/SettingsHelpers.js +114 -7
  44. package/core/server/services/settings-helpers/index.js +2 -1
  45. package/core/server/services/stats/StatsService.js +1 -2
  46. package/core/server/services/themes/installer.js +17 -3
  47. package/core/shared/config/env/config.production.json +4 -0
  48. package/core/shared/labs.js +0 -1
  49. package/core/shared/settings-cache/CacheManager.js +2 -0
  50. package/package.json +17 -17
  51. package/tsconfig.tsbuildinfo +1 -1
  52. package/yarn.lock +293 -236
  53. package/components/tryghost-i18n-5.129.1.tgz +0 -0
  54. package/core/built/admin/assets/admin-x-settings/index-DjbkRFc2.mjs +0 -26802
  55. package/core/built/admin/assets/ghost-415f8e3c36dbe0e09f87608628da382d.css +0 -1
  56. package/core/built/admin/assets/ghost-dark-2043bca95512f1fa2ff0bea2f8a632b0.css +0 -1
  57. package/core/server/data/tinybird/readme.md +0 -40
  58. /package/core/built/admin/assets/{chunk.383.09219fde42568dd42ed5.js.LICENSE.txt → chunk.728.214803966b81ffdb1acd.js.LICENSE.txt} +0 -0
@@ -0,0 +1,420 @@
1
+ # Ghost Traffic Analytics Data Model Explainer
2
+
3
+ This document explains the comprehensive data architecture behind Ghost's traffic analytics features,
4
+ covering both the MySQL database schema and Tinybird event streams, their relationships, and how they work together
5
+ to provide real-time analytics.
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Overview](#overview)
10
+ 2. [MySQL Schema (Ghost Database)](#mysql-schema-ghost-database)
11
+ 3. [Tinybird Event Schema](#tinybird-event-schema)
12
+ 4. [Data Flow & Relationships](#data-flow--relationships)
13
+ 5. [API Endpoints](#api-endpoints)
14
+ 6. [Mock Data Considerations](#mock-data-considerations)
15
+
16
+ ## Overview
17
+ Ghost's traffic analytics system has two data sources: MySQL and Tinybird
18
+
19
+ # Ghost Analytics Architecture
20
+
21
+ ```mermaid
22
+ graph TD
23
+ A[Ghost Admin<br/>Frontend] --> B[Ghost<br/>Main Server]
24
+ A --> C[Tinybird<br/>Analytics DB]
25
+ B --> D[MySQL<br/>Database]
26
+ B --> C
27
+ ```
28
+
29
+ - **MySQL Database**: Stores content, members, newsletters, and member attribution events
30
+ - **Tinybird**: Real-time analytics database that processes page views and visitor sessions
31
+ - **Cross-system Integration**: UUID-based relationships between Ghost entities and Tinybird events
32
+
33
+ The system tracks:
34
+ - **Web Traffic**: Page views, sessions, referrers, device info (via Tinybird)
35
+ - **Member Growth**: Signups, conversions, attribution to content (via MySQL)
36
+ - **Newsletter Performance**: Sends, opens, clicks, revenue attribution (via MySQL + Tinybird)
37
+ - **Content Performance**: Views, member conversions, revenue impact (hybrid)
38
+
39
+ ## MySQL Schema (Ghost Database)
40
+
41
+ ### Core Content Tables
42
+
43
+ #### `posts`
44
+ Primary content table storing all posts and pages.
45
+
46
+ **Key fields for analytics:**
47
+ - `id` (string, 24 chars) - Primary key, used in attribution events
48
+ - `uuid` (string, 36 chars) - UUID for Tinybird correlation
49
+ - `title` - Content title
50
+ - `slug` - URL slug
51
+ - `type` - 'post' or 'page'
52
+ - `status` - 'published', 'draft', 'scheduled', 'sent'
53
+ - `published_at` - Publication timestamp
54
+ - `newsletter_id` - Associated newsletter (if sent via email)
55
+
56
+ #### `posts_meta`
57
+ Extended metadata for posts.
58
+
59
+ **Relevant fields:**
60
+ - `post_id` - Foreign key to posts.id
61
+ - `meta_title`, `meta_description` - SEO metadata
62
+ - `email_subject` - Newsletter subject line
63
+ - `email_only` - Whether post is email-exclusive
64
+
65
+ ### Member & Subscription Tables
66
+
67
+ #### `members`
68
+ Core member accounts.
69
+
70
+ **Key fields:**
71
+ - `id` (string, 24 chars) - Primary key
72
+ - `uuid` (string, 36 chars) - UUID for Tinybird correlation
73
+ - `email` - Member email address
74
+ - `status` - 'free', 'paid', 'comped'
75
+ - `created_at` - Signup timestamp
76
+
77
+ #### `newsletters`
78
+ Newsletters are series of emails. Sites can have multiple different newsletters, each with its own name & branding. Sites may have a "Daily newsletter" and a "Weekly roundup" newsletter, for example.
79
+
80
+ **Key fields:**
81
+ - `id` (string, 24 chars) - Primary key
82
+ - `name` - Newsletter name
83
+ - `status` - 'active', 'archived'
84
+
85
+ #### `products` (tiers)
86
+ Subscription tiers/products that members can sign up for with Stripe
87
+
88
+ **Key fields:**
89
+ - `id` (string, 24 chars) - Primary key
90
+ - `name` - Tier name
91
+ - `type` - 'free', 'paid'
92
+ - `monthly_price`, `yearly_price` - Pricing
93
+
94
+ ### Attribution & Event Tables
95
+
96
+ #### `members_created_events`
97
+ Tracks member signups and their attribution.
98
+
99
+ **Key fields:**
100
+ - `id` - Primary key
101
+ - `member_id` - Foreign key to members.id
102
+ - `created_at` - Signup timestamp
103
+ - `attribution_id` - ID of attributed resource (post.id, etc.)
104
+ - `attribution_type` - 'post', 'page', 'url', 'tag', 'author'
105
+ - `attribution_url` - Full URL that drove signup
106
+ - `referrer_source` - Domain that referred the member
107
+ - `referrer_medium` - Marketing medium
108
+ - `referrer_url` - Full referrer URL
109
+ - `source` - 'member', 'import', 'system', 'api', 'admin'
110
+
111
+ #### `members_subscription_created_events`
112
+ Tracks paid subscription starts and their attribution.
113
+
114
+ **Key fields:**
115
+ - `id` - Primary key
116
+ - `member_id` - Foreign key to members.id
117
+ - `subscription_id` - Associated subscription
118
+ - `created_at` - Conversion timestamp
119
+ - `attribution_id` - ID of attributed resource
120
+ - `attribution_type` - 'post', 'page', 'url', 'tag', 'author'
121
+ - `attribution_url` - URL that drove conversion
122
+ - `referrer_source` - Domain that drove conversion
123
+ - `referrer_medium` - Marketing medium
124
+ - `referrer_url` - Full referrer URL
125
+
126
+ #### `members_paid_subscription_events`
127
+ Tracks MRR changes from subscription events.
128
+
129
+ **Key fields:**
130
+ - `member_id` - Foreign key to members.id
131
+ - `subscription_id` - Associated subscription
132
+ - `mrr_delta` - Monthly recurring revenue change
133
+ - `created_at` - Event timestamp
134
+
135
+ ### Email & Newsletter Tables
136
+
137
+ #### `emails`
138
+ When a post is sent as an email, a row is added to this table
139
+
140
+ **Key fields:**
141
+ - `id` - Primary key
142
+ - `post_id` - Associated post (unique)
143
+ - `newsletter_id` - Associated newsletter
144
+ - `status` - 'pending', 'submitted', 'failed'
145
+ - `email_count` - Total recipients
146
+ - `delivered_count` - Successfully delivered
147
+ - `opened_count` - Total opens
148
+ - `failed_count` - Failed deliveries
149
+ - `submitted_at` - Send timestamp
150
+
151
+ #### `email_recipients`
152
+ A row for every member who received a particular email. Tracks which members received each email, and when it was delivered, opened, failed, etc.
153
+
154
+ **Key fields:**
155
+ - `email_id` - Foreign key to emails.id
156
+ - `member_id` - Foreign key to members.id
157
+ - `member_email` - Recipient email
158
+ - `delivered_at` - Delivery timestamp
159
+ - `opened_at` - Open timestamp
160
+ - `failed_at` - Failure timestamp
161
+
162
+ #### `redirects`
163
+ All links in an email are replaced with tracking links to Ghost so we can track clicks in emails
164
+
165
+ **Key fields:**
166
+ - `id` - Primary key
167
+ - `from` - Short redirect URL
168
+ - `to` - Destination URL
169
+ - `post_id` - Associated post (if applicable)
170
+
171
+ #### `members_click_events`
172
+ Email click tracking - each time a member clicks a link in an email
173
+
174
+ **Key fields:**
175
+ - `member_id` - Foreign key to members.id
176
+ - `redirect_id` - Foreign key to redirects.id
177
+ - `created_at` - Click timestamp
178
+
179
+ ### Newsletter Subscription Tables
180
+
181
+ #### `members_newsletters`
182
+ Many-to-many relationship for newsletter subscriptions. Sites can have multiple newsletters, members can be subscribed to 0 or more newsletters
183
+
184
+ **Key fields:**
185
+ - `member_id` - Foreign key to members.id
186
+ - `newsletter_id` - Foreign key to newsletters.id
187
+
188
+ #### `members_subscribe_events`
189
+ Newsletter subscription/unsubscription events. Members can choose to subscribe/unsubscribe to any of a site's newsletters at any time
190
+
191
+ **Key fields:**
192
+ - `member_id` - Foreign key to members.id
193
+ - `newsletter_id` - Foreign key to newsletters.id
194
+ - `subscribed` - true/false for subscribe/unsubscribe
195
+ - `created_at` - Event timestamp
196
+ - `source` - Event source
197
+
198
+ ## Tinybird Event Schema
199
+
200
+ ### Analytics Events Datasource
201
+
202
+ #### `analytics_events`
203
+
204
+ Raw page view events streamed from the frontend. You can find the schema in `datasources` folder.
205
+ Fields with specific data in the schema:
206
+
207
+ ```sql
208
+ `action` LowCardinality(String) -- Usually 'page_hit'
209
+ `site_uuid` LowCardinality(String) -- Extracted from payload
210
+ ```
211
+
212
+ **Payload Structure:**
213
+ ```json
214
+ {
215
+ "site_uuid": "string",
216
+ "member_uuid": "string|undefined", // member.uuid in MySQL
217
+ "member_status": "free|paid|comped|undefined", // member.status in MySQL
218
+ "post_uuid": "string|undefined", // post.uuid in MySQL
219
+ "post_type": "post|page|empty", //post.type in MySQL
220
+ "user-agent": "string",
221
+ "locale": "string",
222
+ "location": "string", // Country code
223
+ "referrer": "string", // used for member attribution
224
+ "pathname": "string",
225
+ "href": "string", // Full URL
226
+ }
227
+ ```
228
+
229
+ ### Materialized View: _mv_hits
230
+
231
+ #### `_mv_hits`
232
+
233
+ Schema for hits can be also found in `datasources` folder.
234
+ Explanation of some of the important fields.
235
+
236
+ ```sql
237
+ `location` String -- Country code
238
+ `source` String -- Referrer domain
239
+ `pathname` String -- URL path
240
+ `href` String -- Full URL
241
+ `device` String -- Device type
242
+ `os` String -- Operating system
243
+ `browser` String -- Browser name
244
+ ```
245
+
246
+ ### Tinybird Endpoints (Pipes)
247
+
248
+ Endpoints which can be found in `endpoints` folder.
249
+
250
+ - `api_active_visitors` - Real-time visitor counts
251
+ - `api_kpis` - Site-wide KPI metrics
252
+ - `api_post_visitor_counts` - Visitor counts by post UUID
253
+ - `api_top_browsers` - Top browsers by visits
254
+ - `api_top_devices` - Top devices by visits
255
+ - `api_top_locations` - Top countries by visits
256
+ - `api_top_os` - Top operating systems by visits
257
+ - `api_top_pages` - Top pages by visits
258
+ - `api_top_sources` - Top referrer sources by visits
259
+
260
+ ## Data Flow & Relationships
261
+
262
+ ### 1. Page View Tracking
263
+
264
+ ```
265
+ Frontend → Tinybird analytics_events → _mv_hits materialized view
266
+ ```
267
+
268
+ **Key Relationships:**
269
+ - `payload.site_uuid` identifies the Ghost site
270
+ - `payload.post_uuid` correlates to `posts.uuid` in MySQL
271
+ - `payload.member_uuid` correlates to `members.uuid` in MySQL
272
+
273
+ ### 2. Member Attribution Flow
274
+
275
+ ```
276
+ Member Signup → members_created_events (with attribution_*)
277
+ Member Conversion → members_subscription_created_events (with attribution_*)
278
+ ```
279
+
280
+ **Attribution Logic:**
281
+ - `attribution_id` contains `posts.id` when attributed to specific content
282
+ - `attribution_type` categorizes the attribution source
283
+ - `attribution_url` stores the full URL that drove the action
284
+ - `referrer_source` tracks the referring domain
285
+
286
+ ### 3. Newsletter Performance Flow
287
+
288
+ ```
289
+ Post Creation → Email Send (emails table) → Individual Recipients (email_recipients)
290
+ Click Tracking → redirects → members_click_events
291
+ ```
292
+
293
+ ### 4. Cross-System Data Correlation
294
+
295
+ **Post Performance Analysis:**
296
+ 1. Get page views from Tinybird using `posts.uuid`
297
+ 2. Get member attribution from MySQL using `posts.id`
298
+ 3. Get email performance from MySQL using `posts.id`
299
+ 4. Combine for comprehensive post analytics
300
+
301
+ **Member Journey Tracking:**
302
+ 1. Tinybird tracks anonymous page views
303
+ 2. MySQL tracks member signup with attribution
304
+ 3. MySQL tracks paid conversion with attribution
305
+
306
+ ## API Endpoints
307
+
308
+ All endpoints require authentication (`mw.authAdminApi`) and are gated behind `labs.isSet('trafficAnalytics')`.
309
+
310
+ ### Core Stats Endpoints
311
+
312
+ ```javascript
313
+ // Member growth
314
+ GET /stats/member_count
315
+ GET /stats/mrr
316
+ GET /stats/subscriptions
317
+
318
+ // Content performance
319
+ GET /stats/top-posts // Attribution-based rankings
320
+ GET /stats/top-posts-views // View-based rankings (Tinybird)
321
+ GET /stats/top-content // Combined content performance
322
+
323
+ // Post-specific analytics
324
+ GET /stats/posts/:id/stats // Individual post performance
325
+ GET /stats/posts/:id/growth // Member attribution for post
326
+ GET /stats/posts/:id/top-referrers // Referrer breakdown for post
327
+
328
+ // Newsletter analytics
329
+ GET /stats/newsletter-stats // Full newsletter performance
330
+ GET /stats/newsletter-basic-stats // Basic stats (faster)
331
+ GET /stats/newsletter-click-stats // Click-through data
332
+ GET /stats/subscriber-count // Subscriber growth
333
+
334
+ // Traffic sources
335
+ GET /stats/referrers // Historical referrer data
336
+ GET /stats/top-sources-growth // Source performance over time
337
+
338
+ // Batch endpoints
339
+ POST /stats/posts-visitor-counts // Bulk visitor counts (Tinybird)
340
+ POST /stats/posts-member-counts // Bulk member attribution (MySQL)
341
+ ```
342
+
343
+ ### Request/Response Patterns
344
+
345
+ Most endpoints support:
346
+ - `date_from` / `date_to` - Date range filtering
347
+ - `limit` - Result count limiting
348
+ - `order` - Sort field and direction
349
+ - `newsletter_id` - Newsletter-specific filtering
350
+ - `timezone`
351
+
352
+ Response format:
353
+ ```json
354
+ {
355
+ "data": [...],
356
+ "meta": {
357
+ "totals": {...}
358
+ }
359
+ }
360
+ ```
361
+
362
+ ## Mock Data Considerations
363
+
364
+ ### Data Volume & Relationships
365
+
366
+ For realistic testing, mock data should maintain proper ratios:
367
+
368
+ **Content:**
369
+ - ~100-500 posts across 6-month period
370
+ - ~10-20 pages (about, contact, etc.)
371
+ - 1-3 newsletters
372
+
373
+ **Members:**
374
+ - ~1000-5000 total members
375
+ - ~70% free, ~25% paid, ~5% comped
376
+ - ~50-100 new signups per month
377
+ - ~10-20% conversion rate from free to paid
378
+
379
+ **Page Views (Tinybird):**
380
+ - ~10-50 views per post (varies widely)
381
+ - ~100-500 daily total page views
382
+ - ~60% direct/type-in, ~25% search, ~10% social, ~5% other referrers
383
+
384
+ **Email Performance:**
385
+ - ~50-80% delivery rate
386
+ - ~20-40% open rate
387
+ - ~2-5% click rate
388
+ - Newsletter sends 1-4x per month
389
+
390
+ ### Key Constraints
391
+
392
+ **UUID Relationships:**
393
+ - `posts.uuid` must match Tinybird `post_uuid` values
394
+ - `members.uuid` must match Tinybird `member_uuid` values
395
+ - UUIDs should be valid v4 format
396
+
397
+ **Attribution Data:**
398
+ - `attribution_id` must reference valid `posts.id`
399
+ - `attribution_url` should be realistic Ghost URLs
400
+ - `referrer_source` should be realistic domains
401
+
402
+ **Temporal Consistency:**
403
+ - Member created events before subscription events
404
+ - Posts published before attribution events
405
+ - Email sends after post publication
406
+ - Page views distributed realistically over time
407
+
408
+ ### Performance Considerations
409
+
410
+ **Indexes:**
411
+ - Date-based queries need temporal indexing
412
+ - Attribution queries need member_id + attribution_id indexes
413
+ - Cross-table joins need proper foreign key indexes
414
+
415
+ **Query Patterns:**
416
+ - Most analytics queries filter by date ranges
417
+ - Post-specific queries join on post IDs/UUIDs
418
+ - Member attribution queries are complex with multiple CTEs
419
+
420
+ This data model enables comprehensive traffic analytics while maintaining performance through strategic use of both MySQL and Tinybird for their respective strengths.
@@ -0,0 +1,84 @@
1
+ ## Tinybird Analytics
2
+
3
+ This is the web analytics implementation using [Tinybird Forward](https://www.tinybird.co/docs/forward).
4
+
5
+ ### Requirements
6
+
7
+ In order to use Tinybird locally, make sure to install the Tinybird CLI.
8
+ **Only for the first time, run:** `yarn tb:install` from the root folder.
9
+
10
+ ### Using Tinybird locally
11
+
12
+ To run Tinybird locally, run `yarn tb` from root.
13
+
14
+ This script will pull and start the `tinybird-local` Docker container, then run `tb dev` to deploy the Tinybird project to the `tinybird-local` container on file changes. The `tb dev` command also launches a Tinybird shell environment where you can run other `tb` CLI commands against the local container. Read more about the [`tb dev` command here](https://www.tinybird.co/docs/forward/dev-reference/commands/tb-dev).
15
+
16
+ ### Connecting Tinybird to Ghost config file
17
+
18
+ In order to use Tinybird local service with Ghost, the config local needs to be updated with Tinybird details.
19
+ Config can contain information about remote and local environments.
20
+
21
+ Make sure to get the proper tokens, which you can obtain by running `tb token ls`, or if you are within Tinybird
22
+ local shell environment, you can just run `token ls`.
23
+
24
+ Make sure you use the proper tokens. If you need read, write permissions, use a token that allows both.
25
+
26
+ Simply disable local to use the cloud version by switching `local: false`.
27
+ This is switchable both via tracker script (ideally not used with the cloud data except with a unique id) and stats page.
28
+
29
+ You can enable or disable local in config script below. Update your `/ghost/core/config.local.json` or `/ghost/core/config.local.jsonc`
30
+ with the following information.
31
+
32
+ ### Config
33
+ Sample config:
34
+ ```json
35
+ {
36
+ "someOtherConfigurationForEmail": {
37
+ "transport": "SMTP",
38
+ "options": {
39
+ "port": 12345
40
+ }
41
+ },
42
+ "tinybird": {
43
+ "tracker": {
44
+ "endpoint": "https://e.ghost.org/tb/web_analytics",
45
+ "token": "xxxxx",
46
+ "datasource": "analytics_events",
47
+ "local": {
48
+ "enabled": true,
49
+ "token": "xxxxx",
50
+ "endpoint": "http://localhost:7181/v0/events",
51
+ "datasource": "analytics_events"
52
+ }
53
+ },
54
+ "stats": {
55
+ "endpoint": "https://api.tinybird.co",
56
+ "token": "xxxxx",
57
+ "local": {
58
+ "enabled": true,
59
+ "token": "xxxxx",
60
+ "endpoint": "http://localhost:7181",
61
+ "datasource": "analytics_events"
62
+ }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ### Testing
69
+
70
+ Tests are executed using `test run` when running `tb dev`.
71
+
72
+
73
+ ### Testing data
74
+
75
+ In fixtures folder, you can find local test data. When tinybird local is running, you can append data, or remove data
76
+ from fixture files here. As you modify the files, the datasources will be auto updated.
77
+
78
+ Keep in mind that as you update fixtures, it will rebuild data, but materialized views will have appended data. Old
79
+ data will not be cleared from them. One way to approach this to make sure data is consistent is to truncate all data
80
+ sources before adding test data to it.
81
+
82
+ ### Architecture
83
+
84
+ [See full documentation regarding analytics architecture in following document](ARCHITECTURE.md)
@@ -0,0 +1,65 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # This script is used to get important values from the Tinybird Local container, and setup Ghost's config
5
+ ## It is used in the e2e test CI workflow to configure Ghost to use the Tinybird local instance
6
+ ## It can also be used locally to configure Ghost to use a tinybird local instance
7
+
8
+ # Store tb info output as JSON to parse multiple values
9
+ ## This includes the workspace ID and a workspace token we can use to authenticate with the Tinybird API
10
+ TB_INFO=$(tb --output json info)
11
+
12
+ # Get the workspace ID from the JSON output
13
+ WORKSPACE_ID=$(echo "$TB_INFO" | jq -r '.local.workspace_id')
14
+
15
+ # Check if workspace ID is valid
16
+ if [ -z "$WORKSPACE_ID" ] || [ "$WORKSPACE_ID" = "null" ]; then
17
+ echo "Error: Failed to get workspace ID from Tinybird. Please ensure Tinybird is running and initialized." >&2
18
+ exit 1
19
+ fi
20
+
21
+ export TINYBIRD_WORKSPACE_ID="$WORKSPACE_ID"
22
+
23
+ WORKSPACE_TOKEN=$(echo "$TB_INFO" | jq -r '.local.token')
24
+
25
+ # Check if workspace token is valid
26
+ if [ -z "$WORKSPACE_TOKEN" ] || [ "$WORKSPACE_TOKEN" = "null" ]; then
27
+ echo "Error: Failed to get workspace token from Tinybird. Please ensure Tinybird is running and initialized." >&2
28
+ exit 1
29
+ fi
30
+
31
+ # Get the admin token from the Tinybird API
32
+ ## This is different from the workspace admin token
33
+ ADMIN_TOKEN=$(curl -s -H "Authorization: Bearer $WORKSPACE_TOKEN" http://localhost:7181/v0/tokens | jq -r '.tokens[] | select(.name == "admin token") | .token')
34
+
35
+ # Check if admin token is valid
36
+ if [ -z "$ADMIN_TOKEN" ] || [ "$ADMIN_TOKEN" = "null" ]; then
37
+ echo "Error: Failed to get admin token from Tinybird API. Please ensure Tinybird is properly configured." >&2
38
+ exit 1
39
+ fi
40
+
41
+ export TINYBIRD_ADMIN_TOKEN="$ADMIN_TOKEN"
42
+
43
+ # Get the tracker token from the Tinybird API
44
+ TRACKER_TOKEN=$(curl -s -H "Authorization: Bearer $WORKSPACE_TOKEN" http://localhost:7181/v0/tokens | jq -r '.tokens[] | select(.name == "tracker") | .token')
45
+
46
+ # Check if tracker token is valid
47
+ if [ -z "$TRACKER_TOKEN" ] || [ "$TRACKER_TOKEN" = "null" ]; then
48
+ echo "Error: Failed to get tracker token from Tinybird API. Please ensure Tinybird is properly configured." >&2
49
+ exit 1
50
+ fi
51
+
52
+ export TINYBIRD_TRACKER_TOKEN="$TRACKER_TOKEN"
53
+
54
+ # Export Ghost configuration as environment variables
55
+ export tinybird__adminToken="$TINYBIRD_ADMIN_TOKEN"
56
+ export tinybird__workspaceId="$TINYBIRD_WORKSPACE_ID"
57
+ export tinybird__stats__endpoint="http://localhost:7181"
58
+
59
+ # If running in GitHub Actions, also export to GITHUB_ENV
60
+ if [ -n "$GITHUB_ENV" ]; then
61
+ echo "tinybird__adminToken=$TINYBIRD_ADMIN_TOKEN" >> $GITHUB_ENV
62
+ echo "tinybird__workspaceId=$TINYBIRD_WORKSPACE_ID" >> $GITHUB_ENV
63
+ echo "tinybird__stats__endpoint=http://localhost:7181" >> $GITHUB_ENV
64
+ echo "TINYBIRD_TRACKER_TOKEN=$TINYBIRD_TRACKER_TOKEN" >> $GITHUB_ENV
65
+ fi
@@ -18,12 +18,32 @@ class ActivityPubService {
18
18
  this.identityTokenService = identityTokenService;
19
19
  }
20
20
  getExpectedWebhooks(secret) {
21
- return [{
21
+ return [
22
+ {
22
23
  event: 'post.published',
23
- target_url: new URL('.ghost/activitypub/webhooks/post/published', this.siteUrl),
24
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/published', this.siteUrl),
24
25
  api_version: 'v5.100.0',
25
26
  secret
26
- }];
27
+ },
28
+ {
29
+ event: 'post.deleted',
30
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/deleted', this.siteUrl),
31
+ api_version: 'v5.100.0',
32
+ secret
33
+ },
34
+ {
35
+ event: 'post.unpublished',
36
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/unpublished', this.siteUrl),
37
+ api_version: 'v5.100.0',
38
+ secret
39
+ },
40
+ {
41
+ event: 'post.published.edited',
42
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/updated', this.siteUrl),
43
+ api_version: 'v5.100.0',
44
+ secret
45
+ }
46
+ ];
27
47
  }
28
48
  async checkWebhookState(expectedWebhooks, integration) {
29
49
  this.logging.info(`Checking ActivityPub Webhook state`);
@@ -55,7 +75,7 @@ class ActivityPubService {
55
75
  .where('roles.name', 'Owner')
56
76
  .first();
57
77
  const token = await this.identityTokenService.getTokenForUser(ownerUser.email, 'Owner');
58
- const res = await (0, node_fetch_1.default)(new URL('.ghost/activitypub/site', this.siteUrl), {
78
+ const res = await (0, node_fetch_1.default)(new URL('.ghost/activitypub/v1/site', this.siteUrl), {
59
79
  headers: {
60
80
  Authorization: `Bearer ${token}`
61
81
  }
@@ -25,12 +25,32 @@ export class ActivityPubService {
25
25
  ) {}
26
26
 
27
27
  getExpectedWebhooks(secret: string): ExpectedWebhook[] {
28
- return [{
29
- event: 'post.published',
30
- target_url: new URL('.ghost/activitypub/webhooks/post/published', this.siteUrl),
31
- api_version: 'v5.100.0',
32
- secret
33
- }];
28
+ return [
29
+ {
30
+ event: 'post.published',
31
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/published', this.siteUrl),
32
+ api_version: 'v5.100.0',
33
+ secret
34
+ },
35
+ {
36
+ event: 'post.deleted',
37
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/deleted', this.siteUrl),
38
+ api_version: 'v5.100.0',
39
+ secret
40
+ },
41
+ {
42
+ event: 'post.unpublished',
43
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/unpublished', this.siteUrl),
44
+ api_version: 'v5.100.0',
45
+ secret
46
+ },
47
+ {
48
+ event: 'post.published.edited',
49
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/updated', this.siteUrl),
50
+ api_version: 'v5.100.0',
51
+ secret
52
+ }
53
+ ];
34
54
  }
35
55
 
36
56
  async checkWebhookState(expectedWebhooks: ExpectedWebhook[], integration: {id: string}) {
@@ -69,7 +89,7 @@ export class ActivityPubService {
69
89
  .first();
70
90
  const token = await this.identityTokenService.getTokenForUser(ownerUser.email, 'Owner');
71
91
 
72
- const res = await fetch(new URL('.ghost/activitypub/site', this.siteUrl), {
92
+ const res = await fetch(new URL('.ghost/activitypub/v1/site', this.siteUrl), {
73
93
  headers: {
74
94
  Authorization: `Bearer ${token}`
75
95
  }
@@ -32,16 +32,22 @@ module.exports = class ActivityPubServiceWrapper {
32
32
  IdentityTokenServiceWrapper.instance
33
33
  );
34
34
 
35
- if (settingsCache.get('social_web_enabled')) {
35
+ const initActivityPubService = async () => {
36
36
  await ActivityPubServiceWrapper.instance.initialiseWebhooks();
37
37
  ActivityPubServiceWrapper.initialised = true;
38
+ };
39
+
40
+ if (settingsCache.get('social_web_enabled')) {
41
+ await initActivityPubService();
38
42
  } else {
39
- events.on('settings.labs.edited', async () => {
43
+ const initActivityPubServiceLater = async () => {
40
44
  if (settingsCache.get('social_web_enabled') && !ActivityPubServiceWrapper.initialised) {
41
- await ActivityPubServiceWrapper.instance.initialiseWebhooks();
42
- ActivityPubServiceWrapper.initialised = true;
45
+ await initActivityPubService();
43
46
  }
44
- });
47
+ };
48
+
49
+ events.on('settings.labs.edited', initActivityPubServiceLater);
50
+ events.on('settings.social_web.edited', initActivityPubServiceLater);
45
51
  }
46
52
  }
47
53
  };