Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const urlAttribution = require('../utils/url-attribution');
const parseReferrerData = urlAttribution.parseReferrerData;
const getReferrer = urlAttribution.getReferrer;
const getGiftReferrer = urlAttribution.getGiftReferrer;

// Location where we want to store the history in sessionStorage
const STORAGE_KEY = 'ghost-history';
Expand Down Expand Up @@ -99,7 +100,16 @@ const LIMIT = 15;
utmTerm: referrerData.utmTerm,
utmContent: referrerData.utmContent
};


// Tag gift-link visits so gift-derived signups are attributed to the gift
// rather than the channel the link was shared through. The real referrer
// url is kept; only the source/medium are overridden.
const giftReferrer = getGiftReferrer(window.location.href, referrerData);
if (giftReferrer) {
attributionData.referrerSource = giftReferrer.source;
attributionData.referrerMedium = giftReferrer.medium;
}

// Use the getReferrer helper to handle same-domain referrer filtering
// This will return null if the referrer is from the same domain
let referrerUrl;
Expand Down
32 changes: 31 additions & 1 deletion ghost/core/core/frontend/src/utils/url-attribution.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,41 @@ function selectPrimaryReferrer(referrerData) {

/**
* One-step function to get the final referrer from a URL
*
*
* @param {string} [url] - URL to parse (defaults to current URL)
* @returns {string|null} Final referrer value
*/
export function getReferrer(url) {
const referrerData = parseReferrerData(url);
return selectPrimaryReferrer(referrerData);
}

/**
* Source/medium to attribute a gift-link visit (`?gift=token`) to, so that
* gift-derived signups appear in member attribution instead of being credited
* to whatever channel the link happened to be shared through (usually Direct).
*
* Returns null when the URL isn't a gift link, or when it already carries an
* explicit ref/source/utm_source the site owner set — that wins over the gift.
* Note: an invalid gift token is stripped server-side via a 301 before the page
* renders, so a `?gift` param present at render time is always a valid gift.
*
* @param {string} url - The URL to inspect (the current page URL)
* @param {AttributionData} referrerData - Already-parsed data for the same URL
* @returns {{source: string, medium: string}|null}
*/
export function getGiftReferrer(url, referrerData) {
if (referrerData && referrerData.source) {
return null;
}

try {
if (new URL(url).searchParams.has('gift')) {
return {source: 'Gift', medium: 'gift'};
}
} catch (e) {
// Malformed URL - not a gift link
}

return null;
}
36 changes: 34 additions & 2 deletions ghost/core/test/unit/frontend/src/url-attribution.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const {JSDOM} = require('jsdom');
// Use path relative to test file
const {
parseReferrerData,
getReferrer
getReferrer,
getGiftReferrer
} = require('../../../../core/frontend/src/utils/url-attribution');

describe('URL Attribution Utils', function () {
Expand Down Expand Up @@ -157,10 +158,41 @@ describe('URL Attribution Utils', function () {
const result = getReferrer('https://example.com/?ref=newsletter');
assert.equal(result, 'newsletter');
});

it('should return null for same-domain referrers', function () {
const result = getReferrer('https://example.com/?ref=https://example.com/page');
assert.equal(result, null);
});
});

describe('getGiftReferrer', function () {
it('returns the Gift source/medium for a gift link', function () {
const url = 'https://example.com/my-post/?gift=token123';
const result = getGiftReferrer(url, parseReferrerData(url));
assert.deepEqual(result, {source: 'Gift', medium: 'gift'});
});

it('returns null when there is no gift param', function () {
const url = 'https://example.com/my-post/';
const result = getGiftReferrer(url, parseReferrerData(url));
assert.equal(result, null);
});

it('defers to an explicit ref/source/utm on the gift link', function () {
const url = 'https://example.com/my-post/?gift=token123&ref=newsletter';
const result = getGiftReferrer(url, parseReferrerData(url));
assert.equal(result, null);
});

it('defers to an explicit utm_source on the gift link', function () {
const url = 'https://example.com/my-post/?gift=token123&utm_source=twitter';
const result = getGiftReferrer(url, parseReferrerData(url));
assert.equal(result, null);
});

it('returns null for a malformed URL', function () {
const result = getGiftReferrer('not a url', {source: null});
assert.equal(result, null);
});
});
});
Loading