ghost 5.129.1 → 5.129.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tryghost-i18n-5.129.2.tgz +0 -0
- package/core/boot.js +6 -5
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +2 -2
- package/core/built/admin/assets/admin-x-activitypub/{index-CWqPqbZ6.mjs → index-B12913rO.mjs} +2 -2
- package/core/built/admin/assets/admin-x-activitypub/{index-t8sCkPyJ.mjs → index-B7EmcyVj.mjs} +1809 -1786
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-FMecMk4J.mjs → CodeEditorView-l2Ex2555.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-CqcRQSMi.mjs → index-C6P_16OJ.mjs} +512 -490
- package/core/built/admin/assets/admin-x-settings/{index-DjbkRFc2.mjs → index-DoLRADbr.mjs} +14974 -11468
- package/core/built/admin/assets/admin-x-settings/{modals-CfWblo-N.mjs → modals-CY1xx4Em.mjs} +7580 -7572
- package/core/built/admin/assets/{chunk.524.5330e1e5569947d7b7f2.js → chunk.524.c8313bccd308920abf9c.js} +7 -7
- package/core/built/admin/assets/{chunk.582.f6a6d1826e91aafd496b.js → chunk.582.e4feab981886cfc91835.js} +10 -10
- package/core/built/admin/assets/{chunk.383.09219fde42568dd42ed5.js → chunk.728.214803966b81ffdb1acd.js} +6957 -6441
- package/core/built/admin/assets/ghost-3d0ad0c58f433d5735532bf25d4fd423.css +1 -0
- package/core/built/admin/assets/ghost-dark-f19869a3fd0ef48c525149b9c87e4241.css +1 -0
- package/core/built/admin/assets/{ghost-1bce1a4ebfdfc6f6f333a827f40f69a6.js → ghost-db9fcb8c1f65776f3ee11c39f19a660b.js} +118 -122
- package/core/built/admin/assets/posts/posts.js +6655 -6647
- package/core/built/admin/assets/stats/stats.js +9454 -9446
- package/core/built/admin/index.html +5 -5
- package/core/frontend/helpers/ghost_head.js +3 -4
- package/core/frontend/meta/get-meta.js +1 -1
- package/core/frontend/meta/schema.js +26 -24
- package/core/frontend/services/proxy.js +6 -0
- package/core/server/data/tinybird/ARCHITECTURE.md +420 -0
- package/core/server/data/tinybird/README.md +84 -0
- package/core/server/data/tinybird/scripts/configure-ghost.sh +65 -0
- package/core/server/services/activitypub/ActivityPubService.js +2 -2
- package/core/server/services/activitypub/ActivityPubService.ts +2 -2
- package/core/server/services/activitypub/ActivityPubServiceWrapper.js +11 -5
- package/core/server/services/email-service/EmailRenderer.js +11 -0
- package/core/server/services/email-service/email-templates/partials/styles.hbs +19 -0
- package/core/server/services/members/members-api/repositories/MemberRepository.js +1 -4
- package/core/server/services/settings/settings-service.js +4 -0
- package/core/server/services/settings-helpers/SettingsHelpers.js +114 -7
- package/core/server/services/settings-helpers/index.js +2 -1
- package/core/server/services/stats/StatsService.js +1 -2
- package/core/shared/labs.js +0 -1
- package/core/shared/settings-cache/CacheManager.js +2 -0
- package/package.json +15 -15
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +175 -149
- package/components/tryghost-i18n-5.129.1.tgz +0 -0
- package/core/built/admin/assets/ghost-415f8e3c36dbe0e09f87608628da382d.css +0 -1
- package/core/built/admin/assets/ghost-dark-2043bca95512f1fa2ff0bea2f8a632b0.css +0 -1
- package/core/server/data/tinybird/readme.md +0 -40
- /package/core/built/admin/assets/{chunk.383.09219fde42568dd42ed5.js.LICENSE.txt → chunk.728.214803966b81ffdb1acd.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%225.129%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%
|
|
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%225.129%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%22657b852ebf%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%2240f7252084%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%22be74778cd8%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%222dcb5663ba%22%2C%22adminXActivitypubCustomUrl%22%3A%22https%3A%2F%2Fcdn.jsdelivr.net%2Fghost%2Fadmin-x-activitypub%400%2Fdist%2Fadmin-x-activitypub.js%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-
|
|
31
|
+
<link integrity="" rel="stylesheet" href="assets/ghost-3d0ad0c58f433d5735532bf25d4fd423.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-c89102f24c3d9502e9db741509767580.js"></script>
|
|
51
|
-
<script src="assets/chunk.
|
|
52
|
-
<script src="assets/chunk.524.
|
|
53
|
-
<script src="assets/ghost-
|
|
51
|
+
<script src="assets/chunk.728.214803966b81ffdb1acd.js"></script>
|
|
52
|
+
<script src="assets/chunk.524.c8313bccd308920abf9c.js"></script>
|
|
53
|
+
<script src="assets/ghost-db9fcb8c1f65776f3ee11c39f19a660b.js"></script>
|
|
54
54
|
</body>
|
|
55
55
|
</html>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Usage: `{{ghost_head}}`
|
|
3
3
|
//
|
|
4
4
|
// Outputs scripts and other assets at the top of a Ghost theme
|
|
5
|
-
const {labs, metaData, settingsCache, config, blogIcon, urlUtils, getFrontendKey} = require('../services/proxy');
|
|
5
|
+
const {labs, metaData, settingsCache, config, blogIcon, urlUtils, getFrontendKey, settingsHelpers} = require('../services/proxy');
|
|
6
6
|
const {escapeExpression, SafeString} = require('../services/handlebars');
|
|
7
7
|
const {generateCustomFontCss, isValidCustomFont, isValidCustomHeadingFont} = require('@tryghost/custom-fonts');
|
|
8
8
|
// BAD REQUIRE
|
|
@@ -362,9 +362,8 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam
|
|
|
362
362
|
if (!_.isEmpty(tagCodeInjection)) {
|
|
363
363
|
head.push(tagCodeInjection);
|
|
364
364
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if (isTbTrackingEnabled && hasTbConfig) {
|
|
365
|
+
// Use settingsHelpers to check if web analytics is enabled (includes all necessary checks)
|
|
366
|
+
if (settingsHelpers.isWebAnalyticsEnabled()) {
|
|
368
367
|
head.push(getTinybirdTrackerScript(dataRoot));
|
|
369
368
|
// Set a flag in response locals to indicate tracking script is being served
|
|
370
369
|
if (dataRoot._locals) {
|
|
@@ -21,7 +21,7 @@ const getOgImage = require('./og-image');
|
|
|
21
21
|
const getPaginatedUrl = require('./paginated-url');
|
|
22
22
|
const getPublishedDate = require('./published-date');
|
|
23
23
|
const getRssUrl = require('./rss-url');
|
|
24
|
-
const getSchema = require('./schema');
|
|
24
|
+
const {getSchema} = require('./schema');
|
|
25
25
|
const getStructuredData = require('./structured-data');
|
|
26
26
|
const getTitle = require('./title');
|
|
27
27
|
const getTwitterImage = require('./twitter-image');
|
|
@@ -48,31 +48,33 @@ function trimSchema(schema) {
|
|
|
48
48
|
return schemaObject;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// note that website isn't included here
|
|
52
|
+
const SOCIAL_PLATFORMS = ['facebook', 'twitter', 'threads', 'bluesky', 'mastodon', 'tiktok', 'youtube', 'instagram', 'linkedin'];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build the `sameAs` array for schema.org Person objects.
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} author either `data.author` or `data.<post|page>.primary_author`.
|
|
58
|
+
* Expected to contain `website` plus any supported social usernames.
|
|
59
|
+
* @returns {string[]} URLs for the author website and each populated social profile.
|
|
60
|
+
*/
|
|
61
|
+
function trimSameAs(author) {
|
|
62
|
+
if (!author || Object.keys(author).length === 0) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
const sameAs = [];
|
|
53
67
|
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
sameAs.push(escapeExpression(data[context].primary_author.website));
|
|
57
|
-
}
|
|
58
|
-
if (data[context].primary_author.facebook) {
|
|
59
|
-
sameAs.push(socialUrls.facebook(data[context].primary_author.facebook));
|
|
60
|
-
}
|
|
61
|
-
if (data[context].primary_author.twitter) {
|
|
62
|
-
sameAs.push(socialUrls.twitter(data[context].primary_author.twitter));
|
|
63
|
-
}
|
|
64
|
-
} else if (context === 'author') {
|
|
65
|
-
if (data.author.website) {
|
|
66
|
-
sameAs.push(escapeExpression(data.author.website));
|
|
67
|
-
}
|
|
68
|
-
if (data.author.facebook) {
|
|
69
|
-
sameAs.push(socialUrls.facebook(data.author.facebook));
|
|
70
|
-
}
|
|
71
|
-
if (data.author.twitter) {
|
|
72
|
-
sameAs.push(socialUrls.twitter(data.author.twitter));
|
|
73
|
-
}
|
|
68
|
+
if (author.website) {
|
|
69
|
+
sameAs.push(escapeExpression(author.website));
|
|
74
70
|
}
|
|
75
71
|
|
|
72
|
+
SOCIAL_PLATFORMS.forEach((platform) => {
|
|
73
|
+
if (author[platform] && typeof socialUrls[platform] === 'function') {
|
|
74
|
+
sameAs.push(escapeExpression(socialUrls[platform](author[platform])));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
76
78
|
return sameAs;
|
|
77
79
|
}
|
|
78
80
|
|
|
@@ -94,7 +96,7 @@ function getPostSchema(metaData, data) {
|
|
|
94
96
|
name: escapeExpression(data[context].primary_author.name),
|
|
95
97
|
image: schemaImageObject(metaData.authorImage),
|
|
96
98
|
url: metaData.authorUrl,
|
|
97
|
-
sameAs: trimSameAs(data
|
|
99
|
+
sameAs: trimSameAs(data[context].primary_author),
|
|
98
100
|
description: data[context].primary_author.metaDescription ?
|
|
99
101
|
escapeExpression(data[context].primary_author.metaDescription) :
|
|
100
102
|
null
|
|
@@ -150,7 +152,7 @@ function getAuthorSchema(metaData, data) {
|
|
|
150
152
|
const schema = {
|
|
151
153
|
'@context': 'https://schema.org',
|
|
152
154
|
'@type': 'Person',
|
|
153
|
-
sameAs: trimSameAs(data
|
|
155
|
+
sameAs: trimSameAs(data.author),
|
|
154
156
|
name: escapeExpression(data.author.name),
|
|
155
157
|
url: metaData.authorUrl,
|
|
156
158
|
image: schemaImageObject(metaData.authorImage) || schemaImageObject(metaData.coverImage),
|
|
@@ -179,4 +181,4 @@ function getSchema(metaData, data) {
|
|
|
179
181
|
return null;
|
|
180
182
|
}
|
|
181
183
|
|
|
182
|
-
module.exports = getSchema;
|
|
184
|
+
module.exports = {getSchema, SOCIAL_PLATFORMS};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// This file contains everything that the helpers and frontend apps require from the core of Ghost
|
|
2
2
|
const settingsCache = require('../../shared/settings-cache');
|
|
3
3
|
const config = require('../../shared/config');
|
|
4
|
+
const settingsHelpers = require('../../server/services/settings-helpers');
|
|
4
5
|
|
|
5
6
|
// Require from the handlebars framework
|
|
6
7
|
const {SafeString} = require('./handlebars');
|
|
@@ -47,6 +48,11 @@ module.exports = {
|
|
|
47
48
|
// TODO: Only expose "get"
|
|
48
49
|
settingsCache: settingsCache,
|
|
49
50
|
|
|
51
|
+
// Settings helpers for calculated settings
|
|
52
|
+
settingsHelpers: {
|
|
53
|
+
isWebAnalyticsEnabled: settingsHelpers.isWebAnalyticsEnabled.bind(settingsHelpers)
|
|
54
|
+
},
|
|
55
|
+
|
|
50
56
|
// TODO: Expose less of the API to make this safe
|
|
51
57
|
api: require('../../server/api').endpoints,
|
|
52
58
|
|
|
@@ -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)
|