const assert = require('assert/strict');
const fetch = require('node-fetch').default;
const {agentProvider, mockManager, fixtureManager} = require('../utils/e2e-framework');
const urlUtils = require('../../core/shared/url-utils');
const jobService = require('../../core/server/services/jobs/job-service');
describe('Click Tracking', function () {
let agent;
before(async function () {
const {adminAgent} = await agentProvider.getAgentsWithFrontend();
agent = adminAgent;
await fixtureManager.init('newsletters', 'members:newsletters');
await agent.loginAsOwner();
});
beforeEach(function () {
mockManager.mockMail();
mockManager.mockMailgun();
});
afterEach(function () {
mockManager.restore();
});
it('Full test', async function () {
const siteUrl = new URL(urlUtils.urlFor('home', true));
const {body: {posts: [draft]}} = await agent.post('/posts/?source=html', {
body: {
posts: [{
title: 'My Newsletter',
html: `
External link https://example.com/a; Internal link ${siteUrl.href}/about;Ghost homepage https://ghost.org
`
}]
}
});
const newsletterSlug = fixtureManager.get('newsletters', 0).slug;
const {body: {posts: [post]}} = await agent.put(
`/posts/${draft.id}/?newsletter=${newsletterSlug}`,
{
body: {
posts: [{
updated_at: draft.updated_at,
status: 'published'
}]
}
}
);
// Wait for the newsletter to be sent
await jobService.allSettled();
const {body: {links}} = await agent.get(
`/links/?filter=${encodeURIComponent(`post_id:'${post.id}'`)}`
);
/** @type {(url: string) => Promise} */
const fetchWithoutFollowingRedirect = url => fetch(url, {redirect: 'manual'});
let internalRedirectHappened = false;
let externalRedirectHappened = false;
let poweredByGhostIgnored = true;
for (const link of links) {
const res = await fetchWithoutFollowingRedirect(link.link.from);
const redirectedToUrl = new URL(res.headers.get('location'));
// startsWith is a little dirty, but we need this because siteUrl
// can have a path when Ghost is hosted on a subdomain.
const isInternal = redirectedToUrl.href.startsWith(siteUrl.href);
if (isInternal) {
internalRedirectHappened = true;
assert(redirectedToUrl.searchParams.get('attribution_id'), 'attribution_id should be present on internal redirects');
assert(redirectedToUrl.searchParams.get('attribution_type'), 'attribution_type should be present on internal redirects');
} else {
externalRedirectHappened = true;
assert(!redirectedToUrl.searchParams.get('attribution_id'), 'attribution_id should not be present on internal redirects');
assert(!redirectedToUrl.searchParams.get('attribution_type'), 'attribution_type should not be present on internal redirects');
}
assert(redirectedToUrl.searchParams.get('ref'), 'ref should be present on all redirects');
// Powered by Ghost link should not be replaced / tracked
if (link.link.to.includes('https://ghost.org/?via=pbg-newsletter')) {
poweredByGhostIgnored = false;
}
}
assert(internalRedirectHappened);
assert(externalRedirectHappened);
assert(poweredByGhostIgnored);
const {body: {members}} = await agent.get(
`/members/`
);
const linkToClick = links[0];
const memberToClickLink = members[0];
const urlOfLinkToClick = new URL(linkToClick.link.from);
urlOfLinkToClick.searchParams.set('m', memberToClickLink.uuid);
const previousClickCount = linkToClick.count.clicks;
await fetchWithoutFollowingRedirect(urlOfLinkToClick.href);
const {body: {links: [clickedLink]}} = await agent.get(
`/links/?filter=${encodeURIComponent(`post_id:'${post.id}'`)}`
);
const clickCount = clickedLink.count.clicks;
const {body: {events: clickEvents}} = await agent.get(
`/members/events/?filter=${encodeURIComponent(`data.member_id:'${memberToClickLink.id}'+type:click_event`)}`
);
const clickEvent = clickEvents.find((/** @type any */ event) => {
return event.data.post.id === post.id && event.data.link.from === urlOfLinkToClick.pathname;
});
assert(clickEvent);
assert(previousClickCount + 1 === clickCount);
});
});