Added click counts to posts admin API (#15435)

closes https://github.com/TryGhost/Team/issues/1928
This commit is contained in:
Simon Backx 2022-09-20 10:05:41 +02:00 committed by GitHub
parent e9f3d90147
commit 63103c2251
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 55 additions and 18 deletions

View File

@ -122,12 +122,12 @@
{{!-- Opens column --}}
<LinkTo @route="editor.edit" @models={{array @post.displayName @post.id}} class="permalink gh-list-data gh-post-list-opens">
{{#if (and @post.email.trackOpens (eq @post.email.status "submitted"))}}
<div class="flex flex-column items-end">
<span class="gh-content-email-stats" {{on "mouseover" (fn (mut this.isOpenStatHovered) true)}} {{on "mouseleave" (fn (mut this.isOpenStatHovered) false)}}>
<div class="flex flex-column items-end" {{on "mouseover" (fn (mut this.isOpenStatHovered) true)}} {{on "mouseleave" (fn (mut this.isOpenStatHovered) false)}}>
<span class="gh-content-email-stats">
{{#if this.isOpenStatHovered}}
<strong>1,283</strong> opens
<strong>{{format-number @post.email.openedCount}}</strong> opens
{{else}}
<strong>52%</strong> opens
<strong>{{@post.email.openRate}}%</strong> opens
{{/if}}
</span>
</div>
@ -137,12 +137,12 @@
{{!-- Clicks column --}}
<LinkTo @route="editor.edit" @models={{array @post.displayName @post.id}} class="permalink gh-list-data gh-post-list-clicks">
{{#if (and @post.email.trackOpens (eq @post.email.status "submitted"))}}
<div class="flex flex-column items-end">
<span class="gh-content-email-stats" {{on "mouseover" (fn (mut this.isClickStatHovered) true)}} {{on "mouseleave" (fn (mut this.isClickStatHovered) false)}}>
<div class="flex flex-column items-end" {{on "mouseover" (fn (mut this.isClickStatHovered) true)}} {{on "mouseleave" (fn (mut this.isClickStatHovered) false)}}>
<span class="gh-content-email-stats">
{{#if this.isClickStatHovered}}
<strong>419</strong> clicks
<strong>{{format-number @post.count.clicks}}</strong> clicks
{{else}}
<strong>17%</strong> clicks
<strong>{{@post.clickRate}}%</strong> clicks
{{/if}}
</span>
</div>

View File

@ -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;

View File

@ -10,7 +10,8 @@ const allowedIncludes = [
'tiers',
'newsletter',
'count.signups',
'count.conversions'
'count.conversions',
'count.clicks'
];
const unsafeAttrs = ['status', 'authors', 'visibility'];

View File

@ -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,8 +25,12 @@ function defaultRelations(frame) {
return false;
}
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) {
let includesOrderedRelations = false;

View File

@ -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');
});
}
};
}

View File

@ -20,6 +20,7 @@ Object {
"codeinjection_head": null,
"comment_id": Any<String>,
"count": Object {
"clicks": 0,
"conversions": 0,
"signups": 0,
},
@ -67,6 +68,7 @@ Object {
"codeinjection_head": null,
"comment_id": Any<String>,
"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<String>,
"count": Object {
"clicks": 0,
"conversions": 0,
"signups": 0,
},
@ -196,6 +199,7 @@ Object {
"codeinjection_head": null,
"comment_id": Any<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"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",

View File

@ -180,6 +180,7 @@ Object {
"codeinjection_head": null,
"comment_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"count": Object {
"clicks": 0,
"conversions": 0,
"signups": 0,
},