Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
146 changes: 146 additions & 0 deletions public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,21 @@ const hasOfflinePageFunctionality = false;
const OFFLINE_PAGE = `/${service}/offline`;

self.addEventListener('install', event => {
// eslint-disable-next-line no-console
console.log('[SW] Installing...');
self.skipWaiting();

Choose a reason for hiding this comment

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

We should evaluate if we want to skip waiting for production to avoid weird behaviour. Needs re-evaluation.

event.waitUntil(async () => {
const cache = await caches.open(cacheName);
if (hasOfflinePageFunctionality) await cache.add(OFFLINE_PAGE);
});
});

self.addEventListener('activate', event => {
// eslint-disable-next-line no-console
console.log('[SW] Activating...');
event.waitUntil(self.clients.claim());
});

const CACHEABLE_FILES = [
// Reverb
/^https:\/\/static(?:\.test)?\.files\.bbci\.co\.uk\/ws\/(?:simorgh-assets|simorgh1-preview-assets|simorgh2-preview-assets)\/public\/static\/js\/reverb\/reverb-3.10.2.js$/,
Expand All @@ -35,7 +44,134 @@ const CACHEABLE_FILES = [
const WEBP_IMAGE =
/^https:\/\/ichef(\.test)?\.bbci\.co\.uk\/(news|images|ace\/(standard|ws))\/.+.webp$/;

// Analytics offline tracking
const ANALYTICS_QUEUE_CACHE = 'analytics-queue-v1';
const MAX_QUEUE_SIZE = 100;

const ANALYTICS_PATTERN =
/^https:\/\/(.*\.)?ati-host|^https:\/\/(.*\.)?chartbeat\.net/;

const isAnalyticsRequest = url => ANALYTICS_PATTERN.test(url);

const queueRequest = async request => {
try {
// eslint-disable-next-line no-console
console.log('[SW] Queueing request:', request.url.substring(0, 100));
const cache = await caches.open(ANALYTICS_QUEUE_CACHE);
const keys = await cache.keys();

// Enforce queue size limit
if (keys.length >= MAX_QUEUE_SIZE) {
// eslint-disable-next-line no-console
console.log('[SW] Queue full, removing oldest');
await cache.delete(keys[0]);
}

const queuedData = {
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
timestamp: Date.now(),
};

const cacheKey = `${ANALYTICS_QUEUE_CACHE}-${Date.now()}-${Math.random()}`;
await cache.put(cacheKey, new Response(JSON.stringify(queuedData)));

// eslint-disable-next-line no-console
console.log('[SW] Queue saved. Length:', keys.length + 1);
} catch (error) {
// eslint-disable-next-line no-console
console.error('[SW] Failed to queue request:', error);
}
};

let queueProcessTimeout;

const processQueue = async () => {
try {
const cache = await caches.open(ANALYTICS_QUEUE_CACHE);
const requests = await cache.keys();

if (requests.length === 0) return;

// eslint-disable-next-line no-console
console.log('[SW] Processing queue. Length:', requests.length);

let successCount = 0;

for (const cacheKey of requests) {
try {
const response = await cache.match(cacheKey);
const queuedData = await response.json();

const originalRequest = new Request(queuedData.url, {
method: queuedData.method,
headers: queuedData.headers,
});

const result = await fetch(originalRequest);

if (result.ok) {
await cache.delete(cacheKey);
successCount += 1;
}
} catch {
// Leave in queue to retry later
}
}

// eslint-disable-next-line no-console
console.log(`[SW] Processed ${successCount}/${requests.length} items successfully`);
} catch (error) {
// eslint-disable-next-line no-console
console.error('[SW] Failed to process queue:', error);
}
};

const debouncedProcessQueue = () => {
clearTimeout(queueProcessTimeout);
queueProcessTimeout = setTimeout(() => {
// eslint-disable-next-line no-console
console.log('[SW] Debounced queue processing triggered');
processQueue();
}, 1000);
};

const handleAnalyticsRequest = async request => {
try {
const response = await fetch(request.clone());

if (response.ok) {
// Debounce queue processing to avoid rapid-fire requests
debouncedProcessQueue();
}

return response;
} catch {
// eslint-disable-next-line no-console
console.log('[SW] Analytics failed (offline) - queueing');
await queueRequest(request);

return new Response(JSON.stringify({ queued: true }), {
status: 202,
statusText: 'Accepted (Queued)',
headers: { 'Content-Type': 'application/json' },
});
}
};

const fetchEventHandler = async event => {
const requestUrl = event.request.url;

// Handle analytics requests
if (isAnalyticsRequest(requestUrl)) {
// eslint-disable-next-line no-console
console.log('[SW] Intercepting analytics:', requestUrl.substring(0, 100));
event.respondWith(handleAnalyticsRequest(event.request));
return;
}

// ORIGINAL CODE BELOW - UNCHANGED
const isRequestForCacheableFile = CACHEABLE_FILES.some(cacheableFile =>
new RegExp(cacheableFile).test(event.request.url),
);
Expand Down Expand Up @@ -93,3 +229,13 @@ const fetchEventHandler = async event => {
};

onfetch = fetchEventHandler;

// Listen for messages from the main thread
self.addEventListener('message', event => {
if (event.data === 'PROCESS_ANALYTICS_QUEUE') {
// eslint-disable-next-line no-console
console.log('[SW] Received message to process queue');
// Use direct processQueue() for manual triggers (no debounce)
processQueue();
}
});
9 changes: 9 additions & 0 deletions src/app/components/ATIAnalytics/atiUrl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const buildATIPageTrackPath = ({
ampExperimentName,
experimentVariant,
readTimeMilliseconds,
networkType,
}: ATIPageTrackingProps) => {
const href = getHref(platform);
const referrer = getReferrer(platform);
Expand Down Expand Up @@ -218,6 +219,12 @@ export const buildATIPageTrackPath = ({
value: isLocServeCookieSet(),
wrap: true,
},
{
key: 'x19',
description: 'effective network type',
value: networkType,
wrap: true,
},
{
key: 'xto',
description: 'marketing campaign',
Expand Down Expand Up @@ -461,6 +468,7 @@ export const buildReverbAnalyticsModel = ({
experimentName,
experimentVariant,
readTimeMilliseconds,
networkType,
}: ATIPageTrackingProps): ReverbBeaconConfig => {
const href = getHref(platform);
const referrer = getReferrer(platform);
Expand Down Expand Up @@ -499,6 +507,7 @@ export const buildReverbAnalyticsModel = ({
x16: aggregatedCampaigns,
x17: categoryName,
x18: isLocServeCookieSet(),
x19: networkType,
item_duration: readTimeMilliseconds,
...(experimentVariant &&
experimentName && {
Expand Down
11 changes: 9 additions & 2 deletions src/app/components/ATIAnalytics/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { use } from 'react';
import { RequestContext } from '#contexts/RequestContext';
import useNetworkStatusTracker from '#app/hooks/useNetworkStatusTracker';
import { ServiceContext } from '../../contexts/ServiceContext';
import CanonicalATIAnalytics from './canonical';
import AmpATIAnalytics from './amp';
Expand All @@ -10,20 +11,26 @@ import { buildATIUrl, buildReverbParams } from './params';
const ATIAnalytics = ({ atiData = {} }: ATIProps) => {
const requestContext = use(RequestContext);
const serviceContext = use(ServiceContext);
const { networkType } = useNetworkStatusTracker();
const { isAmp } = requestContext;
const { useReverb } = serviceContext;

const enhancedAtiData = {
...atiData,
networkType,
};

const urlPageViewParams = buildATIUrl({
requestContext,
serviceContext,
atiData,
atiData: enhancedAtiData,
}) as string;

const reverbParams = useReverb
? buildReverbParams({
requestContext,
serviceContext,
atiData,
atiData: enhancedAtiData,
})
: null;

Expand Down
2 changes: 2 additions & 0 deletions src/app/components/ATIAnalytics/params/buildParams/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const buildPageATIParams = ({
experimentName,
experimentVariant,
readTimeMilliseconds,
networkType,
} = atiData;

return {
Expand Down Expand Up @@ -60,6 +61,7 @@ export const buildPageATIParams = ({
...(ampExperimentName && { ampExperimentName }),
...(experimentName && { experimentName }),
...(experimentVariant && { experimentVariant }),
...(networkType && { networkType }),
};
};

Expand Down
3 changes: 3 additions & 0 deletions src/app/components/ATIAnalytics/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable camelcase */
import { EffectiveNetworkType } from '#app/hooks/useNetworkStatusTracker/type';
import { PageTypes, Platforms, Services } from '../../models/types/global';
import { RequestContextProps } from '../../contexts/RequestContext';
import { ServiceConfig } from '../../models/types/serviceConfig';
Expand Down Expand Up @@ -35,6 +36,7 @@ export interface ATIData {
experimentName?: string | null;
experimentVariant?: string | null;
readTimeMilliseconds?: number | null;
networkType?: EffectiveNetworkType;
}

export interface PageData {
Expand Down Expand Up @@ -215,6 +217,7 @@ export interface ATIPageTrackingProps {
experimentName?: string | null;
experimentVariant?: string | null;
readTimeMilliseconds?: number | null;
networkType?: EffectiveNetworkType;
}

export interface ATIProps {
Expand Down
Loading