Ghost/ghost/member-attribution/lib/ReferrerTranslator.js
Fabien "egg" O'Carroll 104f84f252 Added eslint rule for file naming convention
As discussed with the product team we want to enforce kebab-case file names for
all files, with the exception of files which export a single class, in which
case they should be PascalCase and reflect the class which they export.

This will help find classes faster, and should push better naming for them too.

Some files and packages have been excluded from this linting, specifically when
a library or framework depends on the naming of a file for the functionality
e.g. Ember, knex-migrator, adapter-manager
2023-05-09 12:34:34 -04:00

209 lines
6.3 KiB
JavaScript

/**
* @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);
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;
return {
referrerSource: referrerSource,
referrerMedium: 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;