From 63103c225199cfddd6c71ab62f9608ab2e8fb144 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Tue, 20 Sep 2022 10:05:41 +0200 Subject: [PATCH] Added click counts to posts admin API (#15435) closes https://github.com/TryGhost/Team/issues/1928 --- .../posts-list/list-item-clicks.hbs | 16 ++++++------ ghost/admin/app/models/post.js | 11 ++++++++ ghost/core/core/server/api/endpoints/posts.js | 3 ++- .../utils/serializers/input/posts.js | 7 ++++- ghost/core/core/server/models/post.js | 9 +++++++ .../admin/__snapshots__/posts.test.js.snap | 26 +++++++++++++------ .../__snapshots__/posts.test.js.snap | 1 + 7 files changed, 55 insertions(+), 18 deletions(-) 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 caf496ff94..4f43daf45c 100644 --- a/ghost/admin/app/components/posts-list/list-item-clicks.hbs +++ b/ghost/admin/app/components/posts-list/list-item-clicks.hbs @@ -122,12 +122,12 @@ {{!-- Opens column --}} {{#if (and @post.email.trackOpens (eq @post.email.status "submitted"))}} -
- +
+ {{#if this.isOpenStatHovered}} - 1,283 opens + {{format-number @post.email.openedCount}} opens {{else}} - 52% opens + {{@post.email.openRate}}% opens {{/if}}
@@ -137,12 +137,12 @@ {{!-- Clicks column --}} {{#if (and @post.email.trackOpens (eq @post.email.status "submitted"))}} -
- +
+ {{#if this.isClickStatHovered}} - 419 clicks + {{format-number @post.count.clicks}} clicks {{else}} - 17% clicks + {{@post.clickRate}}% clicks {{/if}}
diff --git a/ghost/admin/app/models/post.js b/ghost/admin/app/models/post.js index c836ed9c9d..390a7addaf 100644 --- a/ghost/admin/app/models/post.js +++ b/ghost/admin/app/models/post.js @@ -253,6 +253,17 @@ export default Model.extend(Comparable, ValidationEngine, { } }), + clickRate: computed('email.emailCount', 'count.clicks', function () { + if (!this.email || !this.email.emailCount) { + return 0; + } + if (!this.count.clicks) { + return 0; + } + + return Math.round(this.count.clicks / this.email.emailCount * 100); + }), + _getPublishedAtBlogTZ() { let publishedAtUTC = this.publishedAtUTC; let publishedAtBlogDate = this.publishedAtBlogDate; diff --git a/ghost/core/core/server/api/endpoints/posts.js b/ghost/core/core/server/api/endpoints/posts.js index 7d2d15a49e..810efbfc6d 100644 --- a/ghost/core/core/server/api/endpoints/posts.js +++ b/ghost/core/core/server/api/endpoints/posts.js @@ -10,7 +10,8 @@ const allowedIncludes = [ 'tiers', 'newsletter', 'count.signups', - 'count.conversions' + 'count.conversions', + 'count.clicks' ]; const unsafeAttrs = ['status', 'authors', 'visibility']; diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/input/posts.js b/ghost/core/core/server/api/endpoints/utils/serializers/input/posts.js index af2f5a2374..7f029df3f5 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/input/posts.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/input/posts.js @@ -6,6 +6,7 @@ const localUtils = require('../../index'); const mobiledoc = require('../../../../../lib/mobiledoc'); const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta; const clean = require('./utils/clean'); +const {labs} = require('../../../../../../frontend/services/proxy'); function removeSourceFormats(frame) { if (frame.options.formats?.includes('mobiledoc') || frame.options.formats?.includes('lexical')) { @@ -24,7 +25,11 @@ function defaultRelations(frame) { return false; } - frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.conversions']; + if (labs.isSet('emailClicks')) { + frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.conversions', 'count.clicks']; + } else { + frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.conversions']; + } } function setDefaultOrder(frame) { diff --git a/ghost/core/core/server/models/post.js b/ghost/core/core/server/models/post.js index cdcd807114..d6c254d73c 100644 --- a/ghost/core/core/server/models/post.js +++ b/ghost/core/core/server/models/post.js @@ -1345,6 +1345,15 @@ Post = ghostBookshelf.Model.extend({ .whereRaw('posts.id = members_subscription_created_events.attribution_id') .as('count__conversions'); }); + }, + clicks(modelOrCollection) { + modelOrCollection.query('columns', 'posts.*', (qb) => { + qb.countDistinct('members_link_click_events.member_id') + .from('members_link_click_events') + .join('link_redirects', 'members_link_click_events.link_id', 'link_redirects.id') + .whereRaw('posts.id = link_redirects.post_id') + .as('count__clicks'); + }); } }; } diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap index 7eb07b21fa..4baf59aff5 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap @@ -20,6 +20,7 @@ Object { "codeinjection_head": null, "comment_id": Any, "count": Object { + "clicks": 0, "conversions": 0, "signups": 0, }, @@ -67,6 +68,7 @@ Object { "codeinjection_head": null, "comment_id": Any, "count": Object { + "clicks": 0, "conversions": 0, "signups": 0, }, @@ -117,7 +119,7 @@ exports[`Posts API Can browse 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": "9894", + "content-length": "9916", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -145,6 +147,7 @@ Object { "codeinjection_head": null, "comment_id": Any, "count": Object { + "clicks": 0, "conversions": 0, "signups": 0, }, @@ -196,6 +199,7 @@ Object { "codeinjection_head": null, "comment_id": Any, "count": Object { + "clicks": 0, "conversions": 0, "signups": 0, }, @@ -268,7 +272,7 @@ exports[`Posts API Can browse with formats 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": "12760", + "content-length": "12782", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -286,6 +290,7 @@ Object { "codeinjection_head": null, "comment_id": Any, "count": Object { + "clicks": 0, "conversions": 0, "signups": 0, }, @@ -337,7 +342,7 @@ exports[`Posts API Create Can create a post with lexical 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": "3743", + "content-length": "3754", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/posts\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -356,6 +361,7 @@ Object { "codeinjection_head": null, "comment_id": Any, "count": Object { + "clicks": 0, "conversions": 0, "signups": 0, }, @@ -407,7 +413,7 @@ exports[`Posts API Create Can create a post with mobiledoc 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": "3559", + "content-length": "3570", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/posts\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -527,6 +533,7 @@ Object { "codeinjection_head": null, "comment_id": Any, "count": Object { + "clicks": 0, "conversions": 0, "signups": 0, }, @@ -578,7 +585,7 @@ exports[`Posts API Update Can update a post with lexical 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": "3694", + "content-length": "3705", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/posts\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -597,6 +604,7 @@ Object { "codeinjection_head": null, "comment_id": Any, "count": Object { + "clicks": 0, "conversions": 0, "signups": 0, }, @@ -648,7 +656,7 @@ exports[`Posts API Update Can update a post with lexical 4: [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": "3691", + "content-length": "3702", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", @@ -667,6 +675,7 @@ Object { "codeinjection_head": null, "comment_id": Any, "count": Object { + "clicks": 0, "conversions": 0, "signups": 0, }, @@ -718,7 +727,7 @@ exports[`Posts API Update Can update a post with mobiledoc 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": "3504", + "content-length": "3515", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/posts\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -737,6 +746,7 @@ Object { "codeinjection_head": null, "comment_id": Any, "count": Object { + "clicks": 0, "conversions": 0, "signups": 0, }, @@ -788,7 +798,7 @@ exports[`Posts API Update Can update a post with mobiledoc 4: [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": "3501", + "content-length": "3512", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Origin, Accept-Encoding", diff --git a/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap b/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap index 0e045ec3cc..015638a2fd 100644 --- a/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap +++ b/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap @@ -180,6 +180,7 @@ Object { "codeinjection_head": null, "comment_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "count": Object { + "clicks": 0, "conversions": 0, "signups": 0, },