diff --git a/ghost/admin/app/components/posts/analytics.hbs b/ghost/admin/app/components/posts/analytics.hbs index 00b5f0f6a2..12877b6fb7 100644 --- a/ghost/admin/app/components/posts/analytics.hbs +++ b/ghost/admin/app/components/posts/analytics.hbs @@ -76,19 +76,10 @@ {{#if this.post.showAttributionAnalytics }}
-

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

-

{{gh-pluralize this.post.count.signups "signup" without-count=true}}

+

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

+

{{gh-pluralize this.post.count.conversions "conversions" without-count=true}}

- - {{#if this.post.showPaidAttributionAnalytics }} -
- -

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

-

Paid {{gh-pluralize this.post.count.paid_conversions "conversion" without-count=true}}

-
-
- {{/if}} {{/if}} {{#if this.post.showAudienceFeedback }} diff --git a/ghost/admin/app/components/posts/old-analytics.hbs b/ghost/admin/app/components/posts/old-analytics.hbs new file mode 100644 index 0000000000..59d55beb5a --- /dev/null +++ b/ghost/admin/app/components/posts/old-analytics.hbs @@ -0,0 +1,157 @@ +
+ + +
+
+ + Posts + + {{svg-jar "arrow-right-small"}}Analytics +
+

+ {{this.post.title}} +

+
+
+ {{#if this.post.hasBeenEmailed }} + {{#if this.post.emailOnly}} + Sent + {{else}} + Published and sent + {{/if}} + {{else}} + Published + {{#if @post.didEmailFail}} + but failed to send + {{else}} + on your site + {{/if}} + {{/if}} + + {{#let (moment-site-tz this.post.publishedAtUTC) as |publishedAt|}} + on + {{moment-format publishedAt "D MMM YYYY"}} + at + {{moment-format publishedAt "HH:mm"}} + {{/let}} +
+ + {{svg-jar "pen" title=""}}Edit post + +
+
+
+ +

+ Engagement +

+
+ {{#if this.post.hasBeenEmailed}} +
+ +

{{format-number this.post.email.emailCount}}

+

Sent

+
+
+ + {{#if this.post.showEmailOpenAnalytics }} +
+ +

{{format-number this.post.email.openedCount}}

+

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

+
+
+ {{/if}} + + {{#if this.post.showEmailClickAnalytics }} +
+ +

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

+

Clicked — {{this.post.clickRate}}%

+
+
+ {{/if}} + {{/if}} + + {{#if this.post.showAttributionAnalytics }} +
+ +

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

+

{{gh-pluralize this.post.count.signups "signup" without-count=true}}

+
+
+ + {{#if this.post.showPaidAttributionAnalytics }} +
+ +

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

+

Paid {{gh-pluralize this.post.count.paid_conversions "conversion" without-count=true}}

+
+
+ {{/if}} + {{/if}} +
+ + {{#if this.isLoaded }} + {{#if this.showLinks }} + {{#if (is-empty this.links) }} + {{!-- Empty state --}} + {{else}} + + {{/if}} + {{/if}} + + {{#if this.showSources }} + {{#if (is-empty this.sources) }} + {{!-- Empty state --}} + {{else}} +

+ Growth from this post +

+
+
+
+ +
+
+
+ {{/if}} + {{/if}} + +

+ Get started with analytics +

+
+ +
+
+
+

Understanding analytics in Ghost

+

Find out how to review the performance of your content and get the most out of post analytics in Ghost.

+
+ +
+
+ +
+
+
+

How to get your content seen online

+

Use these content distribution tactics to get more people to discover your work and increase engagement.

+
+ +
+
+
+ {{else}} +
+
+
+
+
+ {{/if}} +
diff --git a/ghost/admin/app/components/posts/old-analytics.js b/ghost/admin/app/components/posts/old-analytics.js new file mode 100644 index 0000000000..e784d9b15b --- /dev/null +++ b/ghost/admin/app/components/posts/old-analytics.js @@ -0,0 +1,196 @@ +import Component from '@glimmer/component'; +import {action} from '@ember/object'; +import {inject as service} from '@ember/service'; +import {task} from 'ember-concurrency'; +import {tracked} from '@glimmer/tracking'; + +/** + * @typedef {import('../../services/dashboard-stats').SourceAttributionCount} SourceAttributionCount +*/ + +const DISPLAY_OPTIONS = [{ + name: 'Free signups', + value: 'signups' +}, { + name: 'Paid conversions', + value: 'paid' +}]; + +export default class Analytics extends Component { + @service ajax; + @service ghostPaths; + @service settings; + @service membersUtils; + @service utils; + @service feature; + + @tracked sources = null; + @tracked links = null; + @tracked sortColumn = 'signups'; + displayOptions = DISPLAY_OPTIONS; + + get post() { + return this.args.post; + } + + get allowedDisplayOptions() { + if (!this.hasPaidConversionData) { + return this.displayOptions.filter(d => d.value === 'signups'); + } + + if (!this.hasFreeSignups) { + return this.displayOptions.filter(d => d.value === 'paid'); + } + + return this.displayOptions; + } + + get isDropdownDisabled() { + if (!this.hasPaidConversionData || !this.hasFreeSignups) { + return true; + } + + return false; + } + + get selectedDisplayOption() { + if (!this.hasPaidConversionData) { + return this.displayOptions.find(d => d.value === 'signups'); + } + + if (!this.hasFreeSignups) { + return this.displayOptions.find(d => d.value === 'paid'); + } + + return this.displayOptions.find(d => d.value === this.sortColumn) ?? this.displayOptions[0]; + } + + get selectedSortColumn() { + if (!this.hasPaidConversionData) { + return 'signups'; + } + + if (!this.hasFreeSignups) { + return 'paid'; + } + return this.sortColumn; + } + + get hasPaidConversionData() { + return this.sources.some(sourceData => sourceData.paidConversions > 0); + } + + get hasFreeSignups() { + return this.sources.some(sourceData => sourceData.signups > 0); + } + + @action + onDisplayChange(selected) { + this.sortColumn = selected.value; + } + + @action + setSortColumn(column) { + this.sortColumn = column; + } + + @action + updateLink(linkId, linkTo) { + this.links = this.links?.map((link) => { + if (link.link.link_id === linkId) { + return { + ...link, + link: { + ...link.link, + to: this.utils.cleanTrackedUrl(linkTo, false), + title: this.utils.cleanTrackedUrl(linkTo, true) + } + }; + } + return link; + }); + } + + @action + loadData() { + if (this.showSources) { + this.fetchReferrersStats(); + } else { + this.sources = []; + } + + if (this.showLinks) { + this.fetchLinks(); + } else { + this.links = []; + } + } + + async fetchReferrersStats() { + if (this._fetchReferrersStats.isRunning) { + return this._fetchReferrersStats.last; + } + return this._fetchReferrersStats.perform(); + } + + async fetchLinks() { + if (this._fetchLinks.isRunning) { + return this._fetchLinks.last; + } + return this._fetchLinks.perform(); + } + + @task + *_fetchReferrersStats() { + let statsUrl = this.ghostPaths.url.api(`stats/referrers/posts/${this.post.id}`); + let result = yield this.ajax.request(statsUrl); + this.sources = result.stats.map((stat) => { + return { + source: stat.source ?? 'Direct', + signups: stat.signups, + paidConversions: stat.paid_conversions + }; + }); + } + + @task + *_fetchLinks() { + const filter = `post_id:${this.post.id}`; + let statsUrl = this.ghostPaths.url.api(`links/`) + `?filter=${encodeURIComponent(filter)}`; + let result = yield this.ajax.request(statsUrl); + const links = result.links.map((link) => { + return { + ...link, + link: { + ...link.link, + to: this.utils.cleanTrackedUrl(link.link.to, false), + title: this.utils.cleanTrackedUrl(link.link.to, true) + } + }; + }); + + // Remove duplicates by title ad merge + const linksByTitle = links.reduce((acc, link) => { + if (!acc[link.link.title]) { + acc[link.link.title] = link; + } else { + acc[link.link.title].clicks += link.clicks; + } + return acc; + }, {}); + + this.links = Object.values(linksByTitle); + } + + get showLinks() { + return this.post.showEmailClickAnalytics; + } + + get showSources() { + return this.feature.get('sourceAttribution') && this.post.showAttributionAnalytics; + } + + get isLoaded() { + return this.links !== null && this.souces !== null; + } +} diff --git a/ghost/admin/app/templates/posts/analytics.hbs b/ghost/admin/app/templates/posts/analytics.hbs index c4f73092eb..f3d5511b34 100644 --- a/ghost/admin/app/templates/posts/analytics.hbs +++ b/ghost/admin/app/templates/posts/analytics.hbs @@ -1 +1,5 @@ - +{{#if (feature 'audienceFeedback') }} + +{{else}} + +{{/if}} 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 e123266059..80d86d7925 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 @@ -26,7 +26,7 @@ function defaultRelations(frame) { } if (labs.isSet('audienceFeedback')) { - frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.paid_conversions', 'count.clicks', 'count.sentiment', 'count.positive_feedback']; + frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.conversions', 'count.clicks', 'count.sentiment', 'count.positive_feedback']; } else { frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.paid_conversions', 'count.clicks']; } diff --git a/ghost/core/core/server/models/post.js b/ghost/core/core/server/models/post.js index 64bc9509cf..33456e1980 100644 --- a/ghost/core/core/server/models/post.js +++ b/ghost/core/core/server/models/post.js @@ -1346,6 +1346,26 @@ Post = ghostBookshelf.Model.extend({ .as('count__paid_conversions'); }); }, + /** + * Combination of sigups and paid conversions, but unique per member + */ + conversions(modelOrCollection) { + modelOrCollection.query('columns', 'posts.*', (qb) => { + qb.count('*') + .from('k') + .with('k', (q) => { + q.select('member_id') + .from('members_subscription_created_events') + .whereRaw('posts.id = members_subscription_created_events.attribution_id') + .union(function () { + this.select('member_id') + .from('members_created_events') + .whereRaw('posts.id = members_created_events.attribution_id'); + }); + }) + .as('count__conversions'); + }); + }, clicks(modelOrCollection) { modelOrCollection.query('columns', 'posts.*', (qb) => { qb.countDistinct('members_click_events.member_id') 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 49bcd03dc0..4b7d5c8ad2 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 @@ -21,10 +21,9 @@ Object { "comment_id": Any, "count": Object { "clicks": 0, - "paid_conversions": 0, + "conversions": 0, "positive_feedback": 0, "sentiment": 0, - "signups": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "custom_excerpt": null, @@ -71,10 +70,9 @@ Object { "comment_id": Any, "count": Object { "clicks": 0, - "paid_conversions": 0, + "conversions": 0, "positive_feedback": 0, "sentiment": 0, - "signups": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "custom_excerpt": null, @@ -123,7 +121,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": "10226", + "content-length": "10192", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -152,10 +150,9 @@ Object { "comment_id": Any, "count": Object { "clicks": 0, - "paid_conversions": 0, + "conversions": 0, "positive_feedback": 0, "sentiment": 0, - "signups": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "custom_excerpt": null, @@ -206,10 +203,9 @@ Object { "comment_id": Any, "count": Object { "clicks": 0, - "paid_conversions": 0, + "conversions": 0, "positive_feedback": 0, "sentiment": 0, - "signups": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "custom_excerpt": null, @@ -280,7 +276,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": "13092", + "content-length": "13058", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -299,10 +295,9 @@ Object { "comment_id": Any, "count": Object { "clicks": 0, - "paid_conversions": 0, + "conversions": 0, "positive_feedback": 0, "sentiment": 0, - "signups": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "custom_excerpt": null, @@ -352,7 +347,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": "3909", + "content-length": "3892", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/posts\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -372,10 +367,9 @@ Object { "comment_id": Any, "count": Object { "clicks": 0, - "paid_conversions": 0, + "conversions": 0, "positive_feedback": 0, "sentiment": 0, - "signups": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "custom_excerpt": null, @@ -425,7 +419,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": "3725", + "content-length": "3708", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/posts\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -546,10 +540,9 @@ Object { "comment_id": Any, "count": Object { "clicks": 0, - "paid_conversions": 0, + "conversions": 0, "positive_feedback": 0, "sentiment": 0, - "signups": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "custom_excerpt": null, @@ -599,7 +592,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": "3860", + "content-length": "3843", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/posts\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -619,10 +612,9 @@ Object { "comment_id": Any, "count": Object { "clicks": 0, - "paid_conversions": 0, + "conversions": 0, "positive_feedback": 0, "sentiment": 0, - "signups": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "custom_excerpt": null, @@ -672,7 +664,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": "3857", + "content-length": "3840", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -692,10 +684,9 @@ Object { "comment_id": Any, "count": Object { "clicks": 0, - "paid_conversions": 0, + "conversions": 0, "positive_feedback": 0, "sentiment": 0, - "signups": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "custom_excerpt": null, @@ -745,7 +736,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": "3670", + "content-length": "3653", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/posts\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -765,10 +756,9 @@ Object { "comment_id": Any, "count": Object { "clicks": 0, - "paid_conversions": 0, + "conversions": 0, "positive_feedback": 0, "sentiment": 0, - "signups": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "custom_excerpt": null, @@ -818,7 +808,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": "3667", + "content-length": "3650", "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/e2e-webhooks/__snapshots__/posts.test.js.snap b/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap index 2cb21e9c38..1cba24e813 100644 --- a/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap +++ b/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap @@ -283,10 +283,9 @@ Object { "comment_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "count": Object { "clicks": 0, - "paid_conversions": 0, + "conversions": 0, "positive_feedback": 0, "sentiment": 0, - "signups": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "custom_excerpt": null, @@ -521,10 +520,9 @@ Object { "comment_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "count": Object { "clicks": 0, - "paid_conversions": 0, + "conversions": 0, "positive_feedback": 0, "sentiment": 0, - "signups": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "custom_excerpt": null, @@ -737,10 +735,9 @@ Object { "comment_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "count": Object { "clicks": 0, - "paid_conversions": 0, + "conversions": 0, "positive_feedback": 0, "sentiment": 0, - "signups": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "custom_excerpt": null, @@ -1019,10 +1016,9 @@ Object { "comment_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "count": Object { "clicks": 0, - "paid_conversions": 0, + "conversions": 0, "positive_feedback": 0, "sentiment": 0, - "signups": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "custom_excerpt": null,