TryGhost/Ghost

View on GitHub
ghost/member-attribution/lib/ReferrerTranslator.js

Summary

Maintainability
C
1 day
Test Coverage
/**
 * @typedef {Object} ReferrerData
 * @prop {string|null} [referrerSource]
 * @prop {string|null} [referrerMedium]
 * @prop {string|null} [referrerUrl]
 */

const knownReferrers = require('@tryghost/referrers');

/**
 * Translates referrer info into Source and Medium
 */
class ReferrerTranslator {
    /**
     *
     * @param {Object} deps
     * @param {string} deps.siteUrl
     * @param {string} deps.adminUrl
     */
    constructor({adminUrl, siteUrl}) {
        this.adminUrl = this.getUrlFromStr(adminUrl);
        this.siteUrl = this.getUrlFromStr(siteUrl);
    }

    /**
     * Calculate referrer details from history
     * @param {import('./UrlHistory').UrlHistoryArray} history
     * @returns {ReferrerData|null}
     */
    getReferrerDetails(history) {
        // Empty history will return null as it means script is not loaded
        if (history.length === 0) {
            return {
                referrerSource: null,
                referrerMedium: null,
                referrerUrl: null
            };
        }

        for (const item of history) {
            const referrerUrl = this.getUrlFromStr(item.referrerUrl);

            if (referrerUrl?.hostname === 'checkout.stripe.com') {
                // Ignore stripe, because second try payments should not be attributed to Stripe
                continue;
            }

            const referrerSource = item.referrerSource;
            const referrerMedium = item.referrerMedium;

            // If referrer is Ghost Explore
            if (this.isGhostExploreRef({referrerUrl, referrerSource})) {
                return {
                    referrerSource: 'Ghost Explore',
                    referrerMedium: 'Ghost Network',
                    referrerUrl: referrerUrl?.hostname ?? null
                };
            }

            // If referrer is Ghost.org
            if (this.isGhostOrgUrl(referrerUrl)) {
                return {
                    referrerSource: 'Ghost.org',
                    referrerMedium: 'Ghost Network',
                    referrerUrl: referrerUrl?.hostname
                };
            }

            // If referrer is Ghost Newsletter
            if (this.isGhostNewsletter({referrerSource})) {
                return {
                    referrerSource: referrerSource.replace(/-/g, ' '),
                    referrerMedium: 'Email',
                    referrerUrl: referrerUrl?.hostname ?? null
                };
            }

            // If referrer is from query params
            if (referrerSource) {
                const urlData = referrerUrl ? this.getDataFromUrl(referrerUrl) : null;
                const knownSource = Object.values(knownReferrers).find(referrer => referrer.source.toLowerCase() === referrerSource.toLowerCase());
                return {
                    referrerSource: knownSource?.source || referrerSource,
                    referrerMedium: knownSource?.medium || referrerMedium || urlData?.medium || null,
                    referrerUrl: referrerUrl?.hostname ?? null
                };
            }

            // If referrer is known external URL
            if (referrerUrl && !this.isSiteDomain(referrerUrl)) {
                const urlData = this.getDataFromUrl(referrerUrl);

                // Use known source/medium if available
                if (urlData) {
                    return {
                        referrerSource: urlData?.source ?? null,
                        referrerMedium: urlData?.medium ?? null,
                        referrerUrl: referrerUrl?.hostname ?? null
                    };
                }
                // Use the hostname as a source
                return {
                    referrerSource: referrerUrl?.hostname ?? null,
                    referrerMedium: null,
                    referrerUrl: referrerUrl?.hostname ?? null
                };
            }
        }

        return {
            referrerSource: 'Direct',
            referrerMedium: null,
            referrerUrl: null
        };
    }

    // Fetches referrer data from known external URLs
    getDataFromUrl(url) {
        // Allow matching both "google.ac/products" and "google.ac" as a source
        const urlHostPath = url?.host + url?.pathname;
        const urlDataKey = Object.keys(knownReferrers).sort((a, b) => {
            // The longer key has higher the priority so google.ac/products is selected before google.ac
            return b.length - a.length;
        }).find((source) => {
            return urlHostPath?.startsWith(source);
        });

        return urlDataKey ? knownReferrers[urlDataKey] : null;
    }

    /**
     * @private
     * Return URL object for provided URL string
     * @param {string} url
     * @returns {URL|null}
     */
    getUrlFromStr(url) {
        try {
            return new URL(url);
        } catch (e) {
            return null;
        }
    }

    /**
     * @private
     * Return whether the provided URL is a link to the site
     * @param {URL} url
     * @returns {boolean}
     */
    isSiteDomain(url) {
        try {
            if (this.siteUrl && this.siteUrl?.hostname === url?.hostname) {
                if (url?.pathname?.startsWith(this.siteUrl?.pathname)) {
                    return true;
                }
                return false;
            }
            return false;
        } catch (e) {
            return false;
        }
    }

    /**
     * @private
     * Return whether provided ref is a Ghost newsletter
     * @param {Object} deps
     * @param {string|null} deps.referrerSource
     * @returns {boolean}
     */
    isGhostNewsletter({referrerSource}) {
        // if refferer source ends with -newsletter
        return referrerSource?.endsWith('-newsletter');
    }

    /**
     * @private
     * Return whether provided ref is a Ghost.org URL
     * @param {URL|null} referrerUrl
     * @returns {boolean}
     */
    isGhostOrgUrl(referrerUrl) {
        return referrerUrl?.hostname === 'ghost.org';
    }

    /**
     * @private
     * Return whether provided ref is Ghost Explore
     * @param {Object} deps
     * @param {URL|null} deps.referrerUrl
     * @param {string|null} deps.referrerSource
     * @returns {boolean}
     */
    isGhostExploreRef({referrerUrl, referrerSource}) {
        if (referrerSource === 'ghost-explore') {
            return true;
        }

        if (referrerUrl?.hostname
            && this.adminUrl?.hostname === referrerUrl?.hostname
            && referrerUrl?.pathname?.startsWith(this.adminUrl?.pathname)
        ) {
            return true;
        }

        if (referrerUrl?.hostname === 'ghost.org' && referrerUrl?.pathname?.startsWith('/explore')) {
            return true;
        }

        return false;
    }
}

module.exports = ReferrerTranslator;