From 67888e294295b6590a3db54cad0eab85514f56d1 Mon Sep 17 00:00:00 2001 From: Shayne Marc Enzo Ahchoon Date: Mon, 3 Feb 2025 15:22:32 +0000 Subject: [PATCH 01/37] WSTEAM1-1514: Add click tracking at top level --- .../ATIAnalytics/canonical/sendBeaconLite.ts | 12 ++- .../MostRead/Canonical/Item/index.tsx | 11 +- .../hooks/useClickTrackerHandler/index.jsx | 33 ++++++ .../useClickTrackerHandler/index.test.jsx | 21 +++- .../Document/Renderers/LiteRenderer.tsx | 6 ++ .../clickTrackingScript/clickTracking.js | 92 ++++++++++++++++ .../clickTrackingScript/clickTracking.test.js | 100 ++++++++++++++++++ 7 files changed, 268 insertions(+), 7 deletions(-) create mode 100644 src/server/utilities/clickTrackingScript/clickTracking.js create mode 100644 src/server/utilities/clickTrackingScript/clickTracking.test.js diff --git a/src/app/components/ATIAnalytics/canonical/sendBeaconLite.ts b/src/app/components/ATIAnalytics/canonical/sendBeaconLite.ts index 387975e2e95..0cfce17ec0c 100644 --- a/src/app/components/ATIAnalytics/canonical/sendBeaconLite.ts +++ b/src/app/components/ATIAnalytics/canonical/sendBeaconLite.ts @@ -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; diff --git a/src/app/components/MostRead/Canonical/Item/index.tsx b/src/app/components/MostRead/Canonical/Item/index.tsx index 9b993182848..f811c764f45 100755 --- a/src/app/components/MostRead/Canonical/Item/index.tsx +++ b/src/app/components/MostRead/Canonical/Item/index.tsx @@ -1,7 +1,10 @@ /** @jsx jsx */ -import React, { PropsWithChildren } from 'react'; +import React, { PropsWithChildren, useContext } from 'react'; import { jsx } from '@emotion/react'; -import useClickTrackerHandler from '#hooks/useClickTrackerHandler'; +import useClickTrackerHandler, { + LITE_TRACKER_PARAM, + useConstructLiteSiteUrl, +} from '#hooks/useClickTrackerHandler'; import styles from './index.styles'; import { mostReadListGridProps, @@ -15,6 +18,7 @@ import { } from '../../types'; import { Direction } from '../../../../models/types/global'; import Grid from '../../../../legacy/components/Grid'; +import { RequestContext } from '#app/contexts/RequestContext'; export const getParentColumns = (columnLayout: ColumnLayout) => { return columnLayout !== 'oneColumn' @@ -46,7 +50,9 @@ export const MostReadLink = ({ size, eventTrackingData, }: PropsWithChildren) => { + const { isLite } = useContext(RequestContext); const clickTrackerHandler = useClickTrackerHandler(eventTrackingData); + const liteUrl = useConstructLiteSiteUrl(eventTrackingData); return (
@@ -54,6 +60,7 @@ export const MostReadLink = ({ css={[styles.link, size === 'default' && styles.defaultLink]} href={href} onClick={clickTrackerHandler} + {...(isLite && { [LITE_TRACKER_PARAM]: liteUrl })} > {title} diff --git a/src/app/hooks/useClickTrackerHandler/index.jsx b/src/app/hooks/useClickTrackerHandler/index.jsx index 92ae9d74c83..8d2d86d4a7b 100644 --- a/src/app/hooks/useClickTrackerHandler/index.jsx +++ b/src/app/hooks/useClickTrackerHandler/index.jsx @@ -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'; const useClickTrackerHandler = (props = {}) => { const preventNavigation = props?.preventNavigation; @@ -136,4 +138,35 @@ const useClickTrackerHandler = (props = {}) => { ); }; +export const useConstructLiteSiteUrl = (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; + + 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; diff --git a/src/app/hooks/useClickTrackerHandler/index.test.jsx b/src/app/hooks/useClickTrackerHandler/index.test.jsx index 4dcfef622d3..f20f233670b 100644 --- a/src/app/hooks/useClickTrackerHandler/index.test.jsx +++ b/src/app/hooks/useClickTrackerHandler/index.test.jsx @@ -16,7 +16,7 @@ import { import * as serviceContextModule from '../../contexts/ServiceContext'; import pidginData from './fixtureData/tori-51745682.json'; -import useClickTrackerHandler from '.'; +import useClickTrackerHandler, { useConstructLiteSiteUrl } from '.'; const trackingToggleSpy = jest.spyOn(trackingToggle, 'default'); @@ -588,3 +588,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( + () => + useConstructLiteSiteUrl({ + ...defaultProps, + campaignID: 'custom-campaign', + }), + { + wrapper, + }, + ); + + expect(result.current).toContain( + 'atc=PUB-[custom-campaign]-[brand]-[]-[CHD=promo::2]-[]-[]-[]-[]&type=AT', + ); + }); +}); diff --git a/src/server/Document/Renderers/LiteRenderer.tsx b/src/server/Document/Renderers/LiteRenderer.tsx index bf7f5664788..c86d4cb7926 100644 --- a/src/server/Document/Renderers/LiteRenderer.tsx +++ b/src/server/Document/Renderers/LiteRenderer.tsx @@ -1,6 +1,7 @@ /* eslint-disable react/no-danger */ import React, { ReactElement, PropsWithChildren } from 'react'; import { BaseRendererProps } from './types'; +import trackingScript from '#src/server/utilities/clickTrackingScript/clickTracking'; interface Props extends BaseRendererProps { bodyContent: ReactElement; @@ -24,6 +25,11 @@ export default function LitePageRenderer({ {helmetLinkTags} {helmetScriptTags} +
From 537508a8639bb81ce5e06f876db8fe294a91f945 Mon Sep 17 00:00:00 2001 From: Shayne Marc Enzo Ahchoon Date: Fri, 7 Feb 2025 16:45:16 +0000 Subject: [PATCH 09/37] WSTEAM1-1514: Update --- .../MostRead/Canonical/Item/index.tsx | 20 +--- .../hooks/useClickTrackerHandler/README.mdx | 62 ++++++++---- .../hooks/useClickTrackerHandler/index.jsx | 94 +++++++++++-------- .../liteATIClickTracking/index.test.ts | 50 +++++----- .../utilities/liteATIClickTracking/index.ts | 2 +- 5 files changed, 127 insertions(+), 101 deletions(-) diff --git a/src/app/components/MostRead/Canonical/Item/index.tsx b/src/app/components/MostRead/Canonical/Item/index.tsx index d1b210074d6..892dc997ff9 100755 --- a/src/app/components/MostRead/Canonical/Item/index.tsx +++ b/src/app/components/MostRead/Canonical/Item/index.tsx @@ -1,10 +1,7 @@ /** @jsx jsx */ -import React, { PropsWithChildren, useContext } from 'react'; +import React, { PropsWithChildren } from 'react'; import { jsx } from '@emotion/react'; -import useClickTrackerHandler, { - LITE_TRACKER_PARAM, - useConstructLiteSiteATIEventTrackUrl, -} from '#hooks/useClickTrackerHandler'; +import { useATIClickTrackerHandler } from '#hooks/useClickTrackerHandler'; import styles from './index.styles'; import { mostReadListGridProps, @@ -18,7 +15,6 @@ import { } from '../../types'; import { Direction } from '../../../../models/types/global'; import Grid from '../../../../legacy/components/Grid'; -import { RequestContext } from '#app/contexts/RequestContext'; export const getParentColumns = (columnLayout: ColumnLayout) => { return columnLayout !== 'oneColumn' @@ -50,22 +46,14 @@ export const MostReadLink = ({ size, eventTrackingData, }: PropsWithChildren) => { - const { isLite } = useContext(RequestContext); - - const clickTrackerHandler = isLite - ? { - [LITE_TRACKER_PARAM]: useConstructLiteSiteATIEventTrackUrl(eventTrackingData), - } - : { - onClick: useClickTrackerHandler(eventTrackingData), - }; + const clickTrackerHandlerProp = useATIClickTrackerHandler(eventTrackingData); return (
{title} diff --git a/src/app/hooks/useClickTrackerHandler/README.mdx b/src/app/hooks/useClickTrackerHandler/README.mdx index 23f67fabed9..501474b8d11 100644 --- a/src/app/hooks/useClickTrackerHandler/README.mdx +++ b/src/app/hooks/useClickTrackerHandler/README.mdx @@ -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. @@ -38,7 +38,7 @@ A click event is also fired to Optimizely using the same mechanism, but only if ### Props -{` + {` | 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 | @@ -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, a atiTracking url will be returned. + */ + +const Promo = () => { + const trackingProp = useATIClickTrackerHandler({ + componentName: 'promo', + }); + + return ( +
+ + + +
+ ) +}; ``` diff --git a/src/app/hooks/useClickTrackerHandler/index.jsx b/src/app/hooks/useClickTrackerHandler/index.jsx index 18ff50290c1..bbd45a90879 100644 --- a/src/app/hooks/useClickTrackerHandler/index.jsx +++ b/src/app/hooks/useClickTrackerHandler/index.jsx @@ -1,6 +1,6 @@ +/* eslint-disable import/order */ /* 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'; @@ -8,32 +8,59 @@ import OPTIMIZELY_CONFIG from '../../lib/config/optimizely'; import { sendEventBeacon } from '../../components/ATIAnalytics/beacon/index'; import { ServiceContext } from '../../contexts/ServiceContext'; import { isValidClick } from './clickTypes'; +import { RequestContext } from '#app/contexts/RequestContext'; const EVENT_TYPE = 'click'; -export const LITE_TRACKER_PARAM = 'data-ati-tracking'; +export const LITE_ATI_TRACKING = 'data-lite-ati-tracking'; + +const useExtractTrackingProps = (props = {}) => { + const eventTrackingContext = useContext(EventTrackingContext); + + const { componentName, url, advertiserID, format, detailedPlacement } = props; + const { pageIdentifier, platform, producerId, statsDestination } = + eventTrackingContext; + + const campaignID = props?.campaignID || eventTrackingContext?.campaignID; + + return { + pageIdentifier, + producerId, + platform, + statsDestination, + componentName, + campaignID, + format, + type: EVENT_TYPE, + advertiserID, + url, + detailedPlacement, + }; +}; +// TODO - Refactor this once all components have been updated to use the useATIClickTrackerHandler hook. const useClickTrackerHandler = (props = {}) => { + const { + pageIdentifier, + producerId, + platform, + statsDestination, + componentName, + campaignID, + format, + advertiserID, + url, + detailedPlacement, + } = useExtractTrackingProps(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 eventTrackingContext = useContext(EventTrackingContext); - const { - pageIdentifier, - platform, - producerId, - producerName, - statsDestination, - } = eventTrackingContext; - const campaignID = props?.campaignID || eventTrackingContext?.campaignID; + const { producerName } = eventTrackingContext; const { service, useReverb } = useContext(ServiceContext); @@ -139,34 +166,19 @@ 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; - - const { pageIdentifier, platform, producerId, statsDestination } = - eventTrackingContext; - - const campaignID = props?.campaignID || eventTrackingContext?.campaignID; + const atiTrackingParams = useExtractTrackingProps(props); + const atiClickTrackingUrl = buildATIEventTrackUrl(atiTrackingParams); + return atiClickTrackingUrl; +}; - const atiClickTrackingUrl = buildATIEventTrackUrl({ - pageIdentifier, - producerId, - platform, - statsDestination, - componentName, - campaignID, - format, - type: EVENT_TYPE, - advertiserID, - url, - detailedPlacement, - }); +export const useATIClickTrackerHandler = (props = {}) => { + const { isLite } = useContext(RequestContext); + const canonicalHandler = useClickTrackerHandler(props); + const liteHandler = useConstructLiteSiteATIEventTrackUrl(props); - return atiClickTrackingUrl; + return isLite + ? { [LITE_ATI_TRACKING]: liteHandler } + : { onClick: canonicalHandler }; }; export default useClickTrackerHandler; diff --git a/src/server/utilities/liteATIClickTracking/index.test.ts b/src/server/utilities/liteATIClickTracking/index.test.ts index 22b99e630cf..169943b05f6 100644 --- a/src/server/utilities/liteATIClickTracking/index.test.ts +++ b/src/server/utilities/liteATIClickTracking/index.test.ts @@ -12,32 +12,34 @@ const dispatchClick = (targetElement: HTMLElement) => { describe('Click tracking script', () => { const randomUUIDMock = jest.fn(); - beforeEach(() => { - jest.clearAllMocks(); - jest.useFakeTimers().setSystemTime(new Date(1731515402000)); - - trackingScript(); - - document.cookie = - 'atuserid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - delete window.location; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - window.location = { - assign: jest.fn(), - }; - - window.sendBeaconLite = jest.fn(); - + beforeAll(() => { + let mockCookie = ''; + Object.defineProperty(document, 'cookie', { + get() { + return mockCookie; + }, + set(cookieValue) { + mockCookie = cookieValue; + }, + }); + Object.defineProperty(window, 'location', { + value: { + assign: jest.fn(), + }, + }); Object.defineProperty(global, 'crypto', { value: { randomUUID: randomUUIDMock, }, }); + }); + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers().setSystemTime(new Date('2024-11-13T16:30:02.000Z')); + trackingScript(); + document.cookie = ''; + window.sendBeaconLite = jest.fn(); window.dispatchEvent(new Event('load')); }); @@ -50,8 +52,9 @@ describe('Click tracking script', () => { randomUUIDMock.mockReturnValueOnce('randomUniqueId'); dispatchClick(anchorElement); - - expect(document.cookie).toBe('atuserid={"val":"randomUniqueId"}'); + expect(document.cookie).toBe( + 'atuserid={"val":"randomUniqueId"}; path=/; max-age=397; Secure;', + ); }); it('Reuses the atuserid cookie if there is a atuserid cookie on the user browser', () => { @@ -61,7 +64,8 @@ describe('Click tracking script', () => { 'https://logws1363.ati-host.net/?', ); - document.cookie = 'atuserid={"val":"oldCookieId"}'; + document.cookie = + 'atuserid={"val":"oldCookieId"}; path=/; max-age=397; Secure;'; randomUUIDMock.mockReturnValueOnce('newCookieId'); dispatchClick(anchorElement); diff --git a/src/server/utilities/liteATIClickTracking/index.ts b/src/server/utilities/liteATIClickTracking/index.ts index 4de61bb8382..fb97573bedf 100644 --- a/src/server/utilities/liteATIClickTracking/index.ts +++ b/src/server/utilities/liteATIClickTracking/index.ts @@ -49,7 +49,7 @@ export default function trackingScript() { const stringifiedCookieValue = JSON.stringify({ val: atUserIdValue }); if (atUserIdValue) { - document.cookie = `${cookieName}=${stringifiedCookieValue}; path=/; max-age=${expires};`; + document.cookie = `${cookieName}=${stringifiedCookieValue}; path=/; max-age=${expires}; Secure;`; } const rValue = [ From 640e1da25ed7e0aa935999bb2bac942f4daee012 Mon Sep 17 00:00:00 2001 From: Shayne Marc Enzo Ahchoon Date: Mon, 10 Feb 2025 11:06:08 +0000 Subject: [PATCH 10/37] WSTEAM1-1514: Update --- src/app/components/MostRead/Canonical/index.test.tsx | 5 ++++- src/server/Document/__snapshots__/component.test.jsx.snap | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/components/MostRead/Canonical/index.test.tsx b/src/app/components/MostRead/Canonical/index.test.tsx index 3e671b14848..8845b397f3e 100644 --- a/src/app/components/MostRead/Canonical/index.test.tsx +++ b/src/app/components/MostRead/Canonical/index.test.tsx @@ -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( Date: Mon, 10 Feb 2025 14:14:06 +0000 Subject: [PATCH 11/37] WSTEAM1-1514: Update --- src/app/hooks/useClickTrackerHandler/index.jsx | 1 + .../__snapshots__/component.test.jsx.snap | 2 +- .../liteATIClickTracking/index.test.ts | 17 ++++++----------- .../utilities/liteATIClickTracking/index.ts | 2 +- src/testHelpers/jest-setup.js | 2 ++ 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/app/hooks/useClickTrackerHandler/index.jsx b/src/app/hooks/useClickTrackerHandler/index.jsx index bbd45a90879..916f0addc48 100644 --- a/src/app/hooks/useClickTrackerHandler/index.jsx +++ b/src/app/hooks/useClickTrackerHandler/index.jsx @@ -171,6 +171,7 @@ export const useConstructLiteSiteATIEventTrackUrl = (props = {}) => { return atiClickTrackingUrl; }; +// Change this one to the default export once all components have been changed to use this method. export const useATIClickTrackerHandler = (props = {}) => { const { isLite } = useContext(RequestContext); const canonicalHandler = useClickTrackerHandler(props); diff --git a/src/server/Document/__snapshots__/component.test.jsx.snap b/src/server/Document/__snapshots__/component.test.jsx.snap index 5eccf527e9f..6fb019bedd6 100644 --- a/src/server/Document/__snapshots__/component.test.jsx.snap +++ b/src/server/Document/__snapshots__/component.test.jsx.snap @@ -214,7 +214,7 @@ exports[`Document Component should render LITE version correctly 1`] = ` if ((targetElement === null || targetElement === void 0 ? void 0 : targetElement.tagName) === 'A') { event.stopPropagation(); event.preventDefault(); - var atiURL = targetElement.getAttribute('data-ati-tracking'); + var atiURL = targetElement.getAttribute('data-lite-ati-tracking'); if (atiURL == null) { return; } diff --git a/src/server/utilities/liteATIClickTracking/index.test.ts b/src/server/utilities/liteATIClickTracking/index.test.ts index 169943b05f6..9d7b372f54e 100644 --- a/src/server/utilities/liteATIClickTracking/index.test.ts +++ b/src/server/utilities/liteATIClickTracking/index.test.ts @@ -1,3 +1,4 @@ +import { LITE_ATI_TRACKING } from '#app/hooks/useClickTrackerHandler'; import trackingScript from '.'; const dispatchClick = (targetElement: HTMLElement) => { @@ -11,7 +12,6 @@ const dispatchClick = (targetElement: HTMLElement) => { }; describe('Click tracking script', () => { - const randomUUIDMock = jest.fn(); beforeAll(() => { let mockCookie = ''; Object.defineProperty(document, 'cookie', { @@ -27,11 +27,6 @@ describe('Click tracking script', () => { assign: jest.fn(), }, }); - Object.defineProperty(global, 'crypto', { - value: { - randomUUID: randomUUIDMock, - }, - }); }); beforeEach(() => { jest.clearAllMocks(); @@ -46,11 +41,11 @@ describe('Click tracking script', () => { 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', + LITE_ATI_TRACKING, 'https://logws1363.ati-host.net/?', ); - randomUUIDMock.mockReturnValueOnce('randomUniqueId'); + (crypto.randomUUID as jest.Mock).mockReturnValueOnce('randomUniqueId'); dispatchClick(anchorElement); expect(document.cookie).toBe( 'atuserid={"val":"randomUniqueId"}; path=/; max-age=397; Secure;', @@ -60,13 +55,13 @@ describe('Click tracking script', () => { 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', + LITE_ATI_TRACKING, 'https://logws1363.ati-host.net/?', ); document.cookie = 'atuserid={"val":"oldCookieId"}; path=/; max-age=397; Secure;'; - randomUUIDMock.mockReturnValueOnce('newCookieId'); + (crypto.randomUUID as jest.Mock).mockReturnValueOnce('newCookieId'); dispatchClick(anchorElement); const callParam = (window.sendBeaconLite as jest.Mock).mock.calls[0][0]; @@ -77,7 +72,7 @@ describe('Click tracking script', () => { it('Calls sendBeaconLite() with the correct url', () => { const anchorElement = document.createElement('a'); anchorElement.setAttribute( - 'data-ati-tracking', + LITE_ATI_TRACKING, 'https://logws1363.ati-host.net/?', ); diff --git a/src/server/utilities/liteATIClickTracking/index.ts b/src/server/utilities/liteATIClickTracking/index.ts index fb97573bedf..cbd237bbcbe 100644 --- a/src/server/utilities/liteATIClickTracking/index.ts +++ b/src/server/utilities/liteATIClickTracking/index.ts @@ -7,7 +7,7 @@ export default function trackingScript() { event.stopPropagation(); event.preventDefault(); - const atiURL = targetElement.getAttribute('data-ati-tracking'); + const atiURL = targetElement.getAttribute('data-lite-ati-tracking'); if (atiURL == null) { return; diff --git a/src/testHelpers/jest-setup.js b/src/testHelpers/jest-setup.js index 0dc25fef5db..4c6711ee3ab 100644 --- a/src/testHelpers/jest-setup.js +++ b/src/testHelpers/jest-setup.js @@ -15,6 +15,8 @@ global.MessagePort = MessagePort; window.require = jest.fn(); +global.crypto.randomUUID = jest.fn(); + /* * Mock to avoid async behaviour in tests */ From 6a68ef9d131eaa2cbee0a5cb0710696ce87a2f6a Mon Sep 17 00:00:00 2001 From: Shayne Marc Enzo Ahchoon Date: Tue, 11 Feb 2025 10:56:01 +0000 Subject: [PATCH 12/37] WSTEAM1-1514: Update --- .../__snapshots__/component.test.jsx.snap | 70 +------------------ src/server/Document/component.test.jsx | 7 ++ 2 files changed, 9 insertions(+), 68 deletions(-) diff --git a/src/server/Document/__snapshots__/component.test.jsx.snap b/src/server/Document/__snapshots__/component.test.jsx.snap index 529c6a2723c..c1853782939 100644 --- a/src/server/Document/__snapshots__/component.test.jsx.snap +++ b/src/server/Document/__snapshots__/component.test.jsx.snap @@ -206,74 +206,8 @@ exports[`Document Component should render LITE version correctly 1`] = ` .css-7prgni-StyledLink{display:inline-block;} diff --git a/src/server/Document/component.test.jsx b/src/server/Document/component.test.jsx index 2f76c64f627..a11dc96304e 100644 --- a/src/server/Document/component.test.jsx +++ b/src/server/Document/component.test.jsx @@ -8,6 +8,13 @@ import DocumentComponent from './component'; Helmet.canUseDOM = false; +function mockTrackingScript() { + return 'Tracking script placeholder'; +} +jest.mock('#src/server/utilities/liteATIClickTracking', () => + mockTrackingScript.toString(), +); + describe('Document Component', () => { const originalProcessEnv = process.env; From a355db97db66d14d87983c94981a2049b3cad144 Mon Sep 17 00:00:00 2001 From: Shayne Marc Enzo Ahchoon Date: Tue, 11 Feb 2025 11:16:14 +0000 Subject: [PATCH 13/37] WSTEAM1-1514: Update --- src/app/components/MostRead/Canonical/Item/index.tsx | 4 ++-- src/app/hooks/useClickTrackerHandler/README.mdx | 6 +++--- src/server/Document/Renderers/LiteRenderer.tsx | 4 ++-- src/server/utilities/liteATIClickTracking/index.ts | 3 +-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/app/components/MostRead/Canonical/Item/index.tsx b/src/app/components/MostRead/Canonical/Item/index.tsx index 892dc997ff9..ef5696fb43d 100755 --- a/src/app/components/MostRead/Canonical/Item/index.tsx +++ b/src/app/components/MostRead/Canonical/Item/index.tsx @@ -46,14 +46,14 @@ export const MostReadLink = ({ size, eventTrackingData, }: PropsWithChildren) => { - const clickTrackerHandlerProp = useATIClickTrackerHandler(eventTrackingData); + const atiClickTrackerHandler = useATIClickTrackerHandler(eventTrackingData); return (
{title} diff --git a/src/app/hooks/useClickTrackerHandler/README.mdx b/src/app/hooks/useClickTrackerHandler/README.mdx index 501474b8d11..fc194897da6 100644 --- a/src/app/hooks/useClickTrackerHandler/README.mdx +++ b/src/app/hooks/useClickTrackerHandler/README.mdx @@ -185,16 +185,16 @@ const ArticlePage = () => { * 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, a atiTracking url will be returned. + * If on a lite site, an atiTracking url will be returned. */ const Promo = () => { - const trackingProp = useATIClickTrackerHandler({ + const atiClickTrackerHandler = useATIClickTrackerHandler({ componentName: 'promo', }); return ( -
+
diff --git a/src/server/Document/Renderers/LiteRenderer.tsx b/src/server/Document/Renderers/LiteRenderer.tsx index d392a4a1a27..305ab1120ab 100644 --- a/src/server/Document/Renderers/LiteRenderer.tsx +++ b/src/server/Document/Renderers/LiteRenderer.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-danger */ import React, { ReactElement, PropsWithChildren } from 'react'; -import trackingScript from '#src/server/utilities/liteATIClickTracking'; +import liteATIClickTracking from '#src/server/utilities/liteATIClickTracking'; import { BaseRendererProps } from './types'; interface Props extends BaseRendererProps { @@ -27,7 +27,7 @@ export default function LitePageRenderer({ diff --git a/src/server/Document/component.test.jsx b/src/server/Document/component.test.jsx index 86f41cd734d..fcd5b0f0e05 100644 --- a/src/server/Document/component.test.jsx +++ b/src/server/Document/component.test.jsx @@ -8,11 +8,10 @@ import DocumentComponent from './component'; Helmet.canUseDOM = false; -function liteATIClickTracking() { - return 'Tracking script placeholder'; -} jest.mock('#src/server/utilities/liteATIClickTracking', () => - liteATIClickTracking.toString(), + function liteATIClickTracking() { + return 'Tracking script placeholder'; + }.toString(), ); describe('Document Component', () => { diff --git a/src/server/utilities/liteATIClickTracking/index.ts b/src/server/utilities/liteATIClickTracking/index.ts index 639b872be9e..410f9ef6e65 100644 --- a/src/server/utilities/liteATIClickTracking/index.ts +++ b/src/server/utilities/liteATIClickTracking/index.ts @@ -37,7 +37,7 @@ export default () => { } } - if (!user.val && crypto?.randomUUID) { + if (!user.val && window.crypto && crypto.randomUUID) { user.val = crypto.randomUUID(); } From 45b001397c059a9599ebb903b24a05760a051e84 Mon Sep 17 00:00:00 2001 From: Isabella Mitchell Date: Thu, 13 Feb 2025 15:08:38 +0000 Subject: [PATCH 23/37] WORLDSERVICE-91: Renames ati params --- .../liteATIClickTracking/index.test.ts | 1 + .../utilities/liteATIClickTracking/index.ts | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/server/utilities/liteATIClickTracking/index.test.ts b/src/server/utilities/liteATIClickTracking/index.test.ts index 5775957db9e..ef1524cd414 100644 --- a/src/server/utilities/liteATIClickTracking/index.test.ts +++ b/src/server/utilities/liteATIClickTracking/index.test.ts @@ -166,6 +166,7 @@ describe('Click tracking script', () => { lng: 'en-GB', r: '0x0x24x24', re: '4060x1080', + app_type: 'lite', }); }); }); diff --git a/src/server/utilities/liteATIClickTracking/index.ts b/src/server/utilities/liteATIClickTracking/index.ts index 410f9ef6e65..20642e45c35 100644 --- a/src/server/utilities/liteATIClickTracking/index.ts +++ b/src/server/utilities/liteATIClickTracking/index.ts @@ -16,6 +16,7 @@ export default () => { innerWidth, innerHeight, } = window; + const now = new Date(); const hours = now.getHours(); const mins = now.getMinutes(); @@ -48,21 +49,24 @@ export default () => { document.cookie = `${cookieName}=${encodedCookieValue}; path=/; max-age=${expires}; Secure;`; } - const rValue = [ + const screenResolutionColourDepth = [ width || 0, height || 0, colorDepth || 0, pixelDepth || 0, ].join('x'); - const reValue = [innerWidth || 0, innerHeight || 0].join('x'); + const browserViewportResolution = [ + innerWidth || 0, + innerHeight || 0, + ].join('x'); - const hlValue = [hours, mins, secs].join('x'); + const timestamp = [hours, mins, secs].join('x'); let clientSideAtiURL = atiURL - .concat('&', 'r=', rValue) - .concat('&', 're=', reValue) - .concat('&', 'hl=', hlValue); + .concat('&', 'r=', screenResolutionColourDepth) + .concat('&', 're=', browserViewportResolution) + .concat('&', 'hl=', timestamp); if (navigator.language) { clientSideAtiURL = clientSideAtiURL.concat( @@ -72,6 +76,8 @@ export default () => { ); } + clientSideAtiURL = clientSideAtiURL.concat('&', 'app_type=', 'lite'); + if (user.val) { clientSideAtiURL = clientSideAtiURL.concat( '&', From 0d8a51c0ef1f44b90d02a9452e383537edc22d0b Mon Sep 17 00:00:00 2001 From: Isabella Mitchell Date: Thu, 13 Feb 2025 15:14:22 +0000 Subject: [PATCH 24/37] WORLDSERVICE-91: Refactor params to use key value pairs --- .../utilities/liteATIClickTracking/index.ts | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/src/server/utilities/liteATIClickTracking/index.ts b/src/server/utilities/liteATIClickTracking/index.ts index 20642e45c35..817d8c176b9 100644 --- a/src/server/utilities/liteATIClickTracking/index.ts +++ b/src/server/utilities/liteATIClickTracking/index.ts @@ -63,30 +63,19 @@ export default () => { const timestamp = [hours, mins, secs].join('x'); - let clientSideAtiURL = atiURL - .concat('&', 'r=', screenResolutionColourDepth) - .concat('&', 're=', browserViewportResolution) - .concat('&', 'hl=', timestamp); - - if (navigator.language) { - clientSideAtiURL = clientSideAtiURL.concat( - '&', - 'lng=', - navigator.language, - ); - } - - clientSideAtiURL = clientSideAtiURL.concat('&', 'app_type=', 'lite'); - - if (user.val) { - clientSideAtiURL = clientSideAtiURL.concat( - '&', - 'idclient=', - user.val, - ); - } - - window.sendBeaconLite(clientSideAtiURL); + const params = { + r: screenResolutionColourDepth, + re: browserViewportResolution, + hl: timestamp, + ...(navigator.language && { lng: navigator.language }), + app_type: 'lite', + ...(user.val && { idclient: user.val }), + }; + + const paramValues = Object.entries(params) + .map(([key, value]) => `${key}=${value}`) + .join('&'); + window.sendBeaconLite(`${atiURL}&${paramValues}`); } window.location.assign(nextPageUrl); From b5287ddb71fc434a74422ffeec84825daa5ecc20 Mon Sep 17 00:00:00 2001 From: Isabella Mitchell Date: Thu, 13 Feb 2025 15:54:11 +0000 Subject: [PATCH 25/37] WORLDSERVICE-91:Revert due to Opera mini error --- .../utilities/liteATIClickTracking/index.ts | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/server/utilities/liteATIClickTracking/index.ts b/src/server/utilities/liteATIClickTracking/index.ts index 817d8c176b9..20642e45c35 100644 --- a/src/server/utilities/liteATIClickTracking/index.ts +++ b/src/server/utilities/liteATIClickTracking/index.ts @@ -63,19 +63,30 @@ export default () => { const timestamp = [hours, mins, secs].join('x'); - const params = { - r: screenResolutionColourDepth, - re: browserViewportResolution, - hl: timestamp, - ...(navigator.language && { lng: navigator.language }), - app_type: 'lite', - ...(user.val && { idclient: user.val }), - }; - - const paramValues = Object.entries(params) - .map(([key, value]) => `${key}=${value}`) - .join('&'); - window.sendBeaconLite(`${atiURL}&${paramValues}`); + let clientSideAtiURL = atiURL + .concat('&', 'r=', screenResolutionColourDepth) + .concat('&', 're=', browserViewportResolution) + .concat('&', 'hl=', timestamp); + + if (navigator.language) { + clientSideAtiURL = clientSideAtiURL.concat( + '&', + 'lng=', + navigator.language, + ); + } + + clientSideAtiURL = clientSideAtiURL.concat('&', 'app_type=', 'lite'); + + if (user.val) { + clientSideAtiURL = clientSideAtiURL.concat( + '&', + 'idclient=', + user.val, + ); + } + + window.sendBeaconLite(clientSideAtiURL); } window.location.assign(nextPageUrl); From 9b110b47eba5db273f8a09dbb0420ea5b1b52659 Mon Sep 17 00:00:00 2001 From: Isabella Mitchell Date: Thu, 13 Feb 2025 16:25:19 +0000 Subject: [PATCH 26/37] WSTEAM1-1514: Adds document referrer --- src/server/utilities/liteATIClickTracking/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/server/utilities/liteATIClickTracking/index.ts b/src/server/utilities/liteATIClickTracking/index.ts index 20642e45c35..bc9d171e3bf 100644 --- a/src/server/utilities/liteATIClickTracking/index.ts +++ b/src/server/utilities/liteATIClickTracking/index.ts @@ -86,6 +86,14 @@ export default () => { ); } + if (document.referrer) { + clientSideAtiURL = clientSideAtiURL.concat( + '&', + 'ref=', + document.referrer, + ); + } + window.sendBeaconLite(clientSideAtiURL); } From f6e26c76f70f18a4aa4fbcdc88c2786631aa5484 Mon Sep 17 00:00:00 2001 From: Isabella Mitchell Date: Thu, 13 Feb 2025 16:25:42 +0000 Subject: [PATCH 27/37] WSTEAM1-1514: Adds unit tests. Refactors --- .../hooks/useClickTrackerHandler/index.jsx | 4 +- .../liteATIClickTracking/index.test.ts | 70 +++++++++++-------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/app/hooks/useClickTrackerHandler/index.jsx b/src/app/hooks/useClickTrackerHandler/index.jsx index 352a1caaa28..ca54c151629 100644 --- a/src/app/hooks/useClickTrackerHandler/index.jsx +++ b/src/app/hooks/useClickTrackerHandler/index.jsx @@ -171,12 +171,12 @@ export const useConstructLiteSiteATIEventTrackUrl = (props = {}) => { export const useATIClickTrackerHandler = (props = {}) => { const { isLite } = useContext(RequestContext); - const canonicalHandler = useClickTrackerHandler(props); + const clickTrackerHandler = useClickTrackerHandler(props); const liteHandler = useConstructLiteSiteATIEventTrackUrl(props); return isLite ? { [LITE_ATI_TRACKING]: liteHandler } - : { onClick: canonicalHandler }; + : { onClick: clickTrackerHandler }; }; export default useClickTrackerHandler; diff --git a/src/server/utilities/liteATIClickTracking/index.test.ts b/src/server/utilities/liteATIClickTracking/index.test.ts index ef1524cd414..d4cef9ae300 100644 --- a/src/server/utilities/liteATIClickTracking/index.test.ts +++ b/src/server/utilities/liteATIClickTracking/index.test.ts @@ -1,6 +1,24 @@ import { LITE_ATI_TRACKING } from '#app/hooks/useClickTrackerHandler'; import liteATIClickTracking from '.'; +const createAnchor = ({ + href = '/', + hasliteSiteTracking, +}: { + href?: string; + hasliteSiteTracking: boolean; +}) => { + const anchorElement = document.createElement('a'); + anchorElement.href = href; + if (hasliteSiteTracking) { + anchorElement.setAttribute( + LITE_ATI_TRACKING, + 'https://logws1363.ati-host.net/?', + ); + } + return anchorElement; +}; + const dispatchClick = (targetElement: HTMLElement) => { document.body.appendChild(targetElement); const event = new MouseEvent('click', { @@ -52,8 +70,10 @@ describe('Click tracking script', () => { }); it('Does not call sendBeacon if the event has no data-ati-tracking parameter, but still redirects', () => { - const anchorElement = document.createElement('a'); - anchorElement.href = '/gahuza'; + const anchorElement = createAnchor({ + href: '/gahuza', + hasliteSiteTracking: false, + }); dispatchClick(anchorElement); @@ -62,12 +82,10 @@ describe('Click tracking script', () => { expect(nextPageUrl).toContain('/gahuza'); }); - it('Does not add userId cookie if crypto is invalid, but stil calls sendBeacon', () => { - const anchorElement = document.createElement('a'); - anchorElement.setAttribute( - LITE_ATI_TRACKING, - 'https://logws1363.ati-host.net/?', - ); + it('Does not add userId cookie if crypto is unsupported, but still calls sendBeacon', () => { + const anchorElement = createAnchor({ + hasliteSiteTracking: true, + }); // @ts-expect-error Some browsers may not have crypto. // eslint-disable-next-line no-global-assign @@ -80,11 +98,9 @@ describe('Click tracking script', () => { }); it('Sets a new cookie if there is no atuserid cookie on the user browser', () => { - const anchorElement = document.createElement('a'); - anchorElement.setAttribute( - LITE_ATI_TRACKING, - 'https://logws1363.ati-host.net/?', - ); + const anchorElement = createAnchor({ + hasliteSiteTracking: true, + }); (crypto.randomUUID as jest.Mock).mockReturnValueOnce('randomUniqueId'); dispatchClick(anchorElement); @@ -97,11 +113,9 @@ describe('Click tracking script', () => { }); it('Does not overwrite content in atuserid cookie if it already exists', () => { - const anchorElement = document.createElement('a'); - anchorElement.setAttribute( - LITE_ATI_TRACKING, - 'https://logws1363.ati-host.net/?', - ); + const anchorElement = createAnchor({ + hasliteSiteTracking: true, + }); const oldCookieId = '22ea8f97e5-4c34-4d23-af1d-4d1789206639'; document.cookie = `atuserid=%7B%22name%22%3A%22atuserid%22%2C%22val%22%3A%${oldCookieId}%22%2C%22options%22%3A%7B%22end%22%3A%222026-03-11T10%3A23%3A55.442Z%22%2C%22path%22%3A%22%2F%22%7D%7D; path=/; max-age=397; Secure;`; @@ -114,11 +128,9 @@ describe('Click tracking script', () => { }); it('Reuses the atuserid cookie if there is a atuserid cookie on the user browser', () => { - const anchorElement = document.createElement('a'); - anchorElement.setAttribute( - LITE_ATI_TRACKING, - 'https://logws1363.ati-host.net/?', - ); + const anchorElement = createAnchor({ + hasliteSiteTracking: true, + }); document.cookie = 'atuserid={"val":"oldCookieId"}; path=/; max-age=397; Secure;'; @@ -130,15 +142,16 @@ describe('Click tracking script', () => { }); it('Calls sendBeaconLite() with the correct url', () => { - const anchorElement = document.createElement('a'); - anchorElement.setAttribute( - LITE_ATI_TRACKING, - 'https://logws1363.ati-host.net/?', - ); + const anchorElement = createAnchor({ + hasliteSiteTracking: true, + }); document.cookie = 'atuserid={"val":"userCookieId"}; path=/; max-age=397; Secure;'; + Object.defineProperty(document, 'referrer', { + value: 'https://www.bbc.com', + }); window.screen = { width: 100, height: 400, @@ -167,6 +180,7 @@ describe('Click tracking script', () => { r: '0x0x24x24', re: '4060x1080', app_type: 'lite', + ref: 'https://www.bbc.com', }); }); }); From 69827469c157ee6a30294f588a9990b39936575c Mon Sep 17 00:00:00 2001 From: Isabella Mitchell Date: Mon, 17 Feb 2025 09:46:37 +0000 Subject: [PATCH 28/37] WSTEAM1-1514: Updates param build to use object.keys --- .../utilities/liteATIClickTracking/index.ts | 46 ++++++------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/src/server/utilities/liteATIClickTracking/index.ts b/src/server/utilities/liteATIClickTracking/index.ts index bc9d171e3bf..3eea80d53e4 100644 --- a/src/server/utilities/liteATIClickTracking/index.ts +++ b/src/server/utilities/liteATIClickTracking/index.ts @@ -63,38 +63,20 @@ export default () => { const timestamp = [hours, mins, secs].join('x'); - let clientSideAtiURL = atiURL - .concat('&', 'r=', screenResolutionColourDepth) - .concat('&', 're=', browserViewportResolution) - .concat('&', 'hl=', timestamp); - - if (navigator.language) { - clientSideAtiURL = clientSideAtiURL.concat( - '&', - 'lng=', - navigator.language, - ); - } - - clientSideAtiURL = clientSideAtiURL.concat('&', 'app_type=', 'lite'); - - if (user.val) { - clientSideAtiURL = clientSideAtiURL.concat( - '&', - 'idclient=', - user.val, - ); - } - - if (document.referrer) { - clientSideAtiURL = clientSideAtiURL.concat( - '&', - 'ref=', - document.referrer, - ); - } - - window.sendBeaconLite(clientSideAtiURL); + const params: Record = { + r: screenResolutionColourDepth, + re: browserViewportResolution, + hl: timestamp, + ...(navigator.language && { lng: navigator.language }), + app_type: 'lite', + ...(user.val && { idclient: user.val }), + ...(document.referrer && { ref: document.referrer }), + }; + + const paramValues = Object.keys(params) + .map(key => `${key}=${params[key]}`) + .join('&'); + window.sendBeaconLite(`${atiURL}&${paramValues}`); } window.location.assign(nextPageUrl); From 2ccb4191ae19a3311367377c495e08e7022ca45d Mon Sep 17 00:00:00 2001 From: Shayne Marc Enzo Ahchoon Date: Mon, 17 Feb 2025 12:02:28 +0000 Subject: [PATCH 29/37] WSTEAM1-1514: Update --- src/app/hooks/useClickTrackerHandler/index.jsx | 7 ++++--- .../utilities/liteATIClickTracking/index.test.ts | 10 ++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/app/hooks/useClickTrackerHandler/index.jsx b/src/app/hooks/useClickTrackerHandler/index.jsx index ca54c151629..20cb4f34611 100644 --- a/src/app/hooks/useClickTrackerHandler/index.jsx +++ b/src/app/hooks/useClickTrackerHandler/index.jsx @@ -12,7 +12,8 @@ import { isValidClick } from './clickTypes'; const EVENT_TYPE = 'click'; export const LITE_ATI_TRACKING = 'data-lite-ati-tracking'; -const useExtractTrackingProps = (props = {}) => { +const extractTrackingProps = (props = {}) => { + // eslint-disable-next-line react-hooks/rules-of-hooks const eventTrackingContext = useContext(EventTrackingContext); const { componentName, url, advertiserID, format, detailedPlacement } = props; @@ -48,7 +49,7 @@ const useClickTrackerHandler = (props = {}) => { advertiserID, url, detailedPlacement, - } = useExtractTrackingProps(props); + } = extractTrackingProps(props); const preventNavigation = props?.preventNavigation; const optimizely = props?.optimizely; @@ -164,7 +165,7 @@ const useClickTrackerHandler = (props = {}) => { }; export const useConstructLiteSiteATIEventTrackUrl = (props = {}) => { - const atiTrackingParams = useExtractTrackingProps(props); + const atiTrackingParams = extractTrackingProps(props); const atiClickTrackingUrl = buildATIEventTrackUrl(atiTrackingParams); return atiClickTrackingUrl; }; diff --git a/src/server/utilities/liteATIClickTracking/index.test.ts b/src/server/utilities/liteATIClickTracking/index.test.ts index d4cef9ae300..c0b206142a3 100644 --- a/src/server/utilities/liteATIClickTracking/index.test.ts +++ b/src/server/utilities/liteATIClickTracking/index.test.ts @@ -94,7 +94,7 @@ describe('Click tracking script', () => { const callParam = (window.sendBeaconLite as jest.Mock).mock.calls[0][0]; const parsedATIParams = Object.fromEntries(new URLSearchParams(callParam)); - expect(parsedATIParams.idclient).toBe(undefined); + expect(parsedATIParams.idclient).toBeUndefined(); }); it('Sets a new cookie if there is no atuserid cookie on the user browser', () => { @@ -117,14 +117,16 @@ describe('Click tracking script', () => { hasliteSiteTracking: true, }); - const oldCookieId = '22ea8f97e5-4c34-4d23-af1d-4d1789206639'; - document.cookie = `atuserid=%7B%22name%22%3A%22atuserid%22%2C%22val%22%3A%${oldCookieId}%22%2C%22options%22%3A%7B%22end%22%3A%222026-03-11T10%3A23%3A55.442Z%22%2C%22path%22%3A%22%2F%22%7D%7D; path=/; max-age=397; Secure;`; + const oldCookieId = 'oldCookieId'; + document.cookie = `atuserid=%7B%22name%22%3A%22atuserid%22%2C%22val%22%3A%22${oldCookieId}%22%2C%22options%22%3A%7B%22end%22%3A%222026-03-11T10%3A23%3A55.442Z%22%2C%22path%22%3A%22%2F%22%7D%7D; path=/; max-age=397; Secure;`; (crypto.randomUUID as jest.Mock).mockReturnValueOnce('newCookieId'); dispatchClick(anchorElement); + const callParam = (window.sendBeaconLite as jest.Mock).mock.calls[0][0]; expect(document.cookie).toBe( - `atuserid=%7B%22name%22%3A%22atuserid%22%2C%22val%22%3A%${oldCookieId}%22%2C%22options%22%3A%7B%22end%22%3A%222026-03-11T10%3A23%3A55.442Z%22%2C%22path%22%3A%22%2F%22%7D%7D; path=/; max-age=397; Secure;`, + `atuserid=%7B%22name%22%3A%22atuserid%22%2C%22val%22%3A%22${oldCookieId}%22%2C%22options%22%3A%7B%22end%22%3A%222026-03-11T10%3A23%3A55.442Z%22%2C%22path%22%3A%22%2F%22%7D%7D; path=/; max-age=397; Secure;`, ); + expect(callParam).toContain('idclient=oldCookieId'); }); it('Reuses the atuserid cookie if there is a atuserid cookie on the user browser', () => { From 07ff7884ab480573942cfab0fc9f7a9baef57fba Mon Sep 17 00:00:00 2001 From: Shayne Marc Enzo Ahchoon Date: Mon, 17 Feb 2025 13:20:36 +0000 Subject: [PATCH 30/37] WSTEAM1-1514: Update --- .../hooks/useClickTrackerHandler/index.jsx | 17 +++++++++------- .../liteATIClickTracking/index.test.ts | 20 +++++++++---------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/app/hooks/useClickTrackerHandler/index.jsx b/src/app/hooks/useClickTrackerHandler/index.jsx index 20cb4f34611..06d320b132c 100644 --- a/src/app/hooks/useClickTrackerHandler/index.jsx +++ b/src/app/hooks/useClickTrackerHandler/index.jsx @@ -17,8 +17,13 @@ const extractTrackingProps = (props = {}) => { const eventTrackingContext = useContext(EventTrackingContext); const { componentName, url, advertiserID, format, detailedPlacement } = props; - const { pageIdentifier, platform, producerId, statsDestination } = - eventTrackingContext; + const { + pageIdentifier, + platform, + producerId, + statsDestination, + producerName, + } = eventTrackingContext; const campaignID = props?.campaignID || eventTrackingContext?.campaignID; @@ -34,6 +39,7 @@ const extractTrackingProps = (props = {}) => { advertiserID, url, detailedPlacement, + producerName, }; }; @@ -49,6 +55,7 @@ const useClickTrackerHandler = (props = {}) => { advertiserID, url, detailedPlacement, + producerName, } = extractTrackingProps(props); const preventNavigation = props?.preventNavigation; @@ -57,9 +64,6 @@ const useClickTrackerHandler = (props = {}) => { const { trackingIsEnabled } = useTrackingToggle(componentName); const [clicked, setClicked] = useState(false); - const eventTrackingContext = useContext(EventTrackingContext); - - const { producerName } = eventTrackingContext; const { service, useReverb } = useContext(ServiceContext); @@ -166,8 +170,7 @@ const useClickTrackerHandler = (props = {}) => { export const useConstructLiteSiteATIEventTrackUrl = (props = {}) => { const atiTrackingParams = extractTrackingProps(props); - const atiClickTrackingUrl = buildATIEventTrackUrl(atiTrackingParams); - return atiClickTrackingUrl; + return buildATIEventTrackUrl(atiTrackingParams); }; export const useATIClickTrackerHandler = (props = {}) => { diff --git a/src/server/utilities/liteATIClickTracking/index.test.ts b/src/server/utilities/liteATIClickTracking/index.test.ts index c0b206142a3..f80b9056e6a 100644 --- a/src/server/utilities/liteATIClickTracking/index.test.ts +++ b/src/server/utilities/liteATIClickTracking/index.test.ts @@ -2,15 +2,15 @@ import { LITE_ATI_TRACKING } from '#app/hooks/useClickTrackerHandler'; import liteATIClickTracking from '.'; const createAnchor = ({ - href = '/', - hasliteSiteTracking, + href = '/gahuza', + isLite, }: { href?: string; - hasliteSiteTracking: boolean; + isLite: boolean; }) => { const anchorElement = document.createElement('a'); anchorElement.href = href; - if (hasliteSiteTracking) { + if (isLite) { anchorElement.setAttribute( LITE_ATI_TRACKING, 'https://logws1363.ati-host.net/?', @@ -72,7 +72,7 @@ describe('Click tracking script', () => { it('Does not call sendBeacon if the event has no data-ati-tracking parameter, but still redirects', () => { const anchorElement = createAnchor({ href: '/gahuza', - hasliteSiteTracking: false, + isLite: false, }); dispatchClick(anchorElement); @@ -84,7 +84,7 @@ describe('Click tracking script', () => { it('Does not add userId cookie if crypto is unsupported, but still calls sendBeacon', () => { const anchorElement = createAnchor({ - hasliteSiteTracking: true, + isLite: true, }); // @ts-expect-error Some browsers may not have crypto. @@ -99,7 +99,7 @@ describe('Click tracking script', () => { it('Sets a new cookie if there is no atuserid cookie on the user browser', () => { const anchorElement = createAnchor({ - hasliteSiteTracking: true, + isLite: true, }); (crypto.randomUUID as jest.Mock).mockReturnValueOnce('randomUniqueId'); @@ -114,7 +114,7 @@ describe('Click tracking script', () => { it('Does not overwrite content in atuserid cookie if it already exists', () => { const anchorElement = createAnchor({ - hasliteSiteTracking: true, + isLite: true, }); const oldCookieId = 'oldCookieId'; @@ -131,7 +131,7 @@ describe('Click tracking script', () => { it('Reuses the atuserid cookie if there is a atuserid cookie on the user browser', () => { const anchorElement = createAnchor({ - hasliteSiteTracking: true, + isLite: true, }); document.cookie = @@ -145,7 +145,7 @@ describe('Click tracking script', () => { it('Calls sendBeaconLite() with the correct url', () => { const anchorElement = createAnchor({ - hasliteSiteTracking: true, + isLite: true, }); document.cookie = From 69f58539aa1d859c438660622269edf9beeef06c Mon Sep 17 00:00:00 2001 From: Shayne Marc Enzo Ahchoon Date: Mon, 17 Feb 2025 14:13:00 +0000 Subject: [PATCH 31/37] WORLDSERVICE-92: Add ATI click tracking to Lite Site CTA --- src/app/components/LiteSiteCta/index.test.tsx | 10 ++++++++++ src/app/components/LiteSiteCta/index.tsx | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/src/app/components/LiteSiteCta/index.test.tsx b/src/app/components/LiteSiteCta/index.test.tsx index 895c91658a3..8c47414a08e 100644 --- a/src/app/components/LiteSiteCta/index.test.tsx +++ b/src/app/components/LiteSiteCta/index.test.tsx @@ -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(, { isLite: true }); + + const [ctaLink] = container.querySelectorAll('a'); + const atiUrl = ctaLink.getAttribute('data-lite-ati-tracking'); + + expect(atiUrl).toContain('lite-site-cta'); + }); }); diff --git a/src/app/components/LiteSiteCta/index.tsx b/src/app/components/LiteSiteCta/index.tsx index 8a06d3207e9..0376a39f844 100644 --- a/src/app/components/LiteSiteCta/index.tsx +++ b/src/app/components/LiteSiteCta/index.tsx @@ -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'; @@ -17,6 +18,7 @@ type CtaLinkProps = { showChevron?: boolean; ignoreLiteExtension?: boolean; className?: string; + useClickHandler?: boolean; }; const CtaLink = ({ @@ -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 ? ( ) : ( @@ -40,6 +47,7 @@ const CtaLink = ({ className={className} css={styles.link} {...(ignoreLiteExtension && { 'data-ignore-lite': true })} + {...(useClickHandler && atiClickTrackerHandler)} > {text} @@ -85,6 +93,7 @@ const LiteSiteCta = () => { text={toMainSite} css={styles.topLinkSpacing} ignoreLiteExtension + useClickHandler showChevron /> From 3e8d696965aea6ea129e4b9fd07342d513f914a3 Mon Sep 17 00:00:00 2001 From: Shayne Marc Enzo Ahchoon Date: Mon, 17 Feb 2025 14:31:27 +0000 Subject: [PATCH 32/37] WSTEAM1-1514: Update --- .../liteATIClickTracking/index.test.ts | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/server/utilities/liteATIClickTracking/index.test.ts b/src/server/utilities/liteATIClickTracking/index.test.ts index f80b9056e6a..de14c58da0a 100644 --- a/src/server/utilities/liteATIClickTracking/index.test.ts +++ b/src/server/utilities/liteATIClickTracking/index.test.ts @@ -3,10 +3,10 @@ import liteATIClickTracking from '.'; const createAnchor = ({ href = '/gahuza', - isLite, + isLite = true, }: { href?: string; - isLite: boolean; + isLite?: boolean; }) => { const anchorElement = document.createElement('a'); anchorElement.href = href; @@ -83,9 +83,7 @@ describe('Click tracking script', () => { }); it('Does not add userId cookie if crypto is unsupported, but still calls sendBeacon', () => { - const anchorElement = createAnchor({ - isLite: true, - }); + const anchorElement = createAnchor({}); // @ts-expect-error Some browsers may not have crypto. // eslint-disable-next-line no-global-assign @@ -98,9 +96,7 @@ describe('Click tracking script', () => { }); it('Sets a new cookie if there is no atuserid cookie on the user browser', () => { - const anchorElement = createAnchor({ - isLite: true, - }); + const anchorElement = createAnchor({}); (crypto.randomUUID as jest.Mock).mockReturnValueOnce('randomUniqueId'); dispatchClick(anchorElement); @@ -113,9 +109,7 @@ describe('Click tracking script', () => { }); it('Does not overwrite content in atuserid cookie if it already exists', () => { - const anchorElement = createAnchor({ - isLite: true, - }); + const anchorElement = createAnchor({}); const oldCookieId = 'oldCookieId'; document.cookie = `atuserid=%7B%22name%22%3A%22atuserid%22%2C%22val%22%3A%22${oldCookieId}%22%2C%22options%22%3A%7B%22end%22%3A%222026-03-11T10%3A23%3A55.442Z%22%2C%22path%22%3A%22%2F%22%7D%7D; path=/; max-age=397; Secure;`; @@ -130,9 +124,7 @@ describe('Click tracking script', () => { }); it('Reuses the atuserid cookie if there is a atuserid cookie on the user browser', () => { - const anchorElement = createAnchor({ - isLite: true, - }); + const anchorElement = createAnchor({}); document.cookie = 'atuserid={"val":"oldCookieId"}; path=/; max-age=397; Secure;'; @@ -144,9 +136,7 @@ describe('Click tracking script', () => { }); it('Calls sendBeaconLite() with the correct url', () => { - const anchorElement = createAnchor({ - isLite: true, - }); + const anchorElement = createAnchor({}); document.cookie = 'atuserid={"val":"userCookieId"}; path=/; max-age=397; Secure;'; From b0895f592cf1a63573292e0facfb4d49b9d588aa Mon Sep 17 00:00:00 2001 From: Shayne Marc Enzo Ahchoon Date: Mon, 17 Feb 2025 15:00:02 +0000 Subject: [PATCH 33/37] WORLDSERVICE-92: Update snapshots --- .../articles/gahuzaLiteSite/__snapshots__/lite.test.js.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/src/integration/pages/articles/gahuzaLiteSite/__snapshots__/lite.test.js.snap b/src/integration/pages/articles/gahuzaLiteSite/__snapshots__/lite.test.js.snap index 2d7ab8a0dd9..db28c77ed0a 100644 --- a/src/integration/pages/articles/gahuzaLiteSite/__snapshots__/lite.test.js.snap +++ b/src/integration/pages/articles/gahuzaLiteSite/__snapshots__/lite.test.js.snap @@ -29,6 +29,7 @@ exports[`Lite Site Articles Lite Site Cta should match snapshot 1`] = ` Date: Tue, 18 Feb 2025 10:54:04 +0000 Subject: [PATCH 34/37] WORLDSERVICE-92: Adds for loop to handle nested elements --- .../utilities/liteATIClickTracking/index.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/server/utilities/liteATIClickTracking/index.ts b/src/server/utilities/liteATIClickTracking/index.ts index 3eea80d53e4..b7f6051b01f 100644 --- a/src/server/utilities/liteATIClickTracking/index.ts +++ b/src/server/utilities/liteATIClickTracking/index.ts @@ -1,14 +1,26 @@ export default () => { window.addEventListener('load', () => { document.addEventListener('click', (event: MouseEvent) => { - const targetElement = event.target as HTMLElement; + let targetElement; + const clickedElement = event.target as HTMLElement; + let currentElement = clickedElement; + for ( + ; + currentElement; + currentElement = currentElement.parentElement as HTMLElement + ) { + if (currentElement.tagName === 'A') { + targetElement = currentElement; + break; + } + } if (targetElement?.tagName === 'A') { event.stopPropagation(); event.preventDefault(); const atiURL = targetElement.getAttribute('data-lite-ati-tracking'); - const currentAnchorElement = event.target as HTMLAnchorElement; - const nextPageUrl = currentAnchorElement?.href; + const anchorElement = targetElement as HTMLAnchorElement; + const nextPageUrl = anchorElement?.href; if (atiURL) { const { From e74929c31c44d8eddfa36f6ba7a95256c09ffcf2 Mon Sep 17 00:00:00 2001 From: Shayne Marc Enzo Ahchoon Date: Tue, 18 Feb 2025 11:39:50 +0000 Subject: [PATCH 35/37] WORLDSERVICE-92: Change beacon request to synchronous. --- src/app/components/ATIAnalytics/canonical/sendBeaconLite.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/ATIAnalytics/canonical/sendBeaconLite.ts b/src/app/components/ATIAnalytics/canonical/sendBeaconLite.ts index 0cfce17ec0c..5e3863114cc 100644 --- a/src/app/components/ATIAnalytics/canonical/sendBeaconLite.ts +++ b/src/app/components/ATIAnalytics/canonical/sendBeaconLite.ts @@ -1,7 +1,7 @@ const sendBeaconLite = (atiPageViewUrlString: string) => ` function sendBeaconLite (atiPageViewUrlString) { var xhr = new XMLHttpRequest(); - xhr.open("GET", atiPageViewUrlString, true); + xhr.open("GET", atiPageViewUrlString, false); xhr.withCredentials = true; xhr.send(); } From 7a5d95ab0c12171005d343c5bea11980d89995c4 Mon Sep 17 00:00:00 2001 From: Shayne Marc Enzo Ahchoon Date: Tue, 18 Feb 2025 12:02:33 +0000 Subject: [PATCH 36/37] WORLDSERVICE-92: Update. --- src/app/components/ATIAnalytics/canonical/index.test.tsx | 2 +- .../components/ATIAnalytics/canonical/sendBeaconLite.test.ts | 2 +- src/server/utilities/liteATIClickTracking/index.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/components/ATIAnalytics/canonical/index.test.tsx b/src/app/components/ATIAnalytics/canonical/index.test.tsx index ca43428bd29..1e4787b8377 100644 --- a/src/app/components/ATIAnalytics/canonical/index.test.tsx +++ b/src/app/components/ATIAnalytics/canonical/index.test.tsx @@ -49,7 +49,7 @@ describe('Canonical ATI Analytics', () => { expect(helmet.scriptTags[0].innerHTML).toEqual(` function sendBeaconLite (atiPageViewUrlString) { var xhr = new XMLHttpRequest(); - xhr.open("GET", atiPageViewUrlString, true); + xhr.open("GET", atiPageViewUrlString, false); xhr.withCredentials = true; xhr.send(); } diff --git a/src/app/components/ATIAnalytics/canonical/sendBeaconLite.test.ts b/src/app/components/ATIAnalytics/canonical/sendBeaconLite.test.ts index 95d2c3a85b6..4622c48902a 100644 --- a/src/app/components/ATIAnalytics/canonical/sendBeaconLite.test.ts +++ b/src/app/components/ATIAnalytics/canonical/sendBeaconLite.test.ts @@ -29,7 +29,7 @@ describe('sendBeaconLite', () => { expect(XMLHttpRequestMock.open).toHaveBeenCalledWith( 'GET', 'https://foobar.com', - true, + false, ); }); }); diff --git a/src/server/utilities/liteATIClickTracking/index.ts b/src/server/utilities/liteATIClickTracking/index.ts index b7f6051b01f..52b61b39736 100644 --- a/src/server/utilities/liteATIClickTracking/index.ts +++ b/src/server/utilities/liteATIClickTracking/index.ts @@ -88,6 +88,7 @@ export default () => { const paramValues = Object.keys(params) .map(key => `${key}=${params[key]}`) .join('&'); + window.sendBeaconLite(`${atiURL}&${paramValues}`); } From fba1ad44a82cc5c5cbbeed39373a9907e7be4dd4 Mon Sep 17 00:00:00 2001 From: Shayne Marc Enzo Ahchoon Date: Tue, 18 Feb 2025 14:06:58 +0000 Subject: [PATCH 37/37] WORLDSERVICE-92: Update. --- src/app/components/ATIAnalytics/canonical/index.test.tsx | 2 +- .../ATIAnalytics/canonical/sendBeaconLite.test.ts | 2 +- .../components/ATIAnalytics/canonical/sendBeaconLite.ts | 2 +- src/server/utilities/liteATIClickTracking/index.ts | 9 ++++----- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/app/components/ATIAnalytics/canonical/index.test.tsx b/src/app/components/ATIAnalytics/canonical/index.test.tsx index 1e4787b8377..ca43428bd29 100644 --- a/src/app/components/ATIAnalytics/canonical/index.test.tsx +++ b/src/app/components/ATIAnalytics/canonical/index.test.tsx @@ -49,7 +49,7 @@ describe('Canonical ATI Analytics', () => { expect(helmet.scriptTags[0].innerHTML).toEqual(` function sendBeaconLite (atiPageViewUrlString) { var xhr = new XMLHttpRequest(); - xhr.open("GET", atiPageViewUrlString, false); + xhr.open("GET", atiPageViewUrlString, true); xhr.withCredentials = true; xhr.send(); } diff --git a/src/app/components/ATIAnalytics/canonical/sendBeaconLite.test.ts b/src/app/components/ATIAnalytics/canonical/sendBeaconLite.test.ts index 4622c48902a..95d2c3a85b6 100644 --- a/src/app/components/ATIAnalytics/canonical/sendBeaconLite.test.ts +++ b/src/app/components/ATIAnalytics/canonical/sendBeaconLite.test.ts @@ -29,7 +29,7 @@ describe('sendBeaconLite', () => { expect(XMLHttpRequestMock.open).toHaveBeenCalledWith( 'GET', 'https://foobar.com', - false, + true, ); }); }); diff --git a/src/app/components/ATIAnalytics/canonical/sendBeaconLite.ts b/src/app/components/ATIAnalytics/canonical/sendBeaconLite.ts index 5e3863114cc..0cfce17ec0c 100644 --- a/src/app/components/ATIAnalytics/canonical/sendBeaconLite.ts +++ b/src/app/components/ATIAnalytics/canonical/sendBeaconLite.ts @@ -1,7 +1,7 @@ const sendBeaconLite = (atiPageViewUrlString: string) => ` function sendBeaconLite (atiPageViewUrlString) { var xhr = new XMLHttpRequest(); - xhr.open("GET", atiPageViewUrlString, false); + xhr.open("GET", atiPageViewUrlString, true); xhr.withCredentials = true; xhr.send(); } diff --git a/src/server/utilities/liteATIClickTracking/index.ts b/src/server/utilities/liteATIClickTracking/index.ts index 52b61b39736..21ced4a25c6 100644 --- a/src/server/utilities/liteATIClickTracking/index.ts +++ b/src/server/utilities/liteATIClickTracking/index.ts @@ -3,17 +3,16 @@ export default () => { document.addEventListener('click', (event: MouseEvent) => { let targetElement; const clickedElement = event.target as HTMLElement; + let currentElement = clickedElement; - for ( - ; - currentElement; - currentElement = currentElement.parentElement as HTMLElement - ) { + while (currentElement) { if (currentElement.tagName === 'A') { targetElement = currentElement; break; } + currentElement = currentElement.parentElement as HTMLElement; } + if (targetElement?.tagName === 'A') { event.stopPropagation(); event.preventDefault();