diff --git a/ghost/admin/app/components/posts-list/list-item-clicks.hbs b/ghost/admin/app/components/posts-list/list-item-clicks.hbs index 2c1c65e92d..0f601a4e93 100644 --- a/ghost/admin/app/components/posts-list/list-item-clicks.hbs +++ b/ghost/admin/app/components/posts-list/list-item-clicks.hbs @@ -117,7 +117,7 @@ {{!-- Opened / Signups column --}} - {{#if (and @post.email (not-eq this.settings.membersSignupAccess "none") (not-eq this.settings.editorDefaultEmailRecipients "disabled") (not this.session.user.isContributor) (or @post.isSent @post.isPublished) this.settings.emailTrackOpens @post.email.trackOpens this.feature.emailAnalytics (eq @post.displayName "post"))}} + {{#if @post.showEmailOpenAnalytics }} {{#if this.isHovered}} @@ -130,9 +130,7 @@ opened - {{/if}} - - {{#if (and this.feature.memberAttribution (not-eq this.settings.membersSignupAccess "none") (and @post.isPage) (not this.session.user.isContributor)) }} + {{else if (and @post.isPage @post.showAttributionAnalytics) }} {{@post.count.signups}} @@ -146,7 +144,7 @@ {{!-- Clicked / Conversions column --}} - {{#if (and @post.email (not-eq this.settings.membersSignupAccess "none") (not-eq this.settings.editorDefaultEmailRecipients "disabled") (not this.session.user.isContributor) (or @post.isSent @post.isPublished) this.settings.emailTrackClicks this.feature.emailAnalytics)}} + {{#if @post.showEmailClickAnalytics }} {{#if this.isHovered}} @@ -159,9 +157,7 @@ clicked - {{/if}} - - {{#if (and this.feature.memberAttribution (not-eq this.settings.membersSignupAccess "none") (and @post.isPage) (not this.session.user.isContributor)) }} + {{else if (and @post.isPage @post.showPaidAttributionAnalytics) }} {{@post.count.paid_conversions}} @@ -176,7 +172,7 @@ {{!-- Button column --}}
- {{#if this.isAnalytics}} + {{#if @post.hasAnalyticsPage }} {{svg-jar "stats" title=""}} diff --git a/ghost/admin/app/components/posts-list/list-item-clicks.js b/ghost/admin/app/components/posts-list/list-item-clicks.js index 8f05539b50..b78f85b5a6 100644 --- a/ghost/admin/app/components/posts-list/list-item-clicks.js +++ b/ghost/admin/app/components/posts-list/list-item-clicks.js @@ -24,10 +24,6 @@ export default class PostsListItemClicks extends Component { return text.join(' '); } - get isAnalytics() { - return this.args.post.hasAnalytics; - } - get routeForLink() { if (this.isAnalytics) { return 'posts.analytics'; diff --git a/ghost/admin/app/components/posts/analytics.hbs b/ghost/admin/app/components/posts/analytics.hbs index 784683fcee..c8b7ecb81a 100644 --- a/ghost/admin/app/components/posts/analytics.hbs +++ b/ghost/admin/app/components/posts/analytics.hbs @@ -52,7 +52,7 @@
- {{#if (and this.post.email.trackOpens this.settings.emailTrackOpens) }} + {{#if this.post.showEmailOpenAnalytics }}

{{this.post.email.openRate}}%

@@ -61,7 +61,7 @@
{{/if}} - {{#if this.settings.emailTrackClicks}} + {{#if this.post.showEmailClickAnalytics }}

{{this.post.clickRate}}%

@@ -71,7 +71,7 @@ {{/if}} {{/if}} - {{#if (and (feature 'memberAttribution') (not this.post.emailOnly)) }} + {{#if this.post.showAttributionAnalytics }}

{{format-number this.post.count.signups}}

@@ -79,7 +79,7 @@
- {{#if this.membersUtils.paidMembersEnabled}} + {{#if this.post.showPaidAttributionAnalytics }}

{{format-number this.post.count.paid_conversions}}

diff --git a/ghost/admin/app/components/posts/analytics.js b/ghost/admin/app/components/posts/analytics.js index 5fbbeba756..330ea4f701 100644 --- a/ghost/admin/app/components/posts/analytics.js +++ b/ghost/admin/app/components/posts/analytics.js @@ -119,11 +119,11 @@ export default class Analytics extends Component { } get showLinks() { - return this.settings.get('emailTrackClicks') && (this.post.isSent || (this.post.isPublished && this.post.email)); + return this.post.showEmailClickAnalytics; } get showSources() { - return this.feature.get('sourceAttribution') && !this.membersUtils.isMembersInviteOnly && !this.post.emailOnly; + return this.feature.get('sourceAttribution') && this.post.showAttributionAnalytics; } get isLoaded() { diff --git a/ghost/admin/app/models/email.js b/ghost/admin/app/models/email.js index 06d35afe10..d4c7c7ccb3 100644 --- a/ghost/admin/app/models/email.js +++ b/ghost/admin/app/models/email.js @@ -19,6 +19,7 @@ export default Model.extend({ failedCount: attr('number', {defaultValue: 0}), trackOpens: attr('boolean'), + trackClicks: attr('boolean'), createdAtUTC: attr('moment-utc'), createdBy: attr('string'), diff --git a/ghost/admin/app/models/post.js b/ghost/admin/app/models/post.js index ae9a8febf8..a43ea3d35e 100644 --- a/ghost/admin/app/models/post.js +++ b/ghost/admin/app/models/post.js @@ -73,6 +73,7 @@ export default Model.extend(Comparable, ValidationEngine, { ghostPaths: service(), clock: service(), settings: service(), + membersUtils: service(), displayName: 'post', validationType: 'post', @@ -182,20 +183,44 @@ export default Model.extend(Comparable, ValidationEngine, { return this.isScheduled && !!this.newsletter && !this.email; }), - hasAnalytics: computed('isPost', 'isSent', 'isPublished', 'email', function () { + showEmailOpenAnalytics: computed('isPost', 'isSent', 'isPublished', 'email', function () { return this.isPost && !this.session.user.isContributor && this.settings.get('membersSignupAccess') !== 'none' + && this.settings.get('editorDefaultEmailRecipients') !== 'disabled' + && (this.isSent || this.isPublished) + && this.email + && this.email.trackOpens + && this.settings.get('emailTrackOpens'); + }), + + showEmailClickAnalytics: computed('isPost', 'isSent', 'isPublished', 'email', function () { + return this.isPost + && !this.session.user.isContributor + && this.settings.get('membersSignupAccess') !== 'none' + && this.settings.get('editorDefaultEmailRecipients') !== 'disabled' + && (this.isSent || this.isPublished) + && this.email + && this.email.trackClicks + && this.settings.get('emailTrackClicks'); + }), + + showAttributionAnalytics: computed('isPage', 'emailOnly', 'isPublished', 'membersUtils.isMembersInviteOnly', function () { + return (this.isPage || !this.emailOnly) + && this.isPublished + && this.feature.get('memberAttribution') + && !this.membersUtils.isMembersInviteOnly + && !this.session.user.isContributor; + }), + + showPaidAttributionAnalytics: computed.and('showAttributionAnalytics', 'membersUtils.paidMembersEnabled'), + + hasAnalyticsPage: computed('isPost', 'showEmailOpenAnalytics', 'showEmailClickAnalytics', 'showAttributionAnalytics', function () { + return this.isPost && ( - ( - // We have clicks or opens data - (this.isSent || (this.isPublished && this.email)) - && (this.settings.get('emailTrackClicks') || this.settings.get('emailTrackOpens')) - ) - || ( - // We have attribution data for pubished posts - this.isPublished && this.feature.get('memberAttribution') - ) + this.showEmailOpenAnalytics + || this.showEmailClickAnalytics + || this.showAttributionAnalytics ); }), diff --git a/ghost/core/core/server/data/migrations/versions/5.17/2022-09-29-12-39-add-track-clicks-column-to-emails.js b/ghost/core/core/server/data/migrations/versions/5.17/2022-09-29-12-39-add-track-clicks-column-to-emails.js new file mode 100644 index 0000000000..d4b09bce58 --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/5.17/2022-09-29-12-39-add-track-clicks-column-to-emails.js @@ -0,0 +1,7 @@ +const {createAddColumnMigration} = require('../../utils'); + +module.exports = createAddColumnMigration('emails', 'track_clicks', { + type: 'bool', + nullable: false, + defaultTo: false +}); diff --git a/ghost/core/core/server/data/schema/schema.js b/ghost/core/core/server/data/schema/schema.js index fde4880bb6..09a7592086 100644 --- a/ghost/core/core/server/data/schema/schema.js +++ b/ghost/core/core/server/data/schema/schema.js @@ -723,6 +723,7 @@ module.exports = { html: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true}, plaintext: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true}, track_opens: {type: 'bool', nullable: false, defaultTo: false}, + track_clicks: {type: 'bool', nullable: false, defaultTo: false}, submitted_at: {type: 'dateTime', nullable: false}, newsletter_id: {type: 'string', maxlength: 24, nullable: true, references: 'newsletters.id'}, created_at: {type: 'dateTime', nullable: false}, diff --git a/ghost/core/core/server/models/email.js b/ghost/core/core/server/models/email.js index f285f3dd3f..0d49e2ccb0 100644 --- a/ghost/core/core/server/models/email.js +++ b/ghost/core/core/server/models/email.js @@ -10,6 +10,7 @@ const Email = ghostBookshelf.Model.extend({ status: 'pending', recipient_filter: 'status:-free', track_opens: false, + track_clicks: false, delivered_count: 0, opened_count: 0, failed_count: 0 diff --git a/ghost/core/core/server/services/mega/mega.js b/ghost/core/core/server/services/mega/mega.js index 9ed45f6bd3..7156763f2c 100644 --- a/ghost/core/core/server/services/mega/mega.js +++ b/ghost/core/core/server/services/mega/mega.js @@ -239,6 +239,7 @@ const addEmail = async (postModel, options) => { plaintext: emailData.plaintext, submitted_at: moment().toDate(), track_opens: !!settingsCache.get('email_track_opens'), + track_clicks: !!settingsCache.get('email_track_clicks'), recipient_filter: emailRecipientFilter, newsletter_id: newsletter.id }, knexOptions); diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/emails.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/emails.test.js.snap index 6ab78db908..74466f3e17 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/emails.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/emails.test.js.snap @@ -22,6 +22,7 @@ Object { "status": "submitted", "subject": "You got mailed!", "submitted_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "track_clicks": false, "track_opens": false, "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, @@ -45,6 +46,7 @@ Object { "status": "failed", "subject": "You got mailed! Again!", "submitted_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "track_clicks": false, "track_opens": false, "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, @@ -67,7 +69,7 @@ exports[`Emails API Can browse emails 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1243", + "content-length": "1285", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -97,6 +99,7 @@ Object { "status": "submitted", "subject": "You got mailed!", "submitted_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "track_clicks": false, "track_opens": false, "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, @@ -109,7 +112,7 @@ exports[`Emails API Can read an email 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "561", + "content-length": "582", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -139,6 +142,7 @@ Object { "status": "pending", "subject": "You got mailed! Again!", "submitted_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "track_clicks": false, "track_opens": false, "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, @@ -151,7 +155,7 @@ exports[`Emails API Can retry a failed email 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "607", + "content-length": "628", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", diff --git a/ghost/core/test/unit/server/data/schema/integrity.test.js b/ghost/core/test/unit/server/data/schema/integrity.test.js index 51c75ca37f..d1ab16bcca 100644 --- a/ghost/core/test/unit/server/data/schema/integrity.test.js +++ b/ghost/core/test/unit/server/data/schema/integrity.test.js @@ -35,7 +35,7 @@ const validateRouteSettings = require('../../../../../core/server/services/route */ describe('DB version integrity', function () { // Only these variables should need updating - const currentSchemaHash = '999d625400e6d87efe0fd66e6bda4059'; + const currentSchemaHash = '2a59debcacc1e3dc0b15e2f729ca4bdb'; const currentFixturesHash = '8cf221f0ed930ac1fe8030a58e60d64b'; const currentSettingsHash = '2978a5684a2d5fcf089f61f5d368a0c0'; const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';