mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-03 03:55:26 +03:00
c2dfb2b579
refs https://github.com/TryGhost/Team/issues/2213 When an email contains multiple links to the same destination, that link is only shown once on the analytics page. The total displayed count is not summed correctly.
277 lines
8.0 KiB
JavaScript
277 lines
8.0 KiB
JavaScript
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';
|
|
@tracked showSuccess;
|
|
@tracked updateLinkId;
|
|
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);
|
|
}
|
|
|
|
get totalFeedback() {
|
|
return this.post.count.positive_feedback + this.post.count.negative_feedback;
|
|
}
|
|
|
|
get feedbackChartData() {
|
|
const values = [this.post.count.positive_feedback, this.post.count.negative_feedback];
|
|
const labels = ['More like this', 'Less like this'];
|
|
const links = [
|
|
{filterParam: '(feedback.post_id:' + this.post.id + '+feedback.score:1)'},
|
|
{filterParam: '(feedback.post_id:' + this.post.id + '+feedback.score:0)'}
|
|
];
|
|
const colors = ['#F080B2', '#8452f633'];
|
|
return {values, labels, links, colors};
|
|
}
|
|
|
|
@action
|
|
onDisplayChange(selected) {
|
|
this.sortColumn = selected.value;
|
|
}
|
|
|
|
@action
|
|
setSortColumn(column) {
|
|
this.sortColumn = column;
|
|
}
|
|
|
|
@action
|
|
updateLink(linkId, linkTo) {
|
|
if (this._updateLinks.isRunning) {
|
|
return this._updateLinks.last;
|
|
}
|
|
return this._updateLinks.perform(linkId, linkTo);
|
|
}
|
|
|
|
@action
|
|
loadData() {
|
|
if (this.showSources) {
|
|
this.fetchReferrersStats();
|
|
} else {
|
|
this.sources = [];
|
|
}
|
|
|
|
if (this.showLinks) {
|
|
this.fetchLinks();
|
|
} else {
|
|
this.links = [];
|
|
}
|
|
}
|
|
|
|
updateLinkData(linksData) {
|
|
let updatedLinks;
|
|
if (this.links?.length) {
|
|
updatedLinks = this.links.map((link) => {
|
|
let linkData = linksData.find(l => l.link.link_id === link.link.link_id);
|
|
if (linkData) {
|
|
return {
|
|
...linkData,
|
|
link: {
|
|
...linkData.link,
|
|
originalTo: linkData.link.to,
|
|
to: this.utils.cleanTrackedUrl(linkData.link.to, false),
|
|
title: this.utils.cleanTrackedUrl(linkData.link.to, true)
|
|
}
|
|
};
|
|
}
|
|
return link;
|
|
});
|
|
} else {
|
|
updatedLinks = linksData.map((link) => {
|
|
return {
|
|
...link,
|
|
link: {
|
|
...link.link,
|
|
originalTo: link.link.to,
|
|
to: this.utils.cleanTrackedUrl(link.link.to, false),
|
|
title: this.utils.cleanTrackedUrl(link.link.to, true)
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
// Remove duplicates by title ad merge
|
|
const linksByTitle = updatedLinks.reduce((acc, link) => {
|
|
if (!acc[link.link.title]) {
|
|
acc[link.link.title] = link;
|
|
} else {
|
|
if (!acc[link.link.title].count) {
|
|
acc[link.link.title].count = {clicks: 0};
|
|
}
|
|
if (!acc[link.link.title].count.clicks) {
|
|
acc[link.link.title].count.clicks = 0;
|
|
}
|
|
|
|
acc[link.link.title].count.clicks += (link.count?.clicks ?? 0);
|
|
}
|
|
return acc;
|
|
}, {});
|
|
|
|
this.links = Object.values(linksByTitle);
|
|
}
|
|
|
|
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
|
|
*_updateLinks(linkId, newLink) {
|
|
this.updateLinkId = linkId;
|
|
let currentLink;
|
|
this.links = this.links?.map((link) => {
|
|
if (link.link.link_id === linkId) {
|
|
currentLink = new URL(link.link.originalTo);
|
|
return {
|
|
...link,
|
|
link: {
|
|
...link.link,
|
|
to: this.utils.cleanTrackedUrl(newLink, false),
|
|
title: this.utils.cleanTrackedUrl(newLink, true)
|
|
}
|
|
};
|
|
}
|
|
return link;
|
|
});
|
|
|
|
const filter = `post_id:${this.post.id}+to:'${currentLink}'`;
|
|
let bulkUpdateUrl = this.ghostPaths.url.api(`links/bulk`) + `?filter=${encodeURIComponent(filter)}`;
|
|
yield this.ajax.put(bulkUpdateUrl, {
|
|
data: {
|
|
bulk: {
|
|
action: 'updateLink',
|
|
meta: {link: {to: newLink}}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Refresh links data
|
|
const linksFilter = `post_id:${this.post.id}`;
|
|
let statsUrl = this.ghostPaths.url.api(`links/`) + `?filter=${encodeURIComponent(linksFilter)}`;
|
|
let result = yield this.ajax.request(statsUrl);
|
|
this.updateLinkData(result.links);
|
|
this.showSuccess = this.updateLinkId;
|
|
setTimeout(() => {
|
|
this.showSuccess = null;
|
|
}, 2000);
|
|
}
|
|
|
|
@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);
|
|
this.updateLinkData(result.links);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|