TryGhost/Ghost

View on GitHub
ghost/admin/app/components/posts/debug.js

Summary

Maintainability
A
0 mins
Test Coverage
import Component from '@glimmer/component';
import moment from 'moment-timezone';
import {action} from '@ember/object';
import {didCancel, task, timeout} from 'ember-concurrency';
import {formatNumber} from 'ghost-admin/helpers/format-number';
import {ghPluralize} from 'ghost-admin/helpers/gh-pluralize';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class Debug extends Component {
    @service ajax;
    @service ghostPaths;
    @service settings;
    @service membersUtils;
    @service utils;
    @service feature;
    @service store;

    @tracked emailBatches = null;
    @tracked recipientFailures = null;
    @tracked loading = true;
    @tracked analyticsStatus = null;
    @tracked latestEmail = null;

    get post() {
        return this.args.post;
    }

    get email() {
        return this.latestEmail ?? this.post.email;
    }

    async updateEmail() {
        try {
            this.latestEmail = await this.store.findRecord('email', this.post.email.id, {reload: true});
        } catch (e) {
            // Skip
        }
    }

    get emailError() {
        // get failed batches count
        let failedBatches = this.emailBatchesData?.filter((batch) => {
            return batch.statusClass === 'failed';
        }).length || 0;
        // get total batch count
        let totalBatches = this.emailBatchesData?.length || 0;

        let details = (this.loading || !totalBatches) ? '' : `${failedBatches} of ${ghPluralize(totalBatches, 'batch')} failed to send, check below for more details.`;
        return {
            message: this.post.email?.error || 'Failed to send email.',
            details
        };
    }

    get emailSettings() {
        return {
            statusClass: this.email?.status,
            status: this.getStatusLabel(this.email?.status),
            recipientFilter: this.email?.recipientFilter,
            createdAt: this.email?.createdAtUTC ? moment(this.email.createdAtUTC).format('DD MMM, YYYY, HH:mm:ss') : '',
            submittedAt: this.email?.submittedAtUTC ? moment(this.email.submittedAtUTC).format('DD MMM, YYYY, HH:mm:ss') : '',
            emailsSent: this.email?.emailCount,
            emailsDelivered: this.email?.deliveredCount,
            emailsOpened: this.email?.openedCount,
            emailsFailed: this.email?.failedCount,
            trackOpens: this.email?.trackOpens,
            trackClicks: this.email?.trackClicks,
            feedbackEnabled: this.email?.feedbackEnabled
        };
    }

    get tabTotals() {
        return {
            temporaryFailures: formatNumber(this.temporaryFailureData?.length || 0),
            permanentFailures: formatNumber(this.permanentFailureData?.length || 0),
            erroredBatches: formatNumber(this.emailBatchesData?.filter((batch) => {
                return batch.statusClass === 'failed';
            }).length || 0)
        };
    }

    get emailBatchesData() {
        return this.emailBatches?.map((batch) => {
            return {
                id: batch.id,
                status: this.getStatusLabel(batch.status),
                statusClass: batch.status,
                createdAt: batch.created_at ? moment(batch.created_at).format('DD MMM, YYYY, HH:mm:ss') : '',
                segment: batch.member_segment || '',
                providerId: batch.provider_id || null,
                errorMessage: batch.error_message || '',
                errorStatusCode: batch.error_status_code || '',
                recipientCount: batch.count?.recipients || 0
            };
        });
    }

    get temporaryFailureData() {
        return this.recipientFailures?.filter((failure) => {
            return failure.severity === 'temporary';
        }).map((failure) => {
            return {
                id: failure.id,
                code: failure.code,
                failedAt: failure.failed_at ? moment(failure.failed_at).format('DD MMM, YYYY, HH:mm:ss') : '',
                processedAt: failure.email_recipient.processed_at ? moment(failure.email_recipient.processed_at).format('DD MMM, YYYY, HH:mm:ss') : '',
                batchId: failure.email_recipient.batch_id,
                enhancedCode: failure.enhanced_code,
                message: failure.message,
                recipient: {
                    name: failure.email_recipient.member_name || '',
                    email: failure.email_recipient.member_email || '',
                    initials: this.getInitials(failure.email_recipient?.member_name || failure.email_recipient?.member_email)
                },
                member: {
                    record: failure.member,
                    id: failure.member?.id,
                    name: failure.member?.name || '',
                    email: failure.member?.email || '',
                    initials: this.getInitials(failure.member?.name)
                }
            };
        });
    }

    get permanentFailureData() {
        return this.recipientFailures?.filter((failure) => {
            return failure.severity === 'permanent';
        }).map((failure) => {
            return {
                id: failure.id,
                code: failure.code,
                enhancedCode: failure.enhanced_code,
                message: failure.message,
                recipient: {
                    name: failure.email_recipient.member_name || '',
                    email: failure.email_recipient.member_email || '',
                    initials: this.getInitials(failure.email_recipient?.member_name || failure.email_recipient?.member_email)
                },
                member: {
                    record: failure.member,
                    id: failure.member?.id,
                    name: failure.member?.name || '',
                    email: failure.member?.email || '',
                    initials: this.getInitials(failure.member?.name)
                }
            };
        });
    }

    getInitials(name) {
        if (!name) {
            return 'U';
        }
        let names = name.split(' ');
        let intials = names.length > 1 ? [names[0][0], names[names.length - 1][0]] : [names[0][0]];
        return intials.join('').toUpperCase();
    }

    getStatusLabel(status) {
        if (status === 'submitted') {
            return 'Submitted';
        } else if (status === 'submitting') {
            return 'Submitting';
        } else if (status === 'pending') {
            return 'Pending';
        } else if (status === 'failed') {
            return 'Failed';
        }
        return status;
    }

    @action
    loadData() {
        if (this.post.email) {
            this.fetchEmailBatches();
            this.fetchRecipientFailures();
            this.pollAnalyticsStatus.perform();
            this.pollEmail.perform();
        }
    }

    async fetchEmailBatches() {
        try {
            if (this._fetchEmailBatches.isRunning) {
                return this._fetchEmailBatches.last;
            }
            return this._fetchEmailBatches.perform();
        } catch (e) {
            if (!didCancel(e)) {
                // re-throw the non-cancelation error
                throw e;
            }
        }
    }

    @task
    *_fetchEmailBatches() {
        const data = {
            include: 'count.recipients',
            limit: 'all',
            order: 'status asc, created_at desc'
        };

        let statsUrl = this.ghostPaths.url.api(`emails/${this.post.email.id}/batches`);
        let result = yield this.ajax.request(statsUrl, {data});
        this.emailBatches = result.batches;
        this.loading = false;
    }

    async fetchRecipientFailures() {
        try {
            if (this._fetchRecipientFailures.isRunning) {
                return this._fetchRecipientFailures.last;
            }
            return this._fetchRecipientFailures.perform();
        } catch (e) {
            if (!didCancel(e)) {
                // re-throw the non-cancelation error
                throw e;
            }
        }
    }

    @task
    *pollAnalyticsStatus() {
        while (true) {
            yield this.fetchAnalyticsStatus();
            yield timeout(5 * 1000);
        }
    }

    @task
    *pollEmail() {
        while (true) {
            yield timeout(10 * 1000);
            yield this.updateEmail();
        }
    }

    async fetchAnalyticsStatus() {
        try {
            if (this._fetchAnalyticsStatus.isRunning) {
                return this._fetchAnalyticsStatus.last;
            }
            return this._fetchAnalyticsStatus.perform();
        } catch (e) {
            // Skip
        }
    }

    @task
    *_fetchRecipientFailures() {
        const data = {
            include: 'member,email_recipient',
            limit: 'all'
        };
        let statsUrl = this.ghostPaths.url.api(`/emails/${this.post.email.id}/recipient-failures`);
        let result = yield this.ajax.request(statsUrl, {data});
        this.recipientFailures = result.failures;
    }

    @task
    *_fetchAnalyticsStatus() {
        let statsUrl = this.ghostPaths.url.api(`/emails/${this.post.email.id}/analytics`);
        let result = yield this.ajax.request(statsUrl);
        this.analyticsStatus = result;

        // Parse dates
        for (const type of Object.keys(result)) {
            if (!result[type]) {
                result[type] = {};
            }
            let object = result[type];
            for (const key of ['lastStarted', 'lastBegin', 'lastEventTimestamp']) {
                if (object[key]) {
                    object[key] = moment(object[key]).format('DD MMM, YYYY, HH:mm:ss.SSS');
                } else {
                    object[key] = 'N/A';
                }
            }

            if (object.schedule) {
                object = object.schedule;
                for (const key of ['begin', 'end']) {
                    if (object[key]) {
                        object[key] = moment(object[key]).format('DD MMM, YYYY, HH:mm:ss.SSS');
                    } else {
                        object[key] = 'N/A';
                    }
                }
            }
        }
    }

    @action
    scheduleAnalytics() {
        try {
            if (this._scheduleAnalytics.isRunning) {
                return this._scheduleAnalytics.last;
            }
            return this._scheduleAnalytics.perform();
        } catch (e) {
            if (!didCancel(e)) {
                // re-throw the non-cancelation error
                throw e;
            }
        }
    }

    @task
    *_scheduleAnalytics() {
        let statsUrl = this.ghostPaths.url.api(`/emails/${this.post.email.id}/analytics`);
        yield this.ajax.put(statsUrl, {});
        yield this.fetchAnalyticsStatus();
    }

    @action
    cancelScheduleAnalytics() {
        try {
            if (this._cancelScheduleAnalytics.isRunning) {
                return this._cancelScheduleAnalytics.last;
            }
            return this._cancelScheduleAnalytics.perform();
        } catch (e) {
            if (!didCancel(e)) {
                // re-throw the non-cancelation error
                throw e;
            }
        }
    }

    @task
    *_cancelScheduleAnalytics() {
        let statsUrl = this.ghostPaths.url.api(`/emails/analytics`);
        yield this.ajax.delete(statsUrl, {});
        yield this.fetchAnalyticsStatus();
    }
}