Added click tracking for recommendations helper (#18496)

fixes https://github.com/TryGhost/Product/issues/4001
This commit is contained in:
Simon Backx 2023-10-05 11:37:02 +02:00 committed by GitHub
parent d454f18ad2
commit 1882b7048d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 53 additions and 11 deletions

View File

@ -195,6 +195,7 @@ export default class App extends React.Component {
}); });
} }
} }
this.setupRecommendationButtons();
} catch (e) { } catch (e) {
/* eslint-disable no-console */ /* eslint-disable no-console */
console.error(`[Portal] Failed to initialize:`, e); console.error(`[Portal] Failed to initialize:`, e);
@ -888,6 +889,35 @@ export default class App extends React.Component {
}; };
} }
getRecommendationButtons() {
const customTriggerSelector = '[data-recommendation]';
return document.querySelectorAll(customTriggerSelector) || [];
}
/** Setup click tracking for recommendation buttons */
setupRecommendationButtons() {
// Handler for custom buttons
const clickHandler = (event) => {
// Send beacons for recommendation clicks
const recommendationId = event.currentTarget.dataset.recommendation;
if (recommendationId) {
this.dispatchAction('trackRecommendationClicked', {
recommendationId
// eslint-disable-next-line no-console
}).catch(console.error);
} else {
// eslint-disable-next-line no-console
console.warn('[Portal] Invalid usage of data-recommendation attribute');
}
};
const elements = this.getRecommendationButtons();
for (const element of elements) {
element.addEventListener('click', clickHandler, {passive: true});
}
}
render() { render() {
if (this.state.initStatus === 'success') { if (this.state.initStatus === 'success') {
return ( return (

View File

@ -502,6 +502,18 @@ async function oneClickSubscribe({data: {siteUrl}, state}) {
} }
function trackRecommendationClicked({data: {recommendationId}, api}) { function trackRecommendationClicked({data: {recommendationId}, api}) {
try {
const existing = localStorage.getItem('ghost-recommendations-clicked');
const clicked = existing ? JSON.parse(existing) : [];
if (clicked.includes(recommendationId)) {
// Already tracked
return;
}
clicked.push(recommendationId);
localStorage.setItem('ghost-recommendations-clicked', JSON.stringify(clicked));
} catch (e) {
// Ignore localstorage errors (browser not supported or in private mode)
}
api.recommendations.trackClicked({ api.recommendations.trackClicked({
recommendationId recommendationId
}); });

View File

@ -2,7 +2,7 @@
<ul class="recommendations"> <ul class="recommendations">
{{#each recommendations as |rec|}} {{#each recommendations as |rec|}}
<li class="recommendation"> <li class="recommendation">
<a href="{{rec.url}}"> <a href="{{rec.url}}" data-recommendation="{{rec.id}}" target="_blank" rel="noopener">
<img class="recommendation-favicon" src="{{rec.favicon}}" alt="{{rec.title}}"> <img class="recommendation-favicon" src="{{rec.favicon}}" alt="{{rec.title}}">
<h5 class="recommendation-title">{{rec.title}}</h5> <h5 class="recommendation-title">{{rec.title}}</h5>
<p class="recommendation-reason">{{rec.reason}}</p> <p class="recommendation-reason">{{rec.reason}}</p>

View File

@ -41,11 +41,11 @@ describe('{{#recommendations}} helper', function () {
sinon.stub(api, 'recommendationsPublic').get(() => { sinon.stub(api, 'recommendationsPublic').get(() => {
return { return {
browse: sinon.stub().resolves({recommendations: [ browse: sinon.stub().resolves({recommendations: [
{title: 'Recommendation 1', url: 'https://recommendations1.com', favicon: 'https://recommendations1.com/favicon.ico', reason: 'Reason 1'}, {id: '1', title: 'Recommendation 1', url: 'https://recommendations1.com', favicon: 'https://recommendations1.com/favicon.ico', reason: 'Reason 1'},
{title: 'Recommendation 2', url: 'https://recommendations2.com', favicon: 'https://recommendations2.com/favicon.ico', reason: 'Reason 2'}, {id: '2', title: 'Recommendation 2', url: 'https://recommendations2.com', favicon: 'https://recommendations2.com/favicon.ico', reason: 'Reason 2'},
{title: 'Recommendation 3', url: 'https://recommendations3.com', favicon: 'https://recommendations3.com/favicon.ico', reason: 'Reason 3'}, {id: '3', title: 'Recommendation 3', url: 'https://recommendations3.com', favicon: 'https://recommendations3.com/favicon.ico', reason: 'Reason 3'},
{title: 'Recommendation 4', url: 'https://recommendations4.com', favicon: 'https://recommendations4.com/favicon.ico', reason: 'Reason 4'}, {id: '4', title: 'Recommendation 4', url: 'https://recommendations4.com', favicon: 'https://recommendations4.com/favicon.ico', reason: 'Reason 4'},
{title: 'Recommendation 5', url: 'https://recommendations5.com', favicon: 'https://recommendations5.com/favicon.ico', reason: 'Reason 5'} {id: '5', title: 'Recommendation 5', url: 'https://recommendations5.com', favicon: 'https://recommendations5.com/favicon.ico', reason: 'Reason 5'}
], meta: meta}) ], meta: meta})
}; };
}); });
@ -71,35 +71,35 @@ describe('{{#recommendations}} helper', function () {
const expected = trimSpaces(html` const expected = trimSpaces(html`
<ul class="recommendations"> <ul class="recommendations">
<li class="recommendation"> <li class="recommendation">
<a href="https://recommendations1.com"> <a href="https://recommendations1.com" data-recommendation="1" target="_blank" rel="noopener">
<img class="recommendation-favicon" src="https://recommendations1.com/favicon.ico" alt="Recommendation 1"> <img class="recommendation-favicon" src="https://recommendations1.com/favicon.ico" alt="Recommendation 1">
<h5 class="recommendation-title">Recommendation 1</h5> <h5 class="recommendation-title">Recommendation 1</h5>
<p class="recommendation-reason">Reason 1</p> <p class="recommendation-reason">Reason 1</p>
</a> </a>
</li> </li>
<li class="recommendation"> <li class="recommendation">
<a href="https://recommendations2.com"> <a href="https://recommendations2.com" data-recommendation="2" target="_blank" rel="noopener">
<img class="recommendation-favicon" src="https://recommendations2.com/favicon.ico" alt="Recommendation 2"> <img class="recommendation-favicon" src="https://recommendations2.com/favicon.ico" alt="Recommendation 2">
<h5 class="recommendation-title">Recommendation 2</h5> <h5 class="recommendation-title">Recommendation 2</h5>
<p class="recommendation-reason">Reason 2</p> <p class="recommendation-reason">Reason 2</p>
</a> </a>
</li> </li>
<li class="recommendation"> <li class="recommendation">
<a href="https://recommendations3.com"> <a href="https://recommendations3.com" data-recommendation="3" target="_blank" rel="noopener">
<img class="recommendation-favicon" src="https://recommendations3.com/favicon.ico" alt="Recommendation 3"> <img class="recommendation-favicon" src="https://recommendations3.com/favicon.ico" alt="Recommendation 3">
<h5 class="recommendation-title">Recommendation 3</h5> <h5 class="recommendation-title">Recommendation 3</h5>
<p class="recommendation-reason">Reason 3</p> <p class="recommendation-reason">Reason 3</p>
</a> </a>
</li> </li>
<li class="recommendation"> <li class="recommendation">
<a href="https://recommendations4.com"> <a href="https://recommendations4.com" data-recommendation="4" target="_blank" rel="noopener">
<img class="recommendation-favicon" src="https://recommendations4.com/favicon.ico" alt="Recommendation 4"> <img class="recommendation-favicon" src="https://recommendations4.com/favicon.ico" alt="Recommendation 4">
<h5 class="recommendation-title">Recommendation 4</h5> <h5 class="recommendation-title">Recommendation 4</h5>
<p class="recommendation-reason">Reason 4</p> <p class="recommendation-reason">Reason 4</p>
</a> </a>
</li> </li>
<li class="recommendation"> <li class="recommendation">
<a href="https://recommendations5.com"> <a href="https://recommendations5.com" data-recommendation="5" target="_blank" rel="noopener">
<img class="recommendation-favicon" src="https://recommendations5.com/favicon.ico" alt="Recommendation 5"> <img class="recommendation-favicon" src="https://recommendations5.com/favicon.ico" alt="Recommendation 5">
<h5 class="recommendation-title">Recommendation 5</h5> <h5 class="recommendation-title">Recommendation 5</h5>
<p class="recommendation-reason">Reason 5</p> <p class="recommendation-reason">Reason 5</p>