-
Notifications
You must be signed in to change notification settings - Fork 230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WSTEAM1-1514: Add click tracking at top level #12360
base: latest
Are you sure you want to change the base?
Changes from all commits
67888e2
1e894cf
975419f
8c64c73
7839f99
3dcc093
e95925f
b59c095
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,12 @@ | ||
const sendBeaconLite = (atiPageViewUrlString: string) => ` | ||
var xhr = new XMLHttpRequest(); | ||
xhr.open("GET", "${atiPageViewUrlString}", true); | ||
xhr.withCredentials = true; | ||
xhr.send(); | ||
function sendBeaconLite (atiPageViewUrlString) { | ||
var xhr = new XMLHttpRequest(); | ||
xhr.open("GET", atiPageViewUrlString, true); | ||
xhr.withCredentials = true; | ||
xhr.send(); | ||
} | ||
|
||
sendBeaconLite("${atiPageViewUrlString}"); | ||
`; | ||
|
||
export default sendBeaconLite; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,6 +1,7 @@ | ||||||
/* eslint-disable no-console */ | ||||||
import { useContext, useCallback, useState } from 'react'; | ||||||
|
||||||
import { buildATIEventTrackUrl } from '#app/components/ATIAnalytics/atiUrl'; | ||||||
import { EventTrackingContext } from '../../contexts/EventTrackingContext'; | ||||||
import useTrackingToggle from '../useTrackingToggle'; | ||||||
import OPTIMIZELY_CONFIG from '../../lib/config/optimizely'; | ||||||
|
@@ -9,6 +10,7 @@ import { ServiceContext } from '../../contexts/ServiceContext'; | |||||
import { isValidClick } from './clickTypes'; | ||||||
|
||||||
const EVENT_TYPE = 'click'; | ||||||
export const LITE_TRACKER_PARAM = 'data-ati-tracking'; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: Naming things is 🧑🔬 (ignore this for the spike - perhaps when we productionise it, we can rename)
Suggested change
|
||||||
|
||||||
const useClickTrackerHandler = (props = {}) => { | ||||||
const preventNavigation = props?.preventNavigation; | ||||||
|
@@ -136,4 +138,35 @@ const useClickTrackerHandler = (props = {}) => { | |||||
); | ||||||
}; | ||||||
|
||||||
export const useConstructLiteSiteATIEventTrackUrl = (props = {}) => { | ||||||
const eventTrackingContext = useContext(EventTrackingContext); | ||||||
|
||||||
const componentName = props?.componentName; | ||||||
const url = props?.url; | ||||||
const advertiserID = props?.advertiserID; | ||||||
const format = props?.format; | ||||||
const detailedPlacement = props?.detailedPlacement; | ||||||
Comment on lines
+144
to
+148
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use destructuring here? |
||||||
|
||||||
const { pageIdentifier, platform, producerId, statsDestination } = | ||||||
eventTrackingContext; | ||||||
|
||||||
const campaignID = props?.campaignID || eventTrackingContext?.campaignID; | ||||||
|
||||||
const atiClickTrackingUrl = buildATIEventTrackUrl({ | ||||||
pageIdentifier, | ||||||
producerId, | ||||||
platform, | ||||||
statsDestination, | ||||||
componentName, | ||||||
campaignID, | ||||||
format, | ||||||
type: EVENT_TYPE, | ||||||
advertiserID, | ||||||
url, | ||||||
detailedPlacement, | ||||||
}); | ||||||
|
||||||
return atiClickTrackingUrl; | ||||||
}; | ||||||
|
||||||
export default useClickTrackerHandler; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -205,6 +205,77 @@ exports[`Document Component should render LITE version correctly 1`] = ` | |
<style> | ||
.css-7prgni-StyledLink{display:inline-block;} | ||
</style> | ||
<script> | ||
(function trackingScript() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wasn't expecting this, but was there an existing test which already snapshots the Document component? |
||
window.addEventListener('load', function () { | ||
document.addEventListener('click', function (event) { | ||
var targetElement = event.target; | ||
// eslint-disable-next-line no-undef | ||
if ((targetElement === null || targetElement === void 0 ? void 0 : targetElement.tagName) === 'A') { | ||
event.stopPropagation(); | ||
event.preventDefault(); | ||
var atiURL = targetElement.getAttribute('data-ati-tracking'); | ||
if (atiURL == null) { | ||
return; | ||
} | ||
var currentAnchorElement = event.currentTarget; | ||
var nextPageUrl = currentAnchorElement === null || currentAnchorElement === void 0 ? void 0 : currentAnchorElement.href; | ||
var _window = window, | ||
_window$screen = _window.screen, | ||
width = _window$screen.width, | ||
height = _window$screen.height, | ||
colorDepth = _window$screen.colorDepth, | ||
pixelDepth = _window$screen.pixelDepth, | ||
innerWidth = _window.innerWidth, | ||
innerHeight = _window.innerHeight; | ||
var now = new Date(); | ||
var hours = now.getHours(); | ||
var mins = now.getMinutes(); | ||
var secs = now.getSeconds(); | ||
|
||
// COOKIE SETTINGS | ||
var cookieName = 'atuserid'; | ||
var expires = 397; // expires in 13 months | ||
var cookiesForPage = "; ".concat(document.cookie); | ||
var atUserIdCookie = cookiesForPage.split("; ".concat(cookieName, "=")); | ||
var atUserIdValue = null; | ||
if (atUserIdCookie.length === 2) { | ||
var _atUserIdCookie$pop; | ||
var cookieInfo = (_atUserIdCookie$pop = atUserIdCookie.pop()) === null || _atUserIdCookie$pop === void 0 ? void 0 : _atUserIdCookie$pop.split(';').shift(); | ||
if (cookieInfo) { | ||
var decodedCookie = decodeURI(cookieInfo); | ||
var user = JSON.parse(decodedCookie); | ||
atUserIdValue = user.val; | ||
} | ||
} | ||
if (!atUserIdValue && crypto.randomUUID) { | ||
atUserIdValue = crypto.randomUUID(); | ||
} | ||
var stringifiedCookieValue = JSON.stringify({ | ||
val: atUserIdValue | ||
}); | ||
if (atUserIdValue) { | ||
document.cookie = "".concat(cookieName, "=").concat(stringifiedCookieValue, "; path=/; max-age=").concat(expires, ";"); | ||
} | ||
var rValue = [width || 0, height || 0, colorDepth || 0, pixelDepth || 0].join('x'); | ||
var reValue = [innerWidth || 0, innerHeight || 0].join('x'); | ||
var hlValue = [hours, mins, secs].join('x'); | ||
var clientSideAtiURL = atiURL.concat('&', 'r=', rValue).concat('&', 're=', reValue).concat('&', 'hl=', hlValue); | ||
if (navigator.language) { | ||
clientSideAtiURL = clientSideAtiURL.concat('&', 'lng=', navigator.language); | ||
} | ||
if (atUserIdValue) { | ||
clientSideAtiURL = clientSideAtiURL.concat('&', 'idclient=', atUserIdValue); | ||
} | ||
|
||
// eslint-disable-next-line no-undef -- This is provided in a helmet script | ||
window.sendBeaconLite(clientSideAtiURL); | ||
window.location.assign(nextPageUrl); | ||
} | ||
}); | ||
}); | ||
})() | ||
</script> | ||
</head> | ||
<body> | ||
<div> | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,118 @@ | ||||||||||||||||||||||||||||||||
import trackingScript from '.'; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const dispatchClick = (targetElement: HTMLElement) => { | ||||||||||||||||||||||||||||||||
document.body.appendChild(targetElement); | ||||||||||||||||||||||||||||||||
const event = new MouseEvent('click', { | ||||||||||||||||||||||||||||||||
view: window, | ||||||||||||||||||||||||||||||||
bubbles: true, | ||||||||||||||||||||||||||||||||
cancelable: true, | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
targetElement.dispatchEvent(event); | ||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
describe('Click tracking script', () => { | ||||||||||||||||||||||||||||||||
const randomUUIDMock = jest.fn(); | ||||||||||||||||||||||||||||||||
beforeEach(() => { | ||||||||||||||||||||||||||||||||
jest.clearAllMocks(); | ||||||||||||||||||||||||||||||||
jest.useFakeTimers().setSystemTime(new Date(1731515402000)); | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: Could we use the ISO Date, so that we don't need to calculate what date/time this represents?
Suggested change
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
trackingScript(); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
document.cookie = | ||||||||||||||||||||||||||||||||
Check warning Code scanning / CodeQL Clear text transmission of sensitive cookie Medium
Sensitive cookie sent without enforcing SSL encryption.
Copilot Autofix AI 1 day ago To fix the problem, we need to ensure that the cookie is transmitted using SSL by setting the
Suggested changeset
1
src/server/utilities/liteATIClickTracking/index.test.ts
Copilot is powered by AI and may make mistakes. Always verify output.
Refresh and try again.
|
||||||||||||||||||||||||||||||||
'atuserid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we use a more realistic expiry date here?
Suggested change
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||||||||||||||||||||||||||||||||
// @ts-ignore | ||||||||||||||||||||||||||||||||
Comment on lines
+24
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be replaced with
Suggested change
|
||||||||||||||||||||||||||||||||
delete window.location; | ||||||||||||||||||||||||||||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||||||||||||||||||||||||||||||||
// @ts-ignore | ||||||||||||||||||||||||||||||||
window.location = { | ||||||||||||||||||||||||||||||||
assign: jest.fn(), | ||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
window.sendBeaconLite = jest.fn(); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
Object.defineProperty(global, 'crypto', { | ||||||||||||||||||||||||||||||||
value: { | ||||||||||||||||||||||||||||||||
randomUUID: randomUUIDMock, | ||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
Comment on lines
+35
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not for spike but we should probably move this to the src/testHelpers/jest-setup.js file so that we don't have to keep mocking it in other places |
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
window.dispatchEvent(new Event('load')); | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
it('Sets a new cookie if there is no atuserid cookie on the user browser', () => { | ||||||||||||||||||||||||||||||||
const anchorElement = document.createElement('a'); | ||||||||||||||||||||||||||||||||
anchorElement.setAttribute( | ||||||||||||||||||||||||||||||||
'data-ati-tracking', | ||||||||||||||||||||||||||||||||
'https://logws1363.ati-host.net/?', | ||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
randomUUIDMock.mockReturnValueOnce('randomUniqueId'); | ||||||||||||||||||||||||||||||||
dispatchClick(anchorElement); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
expect(document.cookie).toBe('atuserid={"val":"randomUniqueId"}'); | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
it('Reuses the atuserid cookie if there is a atuserid cookie on the user browser', () => { | ||||||||||||||||||||||||||||||||
const anchorElement = document.createElement('a'); | ||||||||||||||||||||||||||||||||
anchorElement.setAttribute( | ||||||||||||||||||||||||||||||||
'data-ati-tracking', | ||||||||||||||||||||||||||||||||
'https://logws1363.ati-host.net/?', | ||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
document.cookie = 'atuserid={"val":"oldCookieId"}'; | ||||||||||||||||||||||||||||||||
Check warning Code scanning / CodeQL Clear text transmission of sensitive cookie Medium
Sensitive cookie sent without enforcing SSL encryption.
Copilot Autofix AI 1 day ago To fix the problem, we need to ensure that the cookie is transmitted using SSL by setting the
Suggested changeset
1
src/server/utilities/liteATIClickTracking/index.test.ts
Copilot is powered by AI and may make mistakes. Always verify output.
Refresh and try again.
|
||||||||||||||||||||||||||||||||
randomUUIDMock.mockReturnValueOnce('newCookieId'); | ||||||||||||||||||||||||||||||||
dispatchClick(anchorElement); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const callParam = (window.sendBeaconLite as jest.Mock).mock.calls[0][0]; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
expect(callParam).toContain('idclient=oldCookieId'); | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
it('Calls sendBeaconLite() with the correct url', () => { | ||||||||||||||||||||||||||||||||
const anchorElement = document.createElement('a'); | ||||||||||||||||||||||||||||||||
anchorElement.setAttribute( | ||||||||||||||||||||||||||||||||
'data-ati-tracking', | ||||||||||||||||||||||||||||||||
'https://logws1363.ati-host.net/?', | ||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
window.screen = { | ||||||||||||||||||||||||||||||||
width: 100, | ||||||||||||||||||||||||||||||||
height: 400, | ||||||||||||||||||||||||||||||||
colorDepth: 24, | ||||||||||||||||||||||||||||||||
pixelDepth: 24, | ||||||||||||||||||||||||||||||||
availWidth: 400, | ||||||||||||||||||||||||||||||||
availHeight: 100, | ||||||||||||||||||||||||||||||||
orientation: 'landscape' as unknown as ScreenOrientation, | ||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||
window.innerWidth = 4060; | ||||||||||||||||||||||||||||||||
window.innerHeight = 1080; | ||||||||||||||||||||||||||||||||
Object.defineProperty(navigator, 'language', { | ||||||||||||||||||||||||||||||||
get() { | ||||||||||||||||||||||||||||||||
return 'en-GB'; | ||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
dispatchClick(anchorElement); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const callParam = (window.sendBeaconLite as jest.Mock).mock.calls[0][0]; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const parsedATIParams = Object.fromEntries(new URLSearchParams(callParam)); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
expect(parsedATIParams).toMatchObject({ | ||||||||||||||||||||||||||||||||
hl: '16x30x2', | ||||||||||||||||||||||||||||||||
lng: 'en-GB', | ||||||||||||||||||||||||||||||||
r: '0x0x24x24', | ||||||||||||||||||||||||||||||||
re: '4060x1080', | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
it('Does not call sendBeacon if the event has no data-ati-tracking parameter', () => { | ||||||||||||||||||||||||||||||||
const anchorElement = document.createElement('a'); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
dispatchClick(anchorElement); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
expect(window.sendBeaconLite).toHaveBeenCalledTimes(0); | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not for the spike, but I would like to see this extracted into a utility - perhaps somewhere adjacent to, or within the useClickTrackerHandler - which we can reuse for all other components we would like to add click tracking to.