Updated attribution service to handle referrer information
refs TryGhost/Team#1907 - calculates final attribution source and medium using captured referrer information in history - adds new referrer-translator that goes through available history and based to determine most valid referrer info - includes referrer url, source and medium in the attribution data for storage
This commit is contained in:
parent
2807a35cb0
commit
c765c3230e
@ -9,20 +9,27 @@ class MemberAttributionServiceWrapper {
|
||||
}
|
||||
|
||||
// Wire up all the dependencies
|
||||
const {MemberAttributionService, UrlTranslator, AttributionBuilder} = require('@tryghost/member-attribution');
|
||||
const {
|
||||
MemberAttributionService, UrlTranslator, ReferrerTranslator, AttributionBuilder
|
||||
} = require('@tryghost/member-attribution');
|
||||
const models = require('../../models');
|
||||
|
||||
const urlTranslator = new UrlTranslator({
|
||||
urlService,
|
||||
urlService,
|
||||
urlUtils,
|
||||
models: {
|
||||
Post: models.Post,
|
||||
User: models.User,
|
||||
Post: models.Post,
|
||||
User: models.User,
|
||||
Tag: models.Tag
|
||||
}
|
||||
});
|
||||
|
||||
this.attributionBuilder = new AttributionBuilder({urlTranslator});
|
||||
const referrerTranslator = new ReferrerTranslator({
|
||||
siteUrl: urlUtils.urlFor('home', true),
|
||||
adminUrl: urlUtils.urlFor('admin', true)
|
||||
});
|
||||
|
||||
this.attributionBuilder = new AttributionBuilder({urlTranslator, referrerTranslator});
|
||||
|
||||
// Expose the service
|
||||
this.service = new MemberAttributionService({
|
||||
|
@ -375,4 +375,30 @@ describe('Member Attribution Service', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that getAttribution correctly resolves all model types that are supported
|
||||
*/
|
||||
describe('getAttribution for referrer', function () {
|
||||
it('resolves urls', async function () {
|
||||
const attribution = await memberAttributionService.service.getAttribution([
|
||||
{
|
||||
id: null,
|
||||
path: '/',
|
||||
time: Date.now(),
|
||||
refSource: 'ghost-explore',
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
}
|
||||
]);
|
||||
attribution.should.match(({
|
||||
id: null,
|
||||
url: '/',
|
||||
type: 'url',
|
||||
refSource: 'Ghost Explore',
|
||||
refMedium: 'Ghost Network',
|
||||
refUrl: null
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
module.exports = {
|
||||
MemberAttributionService: require('./lib/service'),
|
||||
AttributionBuilder: require('./lib/attribution'),
|
||||
UrlTranslator: require('./lib/url-translator')
|
||||
UrlTranslator: require('./lib/url-translator'),
|
||||
ReferrerTranslator: require('./lib/referrer-translator')
|
||||
};
|
||||
|
@ -15,11 +15,19 @@ class Attribution {
|
||||
* @param {string|null} [data.id]
|
||||
* @param {string|null} [data.url] Relative to subdirectory
|
||||
* @param {'page'|'post'|'author'|'tag'|'url'} [data.type]
|
||||
* @param {string|null} [data.refSource]
|
||||
* @param {string|null} [data.refMedium]
|
||||
* @param {URL|null} [data.refUrl]
|
||||
*/
|
||||
constructor({id, url, type}, {urlTranslator}) {
|
||||
constructor({
|
||||
id, url, type, refSource, refMedium, refUrl
|
||||
}, {urlTranslator}) {
|
||||
this.id = id;
|
||||
this.url = url;
|
||||
this.type = type;
|
||||
this.refSource = refSource;
|
||||
this.refMedium = refMedium;
|
||||
this.refUrl = refUrl;
|
||||
|
||||
/**
|
||||
* @private
|
||||
@ -80,18 +88,22 @@ class AttributionBuilder {
|
||||
|
||||
/**
|
||||
*/
|
||||
constructor({urlTranslator}) {
|
||||
constructor({urlTranslator, referrerTranslator}) {
|
||||
this.urlTranslator = urlTranslator;
|
||||
this.referrerTranslator = referrerTranslator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Attribution object with the dependencies injected
|
||||
*/
|
||||
build({id, url, type}) {
|
||||
build({id, url, type, refSource, refMedium, refUrl}) {
|
||||
return new Attribution({
|
||||
id,
|
||||
url,
|
||||
type
|
||||
type,
|
||||
refSource,
|
||||
refMedium,
|
||||
refUrl
|
||||
}, {urlTranslator: this.urlTranslator});
|
||||
}
|
||||
|
||||
@ -105,19 +117,31 @@ class AttributionBuilder {
|
||||
return this.build({
|
||||
id: null,
|
||||
url: null,
|
||||
type: null
|
||||
type: null,
|
||||
refSource: null,
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
});
|
||||
}
|
||||
|
||||
// Note: history iterator is ordered from recent to oldest!
|
||||
const referrerData = this.referrerTranslator.getReferrerDetails(history) || {
|
||||
refSource: null,
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
};
|
||||
|
||||
// Start at the end. Return the first post we find
|
||||
const resources = [];
|
||||
|
||||
// Note: history iterator is ordered from recent to oldest!
|
||||
for (const item of history) {
|
||||
const resource = await this.urlTranslator.getResourceDetails(item);
|
||||
|
||||
if (resource && resource.type === 'post') {
|
||||
return this.build(resource);
|
||||
return this.build({
|
||||
...resource,
|
||||
...referrerData
|
||||
});
|
||||
}
|
||||
|
||||
// Store to avoid that we need to look it up again
|
||||
@ -130,18 +154,25 @@ class AttributionBuilder {
|
||||
// Return first with an id (page, tag, author)
|
||||
for (const resource of resources) {
|
||||
if (resource.id) {
|
||||
return this.build(resource);
|
||||
return this.build({
|
||||
...resource,
|
||||
...referrerData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// No post/page/tag/author found?
|
||||
// Return the last path that was visited
|
||||
if (resources.length > 0) {
|
||||
return this.build(resources[0]);
|
||||
return this.build({
|
||||
...referrerData,
|
||||
...resources[0]
|
||||
});
|
||||
}
|
||||
|
||||
// We only have history items without a path that have invalid ids
|
||||
return this.build({
|
||||
...referrerData,
|
||||
id: null,
|
||||
url: null,
|
||||
type: null
|
||||
|
@ -3,6 +3,9 @@
|
||||
* @prop {string} [path]
|
||||
* @prop {string} [id]
|
||||
* @prop {string} [type]
|
||||
* @prop {string} [refSource]
|
||||
* @prop {string} [refMedium]
|
||||
* @prop {string} [refUrl]
|
||||
* @prop {number} time
|
||||
*/
|
||||
|
||||
|
183
ghost/member-attribution/lib/referrer-translator.js
Normal file
183
ghost/member-attribution/lib/referrer-translator.js
Normal file
@ -0,0 +1,183 @@
|
||||
/**
|
||||
* @typedef {Object} ReferrerData
|
||||
* @prop {string|null} [refSource]
|
||||
* @prop {string|null} [refMedium]
|
||||
* @prop {URL|null} [refUrl]
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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('./history').UrlHistoryArray} history
|
||||
* @returns {ReferrerData|null}
|
||||
*/
|
||||
getReferrerDetails(history) {
|
||||
if (history.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const item of history) {
|
||||
const refUrl = this.getUrlFromStr(item.refUrl);
|
||||
const refSource = item.refSource;
|
||||
const refMedium = item.refMedium;
|
||||
|
||||
// If referrer is Ghost Explore
|
||||
if (this.isGhostExploreRef({refUrl, refSource})) {
|
||||
return {
|
||||
refSource: 'Ghost Explore',
|
||||
refMedium: 'Ghost Network',
|
||||
refUrl: refUrl
|
||||
};
|
||||
}
|
||||
|
||||
// If referrer is Ghost.org
|
||||
if (this.isGhostOrgUrl(refUrl)) {
|
||||
return {
|
||||
refSource: 'Ghost.org',
|
||||
refMedium: 'Ghost Network',
|
||||
refUrl: refUrl
|
||||
};
|
||||
}
|
||||
|
||||
// If referrer is Ghost Newsletter
|
||||
if (this.isGhostNewsletter({refSource})) {
|
||||
return {
|
||||
refSource: refSource.replace(/-/g, ' '),
|
||||
refMedium: 'Email',
|
||||
refUrl: refUrl
|
||||
};
|
||||
}
|
||||
|
||||
// If referrer is from query params
|
||||
if (refSource) {
|
||||
const urlData = this.getDataFromUrl() || {};
|
||||
return {
|
||||
refSource: refSource,
|
||||
refMedium: refMedium || urlData?.medium || null,
|
||||
refUrl: refUrl
|
||||
};
|
||||
}
|
||||
|
||||
// If referrer is known external URL
|
||||
// TODO: Use list of known external urls to fetch source/medium
|
||||
if (refUrl && !this.isSiteDomain(refUrl)) {
|
||||
const urlData = this.getDataFromUrl();
|
||||
|
||||
if (urlData) {
|
||||
return {
|
||||
refSource: urlData?.source,
|
||||
refMedium: urlData?.medium,
|
||||
refUrl: refUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fetches referrer data from known external URLs
|
||||
//TODO: Use list of known external urls to fetch source/medium
|
||||
getDataFromUrl() {
|
||||
return 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.refSource
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isGhostNewsletter({refSource}) {
|
||||
// if refferer source ends with -newsletter
|
||||
return refSource?.endsWith('-newsletter');
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Return whether provided ref is a Ghost.org URL
|
||||
* @param {URL|null} refUrl
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isGhostOrgUrl(refUrl) {
|
||||
return refUrl?.hostname === 'ghost.org';
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Return whether provided ref is Ghost Explore
|
||||
* @param {Object} deps
|
||||
* @param {URL|null} deps.refUrl
|
||||
* @param {string|null} deps.refSource
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isGhostExploreRef({refUrl, refSource}) {
|
||||
if (refSource === 'ghost-explore') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (refUrl?.hostname
|
||||
&& this.adminUrl?.hostname === refUrl?.hostname
|
||||
&& refUrl?.pathname?.startsWith(this.adminUrl?.pathname)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (refUrl?.hostname === 'ghost.org' && refUrl?.pathname?.startsWith('/explore')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ReferrerTranslator;
|
@ -6,80 +6,94 @@ const AttributionBuilder = require('../lib/attribution');
|
||||
|
||||
describe('AttributionBuilder', function () {
|
||||
let attributionBuilder;
|
||||
let urlTranslator;
|
||||
let now;
|
||||
|
||||
before(function () {
|
||||
now = Date.now();
|
||||
attributionBuilder = new AttributionBuilder({
|
||||
urlTranslator: {
|
||||
getResourceDetails(item) {
|
||||
if (!item.path) {
|
||||
if (item.id === 'invalid') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
url: `/${item.type}/${item.id}`
|
||||
};
|
||||
}
|
||||
|
||||
const path = this.stripSubdirectoryFromPath(item.path);
|
||||
|
||||
if (path === '/my-post') {
|
||||
return {
|
||||
id: 123,
|
||||
type: 'post',
|
||||
url: path
|
||||
};
|
||||
}
|
||||
if (path === '/my-page') {
|
||||
return {
|
||||
id: 845,
|
||||
type: 'page',
|
||||
url: path
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: null,
|
||||
type: 'url',
|
||||
url: path
|
||||
};
|
||||
},
|
||||
getResourceById(id, type) {
|
||||
if (id === 'invalid') {
|
||||
urlTranslator = {
|
||||
getResourceDetails(item) {
|
||||
if (!item.path) {
|
||||
if (item.id === 'invalid') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
get(prop) {
|
||||
if (prop === 'title' && type === 'author') {
|
||||
// Simulate an author doesn't have a title
|
||||
return undefined;
|
||||
}
|
||||
if (id === 'no-props') {
|
||||
// Simulate a model without properties for branch coverage
|
||||
return undefined;
|
||||
}
|
||||
return prop;
|
||||
}
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
url: `/${item.type}/${item.id}`
|
||||
};
|
||||
},
|
||||
getUrlTitle(url) {
|
||||
return url;
|
||||
},
|
||||
getUrlByResourceId() {
|
||||
return 'https://absolute/dir/path';
|
||||
},
|
||||
relativeToAbsolute(path) {
|
||||
return 'https://absolute/dir' + path;
|
||||
},
|
||||
stripSubdirectoryFromPath(path) {
|
||||
if (path.startsWith('/dir/')) {
|
||||
return path.substring('/dir/'.length - 1);
|
||||
}
|
||||
|
||||
const path = this.stripSubdirectoryFromPath(item.path);
|
||||
|
||||
if (path === '/my-post') {
|
||||
return {
|
||||
id: 123,
|
||||
type: 'post',
|
||||
url: path
|
||||
};
|
||||
}
|
||||
if (path === '/my-page') {
|
||||
return {
|
||||
id: 845,
|
||||
type: 'page',
|
||||
url: path
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: null,
|
||||
type: 'url',
|
||||
url: path
|
||||
};
|
||||
},
|
||||
getResourceById(id, type) {
|
||||
if (id === 'invalid') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id,
|
||||
get(prop) {
|
||||
if (prop === 'title' && type === 'author') {
|
||||
// Simulate an author doesn't have a title
|
||||
return undefined;
|
||||
}
|
||||
if (id === 'no-props') {
|
||||
// Simulate a model without properties for branch coverage
|
||||
return undefined;
|
||||
}
|
||||
return prop;
|
||||
}
|
||||
return path;
|
||||
};
|
||||
},
|
||||
getUrlTitle(url) {
|
||||
return url;
|
||||
},
|
||||
getUrlByResourceId() {
|
||||
return 'https://absolute/dir/path';
|
||||
},
|
||||
relativeToAbsolute(path) {
|
||||
return 'https://absolute/dir' + path;
|
||||
},
|
||||
stripSubdirectoryFromPath(path) {
|
||||
if (path.startsWith('/dir/')) {
|
||||
return path.substring('/dir/'.length - 1);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
};
|
||||
attributionBuilder = new AttributionBuilder({
|
||||
urlTranslator,
|
||||
referrerTranslator: {
|
||||
getReferrerDetails(history) {
|
||||
if (history) {
|
||||
return {
|
||||
refSource: 'Ghost Explore',
|
||||
refMedium: 'Ghost Network',
|
||||
refUrl: 'https://ghost.org/explore'
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -87,7 +101,7 @@ describe('AttributionBuilder', function () {
|
||||
|
||||
it('Returns empty if empty history', async function () {
|
||||
const history = UrlHistory.create([]);
|
||||
should(await attributionBuilder.getAttribution(history)).match({id: null, type: null, url: null});
|
||||
should(await attributionBuilder.getAttribution(history)).match({id: null, type: null, url: null, refSource: null, refMedium: null, refUrl: null});
|
||||
});
|
||||
|
||||
it('Returns last url', async function () {
|
||||
@ -131,6 +145,40 @@ describe('AttributionBuilder', function () {
|
||||
should(await attributionBuilder.getAttribution(history)).match({type: 'post', id: '123', url: '/post/123'});
|
||||
});
|
||||
|
||||
it('Returns referrer attribution', async function () {
|
||||
const history = UrlHistory.create([
|
||||
{path: '/dir/other', time: now + 123},
|
||||
{id: '123', type: 'post', time: now + 124},
|
||||
{path: '/dir/unknown-page', time: now + 125}
|
||||
]);
|
||||
should(await attributionBuilder.getAttribution(history)).match({
|
||||
refSource: 'Ghost Explore',
|
||||
refMedium: 'Ghost Network',
|
||||
refUrl: 'https://ghost.org/explore'
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns null referrer attribution', async function () {
|
||||
attributionBuilder = new AttributionBuilder({
|
||||
urlTranslator,
|
||||
referrerTranslator: {
|
||||
getReferrerDetails() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
const history = UrlHistory.create([
|
||||
{path: '/dir/other', time: now + 123},
|
||||
{id: '123', type: 'post', time: now + 124},
|
||||
{path: '/dir/unknown-page', time: now + 125}
|
||||
]);
|
||||
should(await attributionBuilder.getAttribution(history)).match({
|
||||
refSource: null,
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns all null if only invalid ids', async function () {
|
||||
const history = UrlHistory.create([
|
||||
{id: 'invalid', type: 'post', time: now + 124},
|
||||
|
@ -77,6 +77,14 @@ describe('UrlHistory', function () {
|
||||
time: Date.now(),
|
||||
type: 'post',
|
||||
id: '123'
|
||||
}],
|
||||
[{
|
||||
time: Date.now(),
|
||||
type: 'post',
|
||||
id: '123',
|
||||
refSource: 'ghost-explore',
|
||||
refMedium: null,
|
||||
refUrl: 'https://ghost.org'
|
||||
}]
|
||||
];
|
||||
for (const input of inputs) {
|
||||
|
180
ghost/member-attribution/test/referrer-translator.test.js
Normal file
180
ghost/member-attribution/test/referrer-translator.test.js
Normal file
@ -0,0 +1,180 @@
|
||||
// Switch these lines once there are useful utils
|
||||
// const testUtils = require('./utils');
|
||||
require('./utils');
|
||||
const ReferrerTranslator = require('../lib/referrer-translator');
|
||||
|
||||
describe('ReferrerTranslator', function () {
|
||||
describe('Constructor', function () {
|
||||
it('doesn\'t throw', function () {
|
||||
new ReferrerTranslator({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getReferrerDetails', function () {
|
||||
let translator;
|
||||
before(function () {
|
||||
translator = new ReferrerTranslator({
|
||||
siteUrl: 'https://example.com',
|
||||
adminUrl: 'https://admin.example.com/ghost'
|
||||
});
|
||||
});
|
||||
|
||||
it('returns ghost explore from source ref for valid history', async function () {
|
||||
should(translator.getReferrerDetails([
|
||||
{
|
||||
refSource: 'ghost-explore',
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
},
|
||||
{
|
||||
refSource: 'ghost-newsletter',
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
},
|
||||
{
|
||||
refSource: 'ghost-newsletter',
|
||||
refMedium: null,
|
||||
refUrl: 'https://t.co'
|
||||
}
|
||||
])).eql({
|
||||
refSource: 'Ghost Explore',
|
||||
refMedium: 'Ghost Network',
|
||||
refUrl: null
|
||||
});
|
||||
});
|
||||
|
||||
it('returns ghost explore from url for valid history', async function () {
|
||||
should(translator.getReferrerDetails([
|
||||
{
|
||||
refSource: null,
|
||||
refMedium: null,
|
||||
refUrl: 'https://ghost.org/explore'
|
||||
},
|
||||
{
|
||||
refSource: 'ghost-newsletter',
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
},
|
||||
{
|
||||
refSource: 'ghost-newsletter',
|
||||
refMedium: null,
|
||||
refUrl: 'https://t.co'
|
||||
}
|
||||
])).eql({
|
||||
refSource: 'Ghost Explore',
|
||||
refMedium: 'Ghost Network',
|
||||
refUrl: new URL('https://ghost.org/explore')
|
||||
});
|
||||
});
|
||||
|
||||
it('returns ghost explore from admin url for valid history', async function () {
|
||||
should(translator.getReferrerDetails([
|
||||
{
|
||||
refSource: null,
|
||||
refMedium: null,
|
||||
refUrl: 'https://admin.example.com/ghost/#/dashboard'
|
||||
},
|
||||
{
|
||||
refSource: 'ghost-newsletter',
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
},
|
||||
{
|
||||
refSource: 'ghost-newsletter',
|
||||
refMedium: null,
|
||||
refUrl: 'https://t.co'
|
||||
}
|
||||
])).eql({
|
||||
refSource: 'Ghost Explore',
|
||||
refMedium: 'Ghost Network',
|
||||
refUrl: new URL('https://admin.example.com/ghost/#/dashboard')
|
||||
});
|
||||
});
|
||||
|
||||
it('returns ghost newsletter ref for valid history', async function () {
|
||||
should(translator.getReferrerDetails([
|
||||
{
|
||||
refSource: 'publisher-weekly-newsletter',
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
},
|
||||
{
|
||||
refSource: 'ghost-explore',
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
},
|
||||
{
|
||||
refSource: 'ghost-newsletter',
|
||||
refMedium: null,
|
||||
refUrl: 'https://t.co'
|
||||
}
|
||||
])).eql({
|
||||
refSource: 'publisher weekly newsletter',
|
||||
refMedium: 'Email',
|
||||
refUrl: null
|
||||
});
|
||||
});
|
||||
|
||||
it('returns ghost.org ref for valid history', async function () {
|
||||
should(translator.getReferrerDetails([
|
||||
{
|
||||
refSource: null,
|
||||
refMedium: null,
|
||||
refUrl: 'https://ghost.org/creators/'
|
||||
},
|
||||
{
|
||||
refSource: 'publisher-weekly-newsletter',
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
},
|
||||
{
|
||||
refSource: 'ghost-explore',
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
}
|
||||
])).eql({
|
||||
refSource: 'Ghost.org',
|
||||
refMedium: 'Ghost Network',
|
||||
refUrl: new URL('https://ghost.org/creators/')
|
||||
});
|
||||
});
|
||||
|
||||
it('returns ref source for valid history', async function () {
|
||||
should(translator.getReferrerDetails([
|
||||
{
|
||||
refSource: 'twitter',
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
},
|
||||
{
|
||||
refSource: 'publisher-weekly-newsletter',
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
},
|
||||
{
|
||||
refSource: 'ghost-explore',
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
}
|
||||
])).eql({
|
||||
refSource: 'twitter',
|
||||
refMedium: null,
|
||||
refUrl: null
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null for empty history', async function () {
|
||||
should(translator.getReferrerDetails([])).eql(null);
|
||||
});
|
||||
|
||||
it('returns null for history with only site url', async function () {
|
||||
should(translator.getReferrerDetails([
|
||||
{
|
||||
refSource: null,
|
||||
refMedium: null,
|
||||
refUrl: 'https://example.com'
|
||||
}
|
||||
])).eql(null);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user