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-1444: Adds Chartbeat to Live Pages With Refactor of SPA Functionality #11579

Open
wants to merge 35 commits into
base: latest
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1bdba7d
Lite page Chartbeat initial impl
amoore108 Apr 30, 2024
a4749f3
type fix
amoore108 Apr 30, 2024
bae4eed
add test
amoore108 Apr 30, 2024
a1791d0
Update index.test.tsx
amoore108 May 1, 2024
243dc74
Test name update
amoore108 May 1, 2024
a9f1814
Merge branch 'latest' into lite-chartbeat-initial-impl
amoore108 May 1, 2024
3a86b31
Merge branch 'latest' into lite-chartbeat-initial-impl
amoore108 May 1, 2024
37a7d83
Merge branch 'latest' into lite-chartbeat-initial-impl
amoore108 May 1, 2024
82dffeb
Merge branch 'latest' into lite-chartbeat-initial-impl
amoore108 May 2, 2024
7651ec0
Merge branch 'latest' into lite-chartbeat-initial-impl
amoore108 May 2, 2024
35f7a39
Merge branch 'latest' into lite-chartbeat-initial-impl
amoore108 May 3, 2024
7e9339c
Merge branch 'latest' into lite-chartbeat-initial-impl
amoore108 May 7, 2024
bba40cc
Merge branch 'latest' into lite-chartbeat-initial-impl
amoore108 May 7, 2024
7649bef
Merge branch 'latest' into lite-chartbeat-initial-impl
amoore108 May 8, 2024
1e92335
ts fix
amoore108 May 8, 2024
70961f0
Merge branch 'latest' into lite-chartbeat-initial-impl
amoore108 May 9, 2024
c306132
Merge branch 'latest' into lite-chartbeat-initial-impl
amoore108 May 13, 2024
72d5fa1
Merge branch 'latest' into lite-chartbeat-initial-impl
amoore108 May 14, 2024
0de8e29
Merge branch 'latest' into lite-chartbeat-initial-impl
amoore108 May 20, 2024
7351a30
Merge branch 'latest' into lite-chartbeat-initial-impl
amoore108 Jan 22, 2025
e243159
Merge branch 'latest' into lite-chartbeat-initial-impl
HarveyPeachey Jan 27, 2025
fdc4f47
Merge branch 'latest' into lite-chartbeat-initial-impl
HarveyPeachey Jan 27, 2025
195cc2f
moves canonical chartbeat logic from userContext
HarveyPeachey Jan 27, 2025
d2ca32e
removes virtual page tracking
HarveyPeachey Jan 27, 2025
89f0ce3
Merge branch 'latest' into lite-chartbeat-initial-impl
HarveyPeachey Jan 27, 2025
8f914a5
remove unused imports
HarveyPeachey Jan 27, 2025
5a58961
removes chartbeat test
HarveyPeachey Jan 27, 2025
400314b
updates bundle size
HarveyPeachey Jan 27, 2025
26c8a03
adjusts bundle size again
HarveyPeachey Jan 27, 2025
1b6a637
updates test
HarveyPeachey Jan 27, 2025
53b392b
Merge branch 'latest' into lite-chartbeat-initial-impl
HarveyPeachey Jan 27, 2025
b06fa6f
updates snapshot
HarveyPeachey Jan 28, 2025
f545a6f
removes superfly reference
HarveyPeachey Jan 28, 2025
d591adf
fixes test typo
HarveyPeachey Jan 28, 2025
cf65c07
Update src/app/components/ChartbeatAnalytics/index.test.tsx
HarveyPeachey Jan 28, 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
4 changes: 2 additions & 2 deletions scripts/bundleSize/bundleSizeConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
* We are allowing a variance of -5 on `MIN_SIZE` and +5 on `MAX_SIZE` to avoid the need for frequent changes, as bundle sizes can fluctuate
*/

export const MIN_SIZE = 646 - 5;
export const MAX_SIZE = 1185 + 5;
export const MIN_SIZE = 640 - 5;
export const MAX_SIZE = 1179 + 5;
154 changes: 3 additions & 151 deletions src/app/components/ChartbeatAnalytics/canonical/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { render, act, waitFor } from '@testing-library/react';
import { render } from '@testing-library/react';
import CanonicalChartbeatAnalytics from '.';
import { CanonicalChartbeatConfig } from '../types';

describe('CanonicalChartbeatAnalytics', () => {
// @ts-expect-error chartbeat requires pSUPERFLY object on global window
global.pSUPERFLY = {
virtualPage: jest.fn(),
};

afterEach(jest.clearAllMocks);

// @ts-expect-error partial data for testing purposes
const pageAConfig: CanonicalChartbeatConfig = {
const pageConfig: CanonicalChartbeatConfig = {
domain: 'test-domain',
sections: 'section1 section2',
virtualReferrer: null,
Expand All @@ -22,155 +16,13 @@ describe('CanonicalChartbeatAnalytics', () => {
uid: 123,
};

const pageBConfig = {
chartbeatConfig: {
domain: 'test-domain',
type: 'article',
sections: 'section1 section2',
chartbeatUID: 1111,
useCanonical: true,
virtualReferrer: '/page-A',
title: 'Page B',
uid: 123,
},
};

const pageCConfig = {
chartbeatConfig: {
domain: 'test-domain',
type: 'article',
sections: 'section1 section2',
chartbeatUID: 1111,
useCanonical: true,
virtualReferrer: '/page-B',
title: 'Page C',
uid: 123,
},
};

it('should return the helmet wrapper with the script snippet', () => {
render(
<CanonicalChartbeatAnalytics
chartbeatConfig={pageAConfig}
chartbeatConfig={pageConfig}
chartbeatSource="//chartbeat.js"
/>,
);
expect(Helmet.peek().scriptTags).toMatchSnapshot();
});

it('should not re-render helment wrapper when config changes to Page B', async () => {
andrewscfc marked this conversation as resolved.
Show resolved Hide resolved
const { rerender } = render(
<CanonicalChartbeatAnalytics
chartbeatConfig={pageAConfig}
chartbeatSource="//chartbeat.js"
/>,
);

await act(async () => {
rerender(
<CanonicalChartbeatAnalytics
chartbeatConfig={pageBConfig.chartbeatConfig}
chartbeatSource="//chartbeat.js"
/>,
);
});

await waitFor(() => {
expect(document.querySelectorAll('script')[0].textContent).toMatch(
/"title":"Page A"/,
);
});
});

it('should call the global virtualPage function when props change', async () => {
const { rerender } = render(
<CanonicalChartbeatAnalytics
chartbeatConfig={pageAConfig}
chartbeatSource="//chartbeat.js"
/>,
);

await act(async () => {
rerender(
<CanonicalChartbeatAnalytics
chartbeatConfig={{
domain: 'test-domain',
type: 'article',
sections: 'section1 section2',
useCanonical: true,
virtualReferrer: '/page-A',
title: 'Page B',
uid: 123,
}}
chartbeatSource="//chartbeat.js"
/>,
);
});

await act(async () => {
// unmount
rerender(<div />);
});

// @ts-expect-error chartbeat requires pSUPERFLY object on global window
expect(global.pSUPERFLY.virtualPage).toHaveBeenCalled();
// @ts-expect-error chartbeat requires pSUPERFLY object on global window
expect(global.pSUPERFLY.virtualPage).toHaveBeenCalledWith({
domain: 'test-domain',
type: 'article',
sections: 'section1 section2',
useCanonical: true,
virtualReferrer: '/page-A',
title: 'Page B',
uid: 123,
});
});

it('should report correct virtual referrer when user navigates back from onward journey', async () => {
// initial PWA load (Page A)
const { rerender } = render(
<CanonicalChartbeatAnalytics
chartbeatConfig={pageAConfig}
chartbeatSource="//chartbeat.js"
/>,
);

// inline link to Page B
await act(async () => {
rerender(
<CanonicalChartbeatAnalytics
chartbeatConfig={pageBConfig.chartbeatConfig}
chartbeatSource="//chartbeat.js"
/>,
);
});

// inline link to Page C
await act(async () => {
rerender(
<CanonicalChartbeatAnalytics
chartbeatConfig={pageCConfig.chartbeatConfig}
chartbeatSource="//chartbeat.js"
/>,
);
});

// press back, return to Page B
await act(async () => {
rerender(
<CanonicalChartbeatAnalytics
chartbeatConfig={pageBConfig.chartbeatConfig}
chartbeatSource="//chartbeat.js"
/>,
);
});

const [[firstCall], [secondCall], [thirdCall]] =
// @ts-expect-error chartbeat requires pSUPERFLY object on global window
global.pSUPERFLY.virtualPage.mock.calls;

expect(firstCall.virtualReferrer).toEqual('/page-A');
expect(secondCall.virtualReferrer).toEqual('/page-B');
expect(thirdCall.virtualReferrer).toEqual('/page-A');
});
});
35 changes: 10 additions & 25 deletions src/app/components/ChartbeatAnalytics/canonical/index.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,26 @@
import React, { useEffect, useRef, useState } from 'react';
import React from 'react';
import { Helmet } from 'react-helmet';
import { CanonicalChartbeatProps } from '../types';
import { chartbeatSource as defaultChartbeatSource } from '../utils';

const CanonicalChartbeatBeacon = ({
chartbeatConfig,
chartbeatSource = defaultChartbeatSource,
}: CanonicalChartbeatProps) => {
const [firstLoadConfig] = useState(chartbeatConfig);

const hasMounted = useRef(false);

useEffect(() => {
// @ts-expect-error chartbeat requires pSUPERFLY object on global window
if (hasMounted.current && window.pSUPERFLY) {
// @ts-expect-error chartbeat requires pSUPERFLY object on global window
window.pSUPERFLY.virtualPage(chartbeatConfig);
}
hasMounted.current = true;
}, [chartbeatConfig]);

return (
<Helmet>
<script async type="text/javascript">
{`
}: CanonicalChartbeatProps) => (
<Helmet>
<script async type="text/javascript">
{`
(function(){
var _sf_async_config = window._sf_async_config = (window._sf_async_config || {});
var config = ${JSON.stringify(firstLoadConfig)};
var config = ${JSON.stringify(chartbeatConfig)};
for (var key in config) {
_sf_async_config[key] = config[key];
}
})();
`}
</script>
<script defer type="text/javascript" src={chartbeatSource} />
</Helmet>
);
};
</script>
<script defer type="text/javascript" src={chartbeatSource} />
</Helmet>
);

export default CanonicalChartbeatBeacon;
30 changes: 18 additions & 12 deletions src/app/components/ChartbeatAnalytics/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { PropsWithChildren, useMemo } from 'react';
import { render } from '@testing-library/react';
import { Helmet } from 'react-helmet';
import { ARTICLE_PAGE } from '../../routes/utils/pageTypes';
import { RequestContextProvider } from '../../contexts/RequestContext';
import { ServiceContextProvider } from '../../contexts/ServiceContext';
Expand All @@ -25,6 +26,7 @@ interface Props {
pageType: PageTypes;
platform: Platforms;
origin: string;
isLite?: boolean;
toggleState?: {
chartbeatAnalytics: {
enabled: boolean;
Expand All @@ -38,6 +40,7 @@ const ContextWrap = ({
platform,
origin,
children,
isLite = false,
toggleState = defaultToggleState,
}: PropsWithChildren<Props>) => {
const memoizedToggleContextValue = useMemo(
Expand All @@ -51,6 +54,7 @@ const ContextWrap = ({
);
return (
<RequestContextProvider
isLite={isLite}
isAmp={platform === 'amp'}
isApp={false}
pageType={pageType}
Expand All @@ -74,6 +78,9 @@ const ContextWrap = ({
};

describe('Charbeats Analytics Container', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should call AmpCharbeatsBeacon when platform is amp and toggle enabled for chartbeat on live', () => {
process.env.SIMORGH_APP_ENV = 'live';
const mockAmp = jest.fn().mockReturnValue('amp-return-value');
Expand All @@ -87,7 +94,6 @@ describe('Charbeats Analytics Container', () => {
domain: 'news-domain',
sections: 'secction1 section2',
contentType: 'article',
virtualReferrer: '/some-path',
title: 'This is an article',
};

Expand Down Expand Up @@ -126,7 +132,7 @@ describe('Charbeats Analytics Container', () => {
expect(container.firstChild?.textContent).toEqual('amp-return-value');
});

it('should return null when toggle is disbaled for live', () => {
it('should return null when toggle is disabled for live', () => {
const toggleState = {
chartbeatAnalytics: {
enabled: false,
Expand Down Expand Up @@ -167,24 +173,20 @@ describe('Charbeats Analytics Container', () => {
expect(container.firstChild).toBeNull();
});

it('should call sendCanonicalChartbeatBeacon when platform is canonical, and toggle enabled for chartbeat on test', () => {
andrewscfc marked this conversation as resolved.
Show resolved Hide resolved
it('should add Chartbeat Helmet script with correct config when platform is canonical, toggle enabled and on test in Lite mode', () => {
process.env.SIMORGH_APP_ENV = 'test';
const mockAmp = jest.fn().mockReturnValue('amp-return-value');
// @ts-expect-error requires mocking for testing purposes
amp.default = mockAmp;

const expectedConfig = {
uid: 50924,
domain: 'test-domain',
idSync: {
bbc_hid: 'cookie',
},
path: '/',
sections: 'secction1 section2',
path: '/pidgin/articles/c00000000o.lite',
sections: 'section1 section2',
title: 'This is a canonical page article',
type: 'article',
useCanonical: true,
virtualReferrer: '/some-path',
};

const toggleState = {
Expand All @@ -198,6 +200,7 @@ describe('Charbeats Analytics Container', () => {
testUtils.getConfig = mockGetConfig;
render(
<ContextWrap
isLite
platform="canonical"
pageType={ARTICLE_PAGE}
origin="test.bbc.com"
Expand All @@ -210,9 +213,12 @@ describe('Charbeats Analytics Container', () => {
</ContextWrap>,
);

expect(sendCanonicalChartbeatBeacon).toHaveBeenCalledTimes(1);
expect(sendCanonicalChartbeatBeacon).toHaveBeenCalledWith(expectedConfig);
const [inlineScript] = Helmet.peek().scriptTags;
const scriptHtml = inlineScript.innerHTML;

expect(testUtils.getConfig).toHaveBeenCalledTimes(1);
expect(mockAmp).not.toHaveBeenCalled();
expect(scriptHtml).toContain(JSON.stringify(expectedConfig));

expect(sendCanonicalChartbeatBeacon).not.toHaveBeenCalled();
});
});
Loading