Skip to content
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

Open
wants to merge 20 commits into
base: latest
Choose a base branch
from

Conversation

shayneahchoon
Copy link
Contributor

@shayneahchoon shayneahchoon commented Feb 3, 2025

Resolves JIRA WSTEAM1-1514 & WORLDSERVICE-91

Overall changes

Implements changes from #12332

Code changes

  • Uses immediately invoked function expression instead of a string to outline script.
  • Adds unit tests.
  • Adds tracking script at top level of document.
  • Adds lite tracking on Most Read links only.
  • IMPORTANT: After this PR is merged, the lite site will be setting cookies on the client side. We may need to add a consent banner in the future if we plan to rollout lite sites to countries that require user consent.

Testing

  1. List the steps used to test this PR.

Helpful Links

Add Links to useful resources related to this PR if applicable.

Coding Standards

Repository use guidelines

src/app/hooks/useClickTrackerHandler/index.jsx Outdated Show resolved Hide resolved
src/app/components/MostRead/Canonical/Item/index.tsx Outdated Show resolved Hide resolved
src/server/utilities/clickTrackingScript/clickTracking.js Outdated Show resolved Hide resolved
src/app/hooks/useClickTrackerHandler/index.jsx Outdated Show resolved Hide resolved
src/server/utilities/clickTrackingScript/clickTracking.js Outdated Show resolved Hide resolved
}
}

if (!atUserIdValue && crypto.randomUUID) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to worry about Opera Mini + Lite, or does this work? Or is the point of Opera Mini that all users have the same ID, so it would be impossible for us to detect unique users?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll check this on Opera Mini on preview

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a feeling that using the opera mini proxy, the userId was the same, so there was little/no value including this value - in the canonical / full fat solution, we don't bother generating the uuid. We might need to do the same thing here?

export const getAtUserId = () => {
if (!onClient()) return null;
// Users accessing the site on opera "extreme data saving mode" have the pages rendered by an intermediate service
// Attempting to track these users is just tracking that proxy, causing all opera mini visitors to have the same id
if (isOperaProxy()) return null;

src/app/hooks/useClickTrackerHandler/index.jsx Outdated Show resolved Hide resolved
src/server/Document/Renderers/LiteRenderer.tsx Outdated Show resolved Hide resolved
src/app/hooks/useClickTrackerHandler/README.mdx Outdated Show resolved Hide resolved
src/app/components/MostRead/Canonical/Item/index.tsx Outdated Show resolved Hide resolved
src/app/hooks/useClickTrackerHandler/README.mdx Outdated Show resolved Hide resolved
src/app/hooks/useClickTrackerHandler/index.jsx Outdated Show resolved Hide resolved
src/server/Document/__snapshots__/component.test.jsx.snap Outdated Show resolved Hide resolved
src/server/utilities/liteATIClickTracking/index.ts Outdated Show resolved Hide resolved
src/server/utilities/liteATIClickTracking/index.ts Outdated Show resolved Hide resolved
src/server/Document/Renderers/LiteRenderer.tsx Outdated Show resolved Hide resolved
}

if (!atUserIdValue && crypto.randomUUID) {
atUserIdValue = crypto.randomUUID();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly here, crypto may not be natively supported in Opera Mini. We have a polyfill from IE here:

import getRandomValues from 'polyfill-crypto.getrandomvalues';
if (!window.crypto && !window.msCrypto) {
window.crypto = {
getRandomValues,
};
}
which Opera could be using.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been working on a PR which replaces uuid with crypto - wonder if we could borrow the generateUUID code from there? https://github.com/bbc/simorgh/blob/38995753d1d339c9f1f937d896610094e9f0ea48/src/app/lib/utilities/getUUID/index.ts

Also, crypto.randomUUID doesn't work on http, only https, meaning it wouldn't work properly on local... not a massive issue I suppose?

@amoore108
Copy link
Contributor

amoore108 commented Feb 11, 2025

Looks like a neat solution!

Just some comments on Opera Mini, as ultimately the Lite site will become its default case. We'll just need to be careful writing plain ol' JS as it won't be getting transpiled down to syntax that Opera Mini supports.

EDIT: The way this is implemented does let it transpile down, so the syntax should be ok 👍

Comment on lines +11 to +13
if (atiURL == null) {
return;
}
Copy link
Contributor

@amoore108 amoore108 Feb 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this not prevent links from proceeding if they don't have the ATI data-attribute?

@@ -0,0 +1,117 @@
import { LITE_ATI_TRACKING } from '#app/hooks/useClickTrackerHandler';
import trackingScript from '.';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import trackingScript from '.';
import liteATIClickTracking from '.';

Comment on lines +11 to +16
function mockTrackingScript() {
return 'Tracking script placeholder';
}
jest.mock('#src/server/utilities/liteATIClickTracking', () =>
mockTrackingScript.toString(),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function mockTrackingScript() {
return 'Tracking script placeholder';
}
jest.mock('#src/server/utilities/liteATIClickTracking', () =>
mockTrackingScript.toString(),
);
function liteATIClickTracking() {
return 'Tracking script placeholder';
}
jest.mock('#src/server/utilities/liteATIClickTracking', () =>
liteATIClickTracking.toString(),
);

Comment on lines +30 to +31
});
beforeEach(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: newline

Suggested change
});
beforeEach(() => {
});
beforeEach(() => {

Comment on lines +17 to +30
Object.defineProperty(document, 'cookie', {
get() {
return mockCookie;
},
set(cookieValue) {
mockCookie = cookieValue;
},
});
Object.defineProperty(window, 'location', {
value: {
assign: jest.fn(),
},
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use mocking here instead of Object.defineProperty?

Comment on lines +42 to +46
const anchorElement = document.createElement('a');
anchorElement.setAttribute(
LITE_ATI_TRACKING,
'https://logws1363.ati-host.net/?',
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We seem to use this in a number of places - is it worth extracting into a separate function & reusing in the tests which need it?

Comment on lines +66 to +69
let clientSideAtiURL = atiURL
.concat('&', 'r=', rValue)
.concat('&', 're=', reValue)
.concat('&', 'hl=', hlValue);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use URLSearchParams here? It should be supported on all browsers..? Although we may need to check: https://caniuse.com/?search=URLSearchParams

Suggested change
let clientSideAtiURL = atiURL
.concat('&', 'r=', rValue)
.concat('&', 're=', reValue)
.concat('&', 'hl=', hlValue);
const params = new URLSearchParams();
params.append('r', screenResolutionColourDepth);
params.append('re', browserViewportResolution);
params.append('hl', timestamp);

If not, could we do something which is a bit more readable - using object key values before appending them all at the bottom - this will make it a bit more future proof if we ever decide to add new params.

Suggested change
let clientSideAtiURL = atiURL
.concat('&', 'r=', rValue)
.concat('&', 're=', reValue)
.concat('&', 'hl=', hlValue);
const params = {
r: screenResolutionColourDepth,
re: browserViewportResolution,
...
...(navigator.language && { lng: navigator.language })
}

);
}

window.sendBeaconLite(clientSideAtiURL);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following on from above, if URLSearchParams is supported:

Suggested change
window.sendBeaconLite(clientSideAtiURL);
window.sendBeaconLite(${atiUrl}`&${params.toString()}`)

If not, and we have gone down the params as an object route, then we can do this:

Suggested change
window.sendBeaconLite(clientSideAtiURL);
const paramValues = Object.entries(params).map(([key, value]) => `${key}=${value}`).join('&')
window.sendBeaconLite(${atiUrl}`&${paramValues}`)

src/server/Document/__snapshots__/component.test.jsx.snap Outdated Show resolved Hide resolved
}
}

if (!atUserIdValue && crypto.randomUUID) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a feeling that using the opera mini proxy, the userId was the same, so there was little/no value including this value - in the canonical / full fat solution, we don't bother generating the uuid. We might need to do the same thing here?

export const getAtUserId = () => {
if (!onClient()) return null;
// Users accessing the site on opera "extreme data saving mode" have the pages rendered by an intermediate service
// Attempting to track these users is just tracking that proxy, causing all opera mini visitors to have the same id
if (isOperaProxy()) return null;

}

if (!atUserIdValue && crypto.randomUUID) {
atUserIdValue = crypto.randomUUID();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been working on a PR which replaces uuid with crypto - wonder if we could borrow the generateUUID code from there? https://github.com/bbc/simorgh/blob/38995753d1d339c9f1f937d896610094e9f0ea48/src/app/lib/utilities/getUUID/index.ts

Also, crypto.randomUUID doesn't work on http, only https, meaning it wouldn't work properly on local... not a massive issue I suppose?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants