Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
67888e2
WSTEAM1-1514: Add click tracking at top level
shayneahchoon Feb 3, 2025
1e894cf
WSTEAM1-1514: Update
shayneahchoon Feb 3, 2025
975419f
WSTEAM1-1514: Update
shayneahchoon Feb 3, 2025
8c64c73
WSTEAM1-1514: Change to TS:
shayneahchoon Feb 3, 2025
7839f99
WSTEAM1-1514: rename
shayneahchoon Feb 3, 2025
3dcc093
WSTEAM1-1514: Update
shayneahchoon Feb 4, 2025
e95925f
WSTEAM1-1514: Update
shayneahchoon Feb 4, 2025
b59c095
WSTEAM1-1514: Update
shayneahchoon Feb 4, 2025
537508a
WSTEAM1-1514: Update
shayneahchoon Feb 7, 2025
640e1da
WSTEAM1-1514: Update
shayneahchoon Feb 10, 2025
9453f07
WSTEAM1-1514: Update
shayneahchoon Feb 10, 2025
86f0854
Merge branch 'latest' into WSTEAM1-1514-LITE-TRACKER-V2
shayneahchoon Feb 11, 2025
a12ff7c
Merge branch 'latest' into WSTEAM1-1514-LITE-TRACKER-V2
shayneahchoon Feb 11, 2025
30ae145
Merge branch 'latest' into WSTEAM1-1514-LITE-TRACKER-V2
shayneahchoon Feb 11, 2025
6a68ef9
WSTEAM1-1514: Update
shayneahchoon Feb 11, 2025
a355db9
WSTEAM1-1514: Update
shayneahchoon Feb 11, 2025
8e8825d
WS-91: Lint
Isabella-Mitchell Feb 11, 2025
e05c1f9
Merge branch 'latest' into WSTEAM1-1514-LITE-TRACKER-V2
Isabella-Mitchell Feb 11, 2025
7d987d7
WS-91: Lint
Isabella-Mitchell Feb 11, 2025
21adf6e
WS-91: Removes comments
Isabella-Mitchell Feb 11, 2025
e2bd36a
WSTEAM1-1514: Update
shayneahchoon Feb 12, 2025
c0c5a5a
WS-91: Handles encoded Cookies
Isabella-Mitchell Feb 12, 2025
c775b97
WS-91: Fixes redirect
Isabella-Mitchell Feb 12, 2025
a80cb91
WSTEAM1-1514: UpdatE
shayneahchoon Feb 12, 2025
a2657f0
WSTEAM1-1514: Update
shayneahchoon Feb 12, 2025
b0a9565
WSTEAM1-1514: Lint
shayneahchoon Feb 13, 2025
45b0013
WORLDSERVICE-91: Renames ati params
Isabella-Mitchell Feb 13, 2025
0d8a51c
WORLDSERVICE-91: Refactor params to use key value pairs
Isabella-Mitchell Feb 13, 2025
b5287dd
WORLDSERVICE-91:Revert due to Opera mini error
Isabella-Mitchell Feb 13, 2025
9b110b4
WSTEAM1-1514: Adds document referrer
Isabella-Mitchell Feb 13, 2025
f6e26c7
WSTEAM1-1514: Adds unit tests. Refactors
Isabella-Mitchell Feb 13, 2025
6982746
WSTEAM1-1514: Updates param build to use object.keys
Isabella-Mitchell Feb 17, 2025
2ccb419
WSTEAM1-1514: Update
shayneahchoon Feb 17, 2025
07ff788
WSTEAM1-1514: Update
shayneahchoon Feb 17, 2025
4f80f46
Merge branch 'latest' into WSTEAM1-1514-LITE-TRACKER-V2
shayneahchoon Feb 17, 2025
69f5853
WORLDSERVICE-92: Add ATI click tracking to Lite Site CTA
shayneahchoon Feb 17, 2025
3e8d696
WSTEAM1-1514: Update
shayneahchoon Feb 17, 2025
998e4c6
Merge branch 'WSTEAM1-1514-LITE-TRACKER-V2' of github.com:bbc/simorgh…
shayneahchoon Feb 17, 2025
b0895f5
WORLDSERVICE-92: Update snapshots
shayneahchoon Feb 17, 2025
710c9bc
WORLDSERVICE-92: Adds for loop to handle nested elements
Isabella-Mitchell Feb 18, 2025
e74929c
WORLDSERVICE-92: Change beacon request to synchronous.
shayneahchoon Feb 18, 2025
7a5d95a
WORLDSERVICE-92: Update.
shayneahchoon Feb 18, 2025
fba1ad4
WORLDSERVICE-92: Update.
shayneahchoon Feb 18, 2025
74499f1
Merge pull request #12419 from bbc/WORLDSERVICE-92
Isabella-Mitchell Feb 19, 2025
2d62c91
Merge branch 'latest' into WSTEAM1-1514-LITE-TRACKER-V2
Isabella-Mitchell Feb 19, 2025
228dfa1
Merge branch 'latest' into WSTEAM1-1514-LITE-TRACKER-V2
shayneahchoon Feb 19, 2025
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
12 changes: 8 additions & 4 deletions src/app/components/ATIAnalytics/canonical/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@ describe('Canonical ATI Analytics', () => {

expect(helmet.scriptTags).toHaveLength(1);
expect(helmet.scriptTags[0].innerHTML).toEqual(`
var xhr = new XMLHttpRequest();
xhr.open("GET", "${expectedUrl}", true);
xhr.withCredentials = true;
xhr.send();
function sendBeaconLite (atiPageViewUrlString) {
var xhr = new XMLHttpRequest();
xhr.open("GET", atiPageViewUrlString, true);
xhr.withCredentials = true;
xhr.send();
}

sendBeaconLite("${expectedUrl}");
`);
});

Expand Down
12 changes: 8 additions & 4 deletions src/app/components/ATIAnalytics/canonical/sendBeaconLite.ts
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;
10 changes: 10 additions & 0 deletions src/app/components/LiteSiteCta/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,14 @@ describe('LiteSiteCTA', () => {
);
expect(ctaLink).toBeTruthy();
});

it(`Should have click tracking on the 'Back to canonical' link.`, () => {
process.env.SIMORGH_ATI_BASE_URL = 'https://logws1363.ati-host.net?';
const { container } = render(<LiteSiteCta />, { isLite: true });

const [ctaLink] = container.querySelectorAll('a');
const atiUrl = ctaLink.getAttribute('data-lite-ati-tracking');

expect(atiUrl).toContain('lite-site-cta');
});
});
9 changes: 9 additions & 0 deletions src/app/components/LiteSiteCta/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @jsx jsx */
import { useContext } from 'react';
import { jsx } from '@emotion/react';
import { useATIClickTrackerHandler } from '#app/hooks/useClickTrackerHandler';
import Paragraph from '../Paragraph';
import Text from '../Text';
import { LeftChevron, RightChevron } from '../icons';
Expand All @@ -17,6 +18,7 @@ type CtaLinkProps = {
showChevron?: boolean;
ignoreLiteExtension?: boolean;
className?: string;
useClickHandler?: boolean;
};

const CtaLink = ({
Expand All @@ -26,8 +28,13 @@ const CtaLink = ({
fontVariant = 'sansRegular',
showChevron = false,
ignoreLiteExtension = false,
useClickHandler = false,
className,
}: CtaLinkProps) => {
const atiClickTrackerHandler = useATIClickTrackerHandler({
componentName: 'lite-site-cta',
});

const chevron = isRtl ? (
<LeftChevron css={styles.chevron} />
) : (
Expand All @@ -40,6 +47,7 @@ const CtaLink = ({
className={className}
css={styles.link}
{...(ignoreLiteExtension && { 'data-ignore-lite': true })}
{...(useClickHandler && atiClickTrackerHandler)}
>
<Text size="brevier" fontVariant={fontVariant} css={styles.linkText}>
{text}
Expand Down Expand Up @@ -85,6 +93,7 @@ const LiteSiteCta = () => {
text={toMainSite}
css={styles.topLinkSpacing}
ignoreLiteExtension
useClickHandler
showChevron
/>
</Paragraph>
Expand Down
6 changes: 3 additions & 3 deletions src/app/components/MostRead/Canonical/Item/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @jsx jsx */
import React, { PropsWithChildren } from 'react';
import { jsx } from '@emotion/react';
import useClickTrackerHandler from '#hooks/useClickTrackerHandler';
import { useATIClickTrackerHandler } from '#hooks/useClickTrackerHandler';
import styles from './index.styles';
import {
mostReadListGridProps,
Expand Down Expand Up @@ -46,14 +46,14 @@ export const MostReadLink = ({
size,
eventTrackingData,
}: PropsWithChildren<MostReadLinkProps>) => {
const clickTrackerHandler = useClickTrackerHandler(eventTrackingData);
const atiClickTrackerHandler = useATIClickTrackerHandler(eventTrackingData);

return (
<div css={getItemCss({ dir, size })} dir={dir}>
<a
css={[styles.link, size === 'default' && styles.defaultLink]}
href={href}
onClick={clickTrackerHandler}
{...atiClickTrackerHandler}
>
{title}
</a>
Expand Down
5 changes: 4 additions & 1 deletion src/app/components/MostRead/Canonical/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ describe('MostRead Canonical', () => {
});

it('should call the click tracking hook with the correct params', () => {
const clickTrackerSpy = jest.spyOn(clickTracking, 'default');
const clickTrackerSpy = jest.spyOn(
clickTracking,
'useATIClickTrackerHandler',
);
render(
<MostReadCanonicalWithContext
service="pidgin"
Expand Down
62 changes: 42 additions & 20 deletions src/app/hooks/useClickTrackerHandler/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,30 @@ import { Meta, Markdown } from '@storybook/blocks';

The `useClickTracker` hook handles:

* Tracking when an element has been clicked
* Sending the event to ATI
* Sending the event to Optimizely (If applicable)
- Tracking when an element has been clicked
- Sending the event to ATI
- Sending the event to Optimizely (If applicable)

`useClickTracker` must be used in combination with [`useViewTracker`](https://github.com/bbc/simorgh/blob/latest/src/app/hooks/useViewTracker/index.jsx) so ATI can calculate the view/click ratio of an element.

A click event is sent to ATI when a user performs a valid click (as per [clickTypes.js](./clickTypes.js)) on a tracked element. Specifically the following are valid clicks:

* ### General
* Middle Click
* Unmodified left click
* Left click + shift
* Tap
* ### Windows
* Left click + ctrl
* Left click + shift + ctrl
* Left click + shift + alt
* Left click + ctrl + alt
* Left click + shift + alt + ctrl
* ### macOS
* Left click + cmd
* Left click + cmd + option
* Left click + shift + option
* Left click + shift + option + cmd
- ### General
- Middle Click
- Unmodified left click
- Left click + shift
- Tap
- ### Windows
- Left click + ctrl
- Left click + shift + ctrl
- Left click + shift + alt
- Left click + ctrl + alt
- Left click + shift + alt + ctrl
- ### macOS
- Left click + cmd
- Left click + cmd + option
- Left click + shift + option
- Left click + shift + option + cmd

The hook returns an event handler promise which can be given to a component's `onClick` property to track clicks on that element and any of its children. After the element has been clicked once, it will no longer send ATI requests on click.

Expand All @@ -38,7 +38,7 @@ A click event is also fired to Optimizely using the same mechanism, but only if
### Props

<Markdown>
{`
{`
| Argument | Type | Required | Example |
| ----------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| campaignID | string | no | Provide this to override the \`campaignID\` provided by the \`EventTrackingContext\` component. This is useful for specific campaigns where you want to use a custom campaign ID |
Expand Down Expand Up @@ -179,4 +179,26 @@ const ArticlePage = () => {
const OptimizelyPromo = withOptimizelyProvider(Promo);
return OptimizelyPromo;
};


/*
* Example 5 - NEW - Log 1 click events for Lite site.
* The new useATIClickTrackerHandler() hook will detect whether a canonical or lite site has been requested, and will return the correct atiTracking method as such:
* If on a canonical site, a react onClick prop will be returned with a callback containing the necessary ATI logic.
* If on a lite site, an atiTracking url will be returned.
*/

const Promo = () => {
const atiClickTrackerHandler = useATIClickTrackerHandler({
componentName: 'promo',
});

return (
<div {...atiClickTrackerHandler}>
<button>Button 1</button>
<button>Button 2</button>
<button>Button 3</button>
</div>
)
};
```
75 changes: 61 additions & 14 deletions src/app/hooks/useClickTrackerHandler/index.jsx
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 { RequestContext } from '#app/contexts/RequestContext';
import { EventTrackingContext } from '../../contexts/EventTrackingContext';
import useTrackingToggle from '../useTrackingToggle';
import OPTIMIZELY_CONFIG from '../../lib/config/optimizely';
Expand All @@ -9,30 +10,61 @@ import { ServiceContext } from '../../contexts/ServiceContext';
import { isValidClick } from './clickTypes';

const EVENT_TYPE = 'click';
export const LITE_ATI_TRACKING = 'data-lite-ati-tracking';

const useClickTrackerHandler = (props = {}) => {
const preventNavigation = props?.preventNavigation;
const componentName = props?.componentName;
const url = props?.url;
const advertiserID = props?.advertiserID;
const format = props?.format;
const optimizely = props?.optimizely;
const optimizelyMetricNameOverride = props?.optimizelyMetricNameOverride;
const detailedPlacement = props?.detailedPlacement;

const { trackingIsEnabled } = useTrackingToggle(componentName);
const [clicked, setClicked] = useState(false);
const extractTrackingProps = (props = {}) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const eventTrackingContext = useContext(EventTrackingContext);

const { componentName, url, advertiserID, format, detailedPlacement } = props;
const {
pageIdentifier,
platform,
producerId,
producerName,
statsDestination,
producerName,
} = eventTrackingContext;

const campaignID = props?.campaignID || eventTrackingContext?.campaignID;

return {
pageIdentifier,
producerId,
platform,
statsDestination,
componentName,
campaignID,
format,
type: EVENT_TYPE,
advertiserID,
url,
detailedPlacement,
producerName,
};
};

const useClickTrackerHandler = (props = {}) => {
const {
pageIdentifier,
producerId,
platform,
statsDestination,
componentName,
campaignID,
format,
advertiserID,
url,
detailedPlacement,
producerName,
} = extractTrackingProps(props);

const preventNavigation = props?.preventNavigation;
const optimizely = props?.optimizely;
const optimizelyMetricNameOverride = props?.optimizelyMetricNameOverride;

const { trackingIsEnabled } = useTrackingToggle(componentName);
const [clicked, setClicked] = useState(false);

const { service, useReverb } = useContext(ServiceContext);

return useCallback(
Expand Down Expand Up @@ -136,4 +168,19 @@ const useClickTrackerHandler = (props = {}) => {
);
};

export const useConstructLiteSiteATIEventTrackUrl = (props = {}) => {
const atiTrackingParams = extractTrackingProps(props);
return buildATIEventTrackUrl(atiTrackingParams);
};

export const useATIClickTrackerHandler = (props = {}) => {
const { isLite } = useContext(RequestContext);
const clickTrackerHandler = useClickTrackerHandler(props);
const liteHandler = useConstructLiteSiteATIEventTrackUrl(props);

return isLite
? { [LITE_ATI_TRACKING]: liteHandler }
: { onClick: clickTrackerHandler };
};

export default useClickTrackerHandler;
23 changes: 22 additions & 1 deletion src/app/hooks/useClickTrackerHandler/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
import * as serviceContextModule from '../../contexts/ServiceContext';

import pidginData from './fixtureData/tori-51745682.json';
import useClickTrackerHandler from '.';
import useClickTrackerHandler, {
useConstructLiteSiteATIEventTrackUrl,
} from '.';

const trackingToggleSpy = jest.spyOn(trackingToggle, 'default');

Expand Down Expand Up @@ -588,3 +590,22 @@ describe('Error handling', () => {
expect(global.fetch).not.toHaveBeenCalled();
});
});

describe('Lite Site - Click tracking', () => {
it('Returns a valid ati tracking url given the input props', () => {
const { result } = renderHook(
() =>
useConstructLiteSiteATIEventTrackUrl({
...defaultProps,
campaignID: 'custom-campaign',
}),
{
wrapper,
},
);

expect(result.current).toContain(
'atc=PUB-[custom-campaign]-[brand]-[]-[CHD=promo::2]-[]-[]-[]-[]&type=AT',
);
});
});
1 change: 1 addition & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ declare global {
bootstrap: () => void;
cmd: { push: () => void };
};
sendBeaconLite: (url: string, data?: BodyInit | null) => boolean;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ exports[`Lite Site Articles Lite Site Cta should match snapshot 1`] = `
<a
class="bbc-16f79jf"
data-ignore-lite="true"
data-lite-ati-tracking="https://logws1363.ati-host.net?s=598285&atc=PUB-[]-[lite-site-cta]-[]-[]-[]-[]-[]-[]&type=AT"
href="http://localhost:7080/gahuza/articles/cey23zx8wx8o"
>
<span
Expand Down
6 changes: 6 additions & 0 deletions src/server/Document/Renderers/LiteRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable react/no-danger */
import React, { ReactElement, PropsWithChildren } from 'react';
import liteATIClickTracking from '#src/server/utilities/liteATIClickTracking';
import { BaseRendererProps } from './types';

interface Props extends BaseRendererProps {
Expand All @@ -24,6 +25,11 @@ export default function LitePageRenderer({
{helmetLinkTags}
{helmetScriptTags}
<style dangerouslySetInnerHTML={{ __html: styles }} />
<script
dangerouslySetInnerHTML={{
__html: `(${liteATIClickTracking.toString()})()`,
}}
/>
</head>
<body>{bodyContent}</body>
</html>
Expand Down
Loading
Loading