mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-26 20:34:02 +03:00
104f84f252
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
209 lines
6.3 KiB
JavaScript
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;
|