Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
a80f734
WS-1838 adding tracking flag
DmitryGron Dec 12, 2025
8649365
WS-1838 adding tracking flag
DmitryGron Dec 12, 2025
3f89eb2
Merge branch 'ws-1837-service-worker-changes' into feature/WS-1838-pw…
DmitryGron Dec 15, 2025
6548df5
Merge branch 'ws-1837-service-worker-changes' into feature/WS-1838-pw…
DmitryGron Dec 15, 2025
5a1cdf7
Merge branch 'ws-1837-service-worker-changes' into feature/WS-1838-pw…
elvinasv Dec 15, 2025
02a40fc
updating due to comments
DmitryGron Dec 16, 2025
14508da
Merge branch 'ws-1837-service-worker-changes' into feature/WS-1838-pw…
DmitryGron Dec 16, 2025
56b0671
small update for offline hooks
DmitryGron Dec 16, 2025
a73f35f
updating hooks offline tracking
DmitryGron Dec 16, 2025
c5cd73d
Merge branch 'ws-1837-service-worker-changes' into feature/WS-1838-pw…
elvinasv Dec 16, 2025
bc8c4c4
update offline flag to work properly
DmitryGron Dec 17, 2025
e58e685
removing nextjs static assets to avoid loop reloadand some other fixes
jinidev Dec 17, 2025
d5746f2
WS-1826-collapsible-nav-for-seo
SantaZena Dec 12, 2025
dfc23cd
Updating unit tests
SantaZena Dec 12, 2025
84d8a04
Updating unit tests nr2
SantaZena Dec 15, 2025
05f499b
Updating unit tests nr3
SantaZena Dec 15, 2025
0d2c345
Removing right line in top nav
SantaZena Dec 17, 2025
574424b
Fix onload condition for promo images & add blurred bg
DmitryGron Dec 18, 2025
55bfd7a
Add isPortraitImage condition back
hotinglok Dec 5, 2025
beb1dcb
Move css into separate component, apply changes to MAPs
hotinglok Dec 9, 2025
e5a6c30
Add changes to hierarchical grid & billboard, update selectors
hotinglok Dec 9, 2025
d67f6eb
Make blurred background image src smaller
hotinglok Dec 9, 2025
58be086
Fix invalid ichef image size
hotinglok Dec 9, 2025
7f73b3a
Prevent any fetching on lite site pages
hotinglok Dec 10, 2025
1425a86
Add stories, add isLite condition to LatestMediaSection
hotinglok Dec 11, 2025
12cb6ba
Add aria-hidden to BlurredBackground
hotinglok Dec 11, 2025
dd3157c
Remove onLoad fix
DmitryGron Dec 18, 2025
03a5daa
Linting
hotinglok Dec 11, 2025
30d0a40
Add fallback if ichef url fails
hotinglok Dec 11, 2025
9e77ae5
Update curations test
hotinglok Dec 11, 2025
021670f
Update integration test snapshots
hotinglok Dec 11, 2025
a0d3aed
Revert mundo fixture
hotinglok Dec 11, 2025
f102a75
Remove isLite prop drilling, add RequestContext to BlurredBackround c…
hotinglok Dec 15, 2025
e99bb0b
Linting
hotinglok Dec 15, 2025
4c4c63d
Fix stories
hotinglok Dec 15, 2025
af07b81
Fix src url
hotinglok Dec 15, 2025
58694d2
Fix LatestMediaSection styles
hotinglok Dec 15, 2025
c1c4c8b
Add dark filter over background to make image stand out more
hotinglok Dec 16, 2025
cc4acad
WS-1882 - Remove hover/focus colour on TopicTags (#13553)
amoore108 Dec 16, 2025
5c403f3
WS-NA: Removes trailing comma if final contibutor has no role or loca…
Isabella-Mitchell Dec 16, 2025
30f934f
added route for homepages as well as condition for page type header
DmitryGron Dec 18, 2025
c10fac2
added unit tests
Nabeel1276 Dec 11, 2025
fcec076
updated structure of data fetch
Nabeel1276 Dec 12, 2025
3e72ac6
called Homepage component to render it
DmitryGron Dec 18, 2025
e1be759
renamed folder to homepage and homepage return in derived page type
Nabeel1276 Dec 15, 2025
e1a633b
removed toggles
Nabeel1276 Dec 15, 2025
d198029
added variant support to derivePageType utility [copilot]
Nabeel1276 Dec 15, 2025
2cf4cdf
added unit tests for homepage derived type
Nabeel1276 Dec 15, 2025
dc4d691
removed unneccessary keys
Nabeel1276 Dec 15, 2025
48e0e4c
moved shouldRender function into utilites folder and updated imports
Nabeel1276 Dec 15, 2025
5dc4dec
removed redundant props
Nabeel1276 Dec 15, 2025
432c37f
updated shouldRender imports
Nabeel1276 Dec 15, 2025
d7e3b4b
fixed imports again
Nabeel1276 Dec 15, 2025
2dd0851
updated imports again
Nabeel1276 Dec 15, 2025
235a544
removed unused props
Nabeel1276 Dec 15, 2025
7743d12
reinstated required props
Nabeel1276 Dec 15, 2025
500f45d
updated cache control
Nabeel1276 Dec 16, 2025
d4070e7
updated unit tests
Nabeel1276 Dec 16, 2025
e074bf9
removed amp as it is not supported in home pages
Nabeel1276 Dec 16, 2025
7f66610
Add test
amoore108 Dec 16, 2025
b8e933d
updated readme
Nabeel1276 Dec 16, 2025
62cb94d
WS-1175: Removes optimizely dependency from article readtime
Isabella-Mitchell Dec 12, 2025
ff0e204
WS-1175: Refactor ReadTime component
Isabella-Mitchell Dec 12, 2025
d33ca26
WS-1175: Fix react issue
Isabella-Mitchell Dec 12, 2025
b4cc30a
WS-1175: Move translations check higher up the tree
Isabella-Mitchell Dec 12, 2025
a82cac6
WS-1175: Reverts passing translations as prop
Isabella-Mitchell Dec 12, 2025
d7d1e30
WS-1175: Tidy up code labelled with experiment comments
Isabella-Mitchell Dec 12, 2025
5f3bbb2
WS-1175: Fix stories and styles
Isabella-Mitchell Dec 12, 2025
93ac0fe
WS-1175: Tidies
Isabella-Mitchell Dec 12, 2025
48b8017
cache was missing
jinidev Dec 17, 2025
0f8261f
adding tests
DmitryGron Dec 18, 2025
fe4c417
Merge branch 'ws-1837-service-worker-changes' into feature/WS-1838-pw…
DmitryGron Dec 18, 2025
1b42ee9
Update index.tsx
DmitryGron Dec 18, 2025
e0b3d5f
Fix onload condition for promo images & add blurred bg
DmitryGron Dec 18, 2025
35c170a
Move css into separate component, apply changes to MAPs
DmitryGron Dec 18, 2025
622a621
added route for homepages as well as condition for page type header
DmitryGron Dec 18, 2025
8fbd336
renamed folder to homepage and homepage return in derived page type
DmitryGron Dec 18, 2025
9c3783a
removing unrelated
DmitryGron Dec 19, 2025
a6ddf13
removing unrelated
DmitryGron Dec 19, 2025
81614e5
removing unrelated
DmitryGron Dec 19, 2025
0db80e1
removing unrelated
DmitryGron Dec 19, 2025
02d6104
removing unrelated
DmitryGron Dec 19, 2025
edab507
updating audit
DmitryGron Dec 19, 2025
2529ceb
Merge branch 'ws-1837-service-worker-changes' into feature/WS-1838-pw…
DmitryGron Dec 19, 2025
78d336d
removing ref
DmitryGron Dec 19, 2025
b5383a6
Merge branch 'ws-1837-service-worker-changes' into feature/WS-1838-pw…
jinidev Dec 22, 2025
8f5bd54
simplefying offline tracking logic
DmitryGron Dec 22, 2025
6ec847a
upgraded the cachename in sw to see updated offlinepage
jinidev Dec 22, 2025
b03ca3f
putting logs and removed isONline check from useOfflinePageFlag hook
jinidev Dec 22, 2025
9b1cde5
sw changes for nextjs bundling/cache resorucing
jinidev Dec 22, 2025
d81aba2
updating test
DmitryGron Dec 22, 2025
0a72b7d
revert sw.js to original
jinidev Dec 22, 2025
2aa1f46
update testds
DmitryGron Dec 22, 2025
ee21809
Merge branch 'ws-1837-service-worker-changes' into feature/WS-1838-pw…
jinidev Dec 22, 2025
64fd026
Update src/app/hooks/usePWAOfflineTracking/index.test.tsx
DmitryGron Jan 2, 2026
d840930
updates due to comments
DmitryGron Jan 2, 2026
9ee5583
updates due to comments
DmitryGron Jan 2, 2026
07ad90d
updates due to comments
DmitryGron Jan 2, 2026
af8a64d
updates due to comments
DmitryGron Jan 2, 2026
f129d1d
Merge branch 'ws-1837-service-worker-changes' into feature/WS-1838-pw…
jinidev Jan 5, 2026
5c6fb54
Adding console logs to help debug event tracking issues
jinidev Jan 5, 2026
ca20f88
prefetching offline page in client side for js chunks pre-caching
jinidev Jan 5, 2026
52e6aa0
revert prefetch , remove filtering in cache of resources
jinidev Jan 6, 2026
8c7d64c
revert sw changes, removed logs
jinidev Jan 7, 2026
a9a165e
Merge branch 'ws-1837-service-worker-changes' into feature/WS-1838-pw…
elvinasv Jan 8, 2026
95ff827
Merge branch 'ws-1837-service-worker-changes' into feature/WS-1838-pw…
elvinasv Jan 8, 2026
eef7277
EffectiveNetworkType import issue fix
jinidev Jan 8, 2026
18c0709
Merge branch 'ws-1837-service-worker-changes' into feature/WS-1838-pw…
elvinasv Jan 9, 2026
fd5a658
Merge branch 'ws-1837-service-worker-changes' into feature/WS-1838-pw…
elvinasv Jan 9, 2026
2af8ada
fix: undefined variable
elvinasv Jan 9, 2026
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
2 changes: 2 additions & 0 deletions src/app/components/ATIAnalytics/canonical/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import usePWAInstallTracker from '#app/hooks/usePWAInstallTracker';
import { reverbUrlHelper } from '@bbc/reverb-url-helper';
import useConnectionBackOnlineTracker from '#app/hooks/useConnectionBackOnlineTracker';
import useConnectionTypeTracker from '#app/hooks/useConnectionTypeTracker';
import usePWAOfflineTracking from '#app/hooks/usePWAOfflineTracking';
import { ATIAnalyticsProps } from '../types';
import getNoScriptTrackingPixelUrl from './getNoScriptTrackingPixelUrl';
import sendPageViewBeaconOperaMini from './sendPageViewBeaconOperaMini';
Expand Down Expand Up @@ -47,6 +48,7 @@ const CanonicalATIAnalytics = ({ reverbParams }: ATIAnalyticsProps) => {

useConnectionTypeTracker();
useConnectionBackOnlineTracker();
usePWAOfflineTracking();

const [reverbBeaconConfig] = useState(reverbParams);

Expand Down
16 changes: 16 additions & 0 deletions src/app/hooks/useCustomEventTracker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ const useCustomEventTracker = ({
].every(Boolean);

if (shouldSendEvent) {
// TEMP: Adding console logs to help debug event tracking issues - will remove later
// eslint-disable-next-line no-console
console.log('Tracking custom event:', {
eventName,
stringifiedData,
campaignID,
pageIdentifier,
platform,
producerId,
producerName,
service,
statsDestination,
experimentName,
experimentVariant,
});

try {
await sendEventBeacon({
type: VIEW_EVENT,
Expand Down
41 changes: 41 additions & 0 deletions src/app/hooks/useOfflinePageFlag/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { renderHook } from '@testing-library/react';
import { renderHook as renderSSRHook } from '@testing-library/react-hooks/server';
import { useOfflinePageFlag, OFFLINE_VISIT_FLAG } from './index';

describe('useOfflinePageFlag', () => {
beforeEach(() => {
Storage.prototype.setItem = jest.fn();
Storage.prototype.getItem = jest.fn();
Storage.prototype.removeItem = jest.fn();
jest.clearAllMocks();
});

it('should set offline flag when rendered', () => {
renderHook(() => useOfflinePageFlag());

expect(localStorage.setItem).toHaveBeenCalledWith(
OFFLINE_VISIT_FLAG,
'true',
);
});

it('should handle localStorage errors gracefully', () => {
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();

Storage.prototype.setItem = jest.fn().mockImplementation(() => {
throw new Error('localStorage is full');
});

expect(() => renderHook(() => useOfflinePageFlag())).not.toThrow();
expect(consoleWarnSpy).toHaveBeenCalledWith(
'useOfflinePageFlag',
expect.any(Error),
);
});

it('should not set flag on server side', () => {
renderSSRHook(() => useOfflinePageFlag());

expect(localStorage.setItem).not.toHaveBeenCalled();
});
});
22 changes: 22 additions & 0 deletions src/app/hooks/useOfflinePageFlag/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect } from 'react';

const OFFLINE_VISIT_FLAG = 'offline_page_visit';

/**
* Sets a flag in localStorage when user visits the offline page.
* Note: Offline page is only accessible in PWA mode (via service worker),
* so no need to check isPWA - if this hook runs, we're already in PWA.
*/
const useOfflinePageFlag = () => {
useEffect(() => {
if (typeof window === 'undefined') return;
try {
localStorage.setItem(OFFLINE_VISIT_FLAG, 'true');
} catch (error) {
// eslint-disable-next-line no-console
console.warn('useOfflinePageFlag', error);
}
}, []);
};

export { useOfflinePageFlag, OFFLINE_VISIT_FLAG };
247 changes: 247 additions & 0 deletions src/app/hooks/usePWAOfflineTracking/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { renderHook } from '@testing-library/react';
import { renderHook as renderSSRHook } from '@testing-library/react-hooks/server';
import { EffectiveNetworkType } from '#app/models/types/global';
import usePWAOfflineTracking from './index';
import useNetworkStatusTracker from '../useNetworkStatusTracker';
import useCustomEventTracker from '../useCustomEventTracker';

jest.mock('../useNetworkStatusTracker');
jest.mock('../useCustomEventTracker');

describe('usePWAOfflineTracking', () => {
const mockTrackOfflinePageViewEvent = jest.fn();
const mockUseNetworkStatusTracker =
useNetworkStatusTracker as jest.MockedFunction<
typeof useNetworkStatusTracker
>;
const mockUseCustomEventTracker =
useCustomEventTracker as jest.MockedFunction<typeof useCustomEventTracker>;

beforeEach(() => {
jest.clearAllMocks();
Storage.prototype.getItem = jest.fn();
Storage.prototype.setItem = jest.fn();
Storage.prototype.removeItem = jest.fn();

mockUseCustomEventTracker.mockReturnValue(mockTrackOfflinePageViewEvent);
});

afterEach(() => {
jest.restoreAllMocks();
});

it('should not fire event when offline flag is not set', () => {
mockUseNetworkStatusTracker.mockReturnValue({
isOnline: true,
networkType: '4g',
});
Storage.prototype.getItem = jest.fn().mockReturnValue(null);

renderHook(() => usePWAOfflineTracking());

expect(mockTrackOfflinePageViewEvent).not.toHaveBeenCalled();
});

it('should fire event when online and flag is set', () => {
mockUseNetworkStatusTracker.mockReturnValue({
isOnline: true,
networkType: '4g',
});
Storage.prototype.getItem = jest.fn().mockReturnValue('true');

renderHook(() => usePWAOfflineTracking());

expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1);
expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledWith('4g');
expect(localStorage.removeItem).toHaveBeenCalledWith('offline_page_visit');
});

it('should fire event on offline→online transition', () => {
Storage.prototype.getItem = jest.fn().mockReturnValue('true');

mockUseNetworkStatusTracker.mockReturnValue({
isOnline: false,
networkType: 'unknown',
});

const { rerender } = renderHook(() => usePWAOfflineTracking());

expect(mockTrackOfflinePageViewEvent).not.toHaveBeenCalled();

mockUseNetworkStatusTracker.mockReturnValue({
isOnline: true,
networkType: '4g',
});

rerender();

expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1);
expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledWith('4g');
expect(localStorage.removeItem).toHaveBeenCalledWith('offline_page_visit');
});

it('should not fire event again without flag being set', () => {
mockUseNetworkStatusTracker.mockReturnValue({
isOnline: true,
networkType: '4g',
});
const mockGetItem = jest
.fn()
.mockReturnValueOnce('true')
.mockReturnValue(null);
Storage.prototype.getItem = mockGetItem;

const { rerender } = renderHook(() => usePWAOfflineTracking());

expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1);

rerender();

expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1);
});

it('should fire event again after flag is set again on next offline visit', () => {
const mockGetItem = jest
.fn()
.mockReturnValueOnce('true')
.mockReturnValueOnce('true');
Storage.prototype.getItem = mockGetItem;

mockUseNetworkStatusTracker.mockReturnValue({
isOnline: false,
networkType: 'unknown',
});

const { rerender } = renderHook(() => usePWAOfflineTracking());

mockUseNetworkStatusTracker.mockReturnValue({
isOnline: true,
networkType: '4g',
});

rerender();

expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1);
expect(localStorage.removeItem).toHaveBeenCalledWith('offline_page_visit');

mockUseNetworkStatusTracker.mockReturnValue({
isOnline: false,
networkType: 'unknown',
});

rerender();

mockUseNetworkStatusTracker.mockReturnValue({
isOnline: true,
networkType: '5g',
});

rerender();

expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(2);
expect(mockTrackOfflinePageViewEvent).toHaveBeenLastCalledWith('5g');
expect(localStorage.removeItem).toHaveBeenCalledTimes(2);
});

it('should remove flag after firing event', () => {
Storage.prototype.getItem = jest.fn().mockReturnValue('true');

mockUseNetworkStatusTracker.mockReturnValue({
isOnline: false,
networkType: 'unknown',
});

const { rerender } = renderHook(() => usePWAOfflineTracking());

mockUseNetworkStatusTracker.mockReturnValue({
isOnline: true,
networkType: '4g',
});

rerender();

expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1);
expect(localStorage.removeItem).toHaveBeenCalledWith('offline_page_visit');
});

it('should not fire when offline even if flag is set', () => {
mockUseNetworkStatusTracker.mockReturnValue({
isOnline: false,
networkType: 'unknown',
});
Storage.prototype.getItem = jest.fn().mockReturnValue('true');

renderHook(() => usePWAOfflineTracking());

expect(mockTrackOfflinePageViewEvent).not.toHaveBeenCalled();
});

it('should handle localStorage errors gracefully', () => {
mockUseNetworkStatusTracker.mockReturnValue({
isOnline: true,
networkType: '4g',
});

const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
Storage.prototype.getItem = jest.fn().mockImplementation(() => {
throw new Error('localStorage access denied');
});

expect(() => renderHook(() => usePWAOfflineTracking())).not.toThrow();
expect(consoleErrorSpy).toHaveBeenCalledWith(
'usePWAOfflineTracking',
expect.any(Error),
);
expect(mockTrackOfflinePageViewEvent).not.toHaveBeenCalled();
});

it('should not track on server side', () => {
mockUseNetworkStatusTracker.mockReturnValue({
isOnline: true,
networkType: '4g',
});
Storage.prototype.getItem = jest.fn().mockReturnValue('true');

renderSSRHook(() => usePWAOfflineTracking());

expect(mockTrackOfflinePageViewEvent).not.toHaveBeenCalled();
});

it.each(['slow-2g', '2g', '3g', '4g', '5g', 'unknown'])(
'should pass correct network type to tracking function: %s',
networkType => {
Storage.prototype.getItem = jest.fn().mockReturnValue('true');

mockUseNetworkStatusTracker.mockReturnValue({
isOnline: true,
networkType: networkType as EffectiveNetworkType,
});

renderHook(() => usePWAOfflineTracking());

expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledWith(networkType);
},
);

it('should only fire on actual offline→online transition, not online→offline', () => {
Storage.prototype.getItem = jest.fn().mockReturnValue('true');

mockUseNetworkStatusTracker.mockReturnValue({
isOnline: true,
networkType: '4g',
});

const { rerender } = renderHook(() => usePWAOfflineTracking());

expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1);

mockUseNetworkStatusTracker.mockReturnValue({
isOnline: false,
networkType: 'unknown',
});

rerender();

expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1);
});
});
43 changes: 43 additions & 0 deletions src/app/hooks/usePWAOfflineTracking/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useEffect } from 'react';
import useNetworkStatusTracker from '../useNetworkStatusTracker';
import useCustomEventTracker from '../useCustomEventTracker';
import { OFFLINE_VISIT_FLAG } from '../useOfflinePageFlag';

const OFFLINE_PAGE_VIEW_EVENT_NAME = 'pwa-offline-page-view';

/**
* Tracks offline→online transitions after user has visited offline page.
* Fires when network comes back online while flag is set.
*
* Flag can only be set in PWA mode (offline page requires service worker).
* By not checking isPWA at dispatch time, we track each offline session separately
* even if user switches between PWA/browser modes during reconnection.
* This prevents data loss and ensures accurate analytics.
*/
const usePWAOfflineTracking = () => {
const { isOnline, networkType } = useNetworkStatusTracker();

const trackOfflinePageViewEvent = useCustomEventTracker({
eventName: OFFLINE_PAGE_VIEW_EVENT_NAME,
});

useEffect(() => {
if (typeof window === 'undefined' || !isOnline) {
return;
}
try {
const offlineVisitFlag = localStorage.getItem(OFFLINE_VISIT_FLAG);

if (offlineVisitFlag !== 'true') {
return;
}
trackOfflinePageViewEvent(networkType);
localStorage.removeItem(OFFLINE_VISIT_FLAG);
} catch (error) {
// eslint-disable-next-line no-console
console.error('usePWAOfflineTracking', error);
}
}, [isOnline, networkType, trackOfflinePageViewEvent]);
};

export default usePWAOfflineTracking;
Loading