From 658039901a414103ab64695a2c5b49af08a55497 Mon Sep 17 00:00:00 2001 From: Jake Lee Kennedy <jake.kennedy@guardian.co.uk> Date: Fri, 17 May 2024 10:48:32 +0100 Subject: [PATCH] use fetch to send commercial metrics (#1382) --- .changeset/lovely-zoos-wink.md | 5 + src/core/send-commercial-metrics.spec.ts | 190 ++++++++++++++--------- src/core/send-commercial-metrics.ts | 11 +- 3 files changed, 129 insertions(+), 77 deletions(-) create mode 100644 .changeset/lovely-zoos-wink.md diff --git a/.changeset/lovely-zoos-wink.md b/.changeset/lovely-zoos-wink.md new file mode 100644 index 000000000..11521fee2 --- /dev/null +++ b/.changeset/lovely-zoos-wink.md @@ -0,0 +1,5 @@ +--- +'@guardian/commercial': minor +--- + +Use fetch instead of sendBeacon for commercial metrics diff --git a/src/core/send-commercial-metrics.spec.ts b/src/core/send-commercial-metrics.spec.ts index f5191c03b..161cda385 100644 --- a/src/core/send-commercial-metrics.spec.ts +++ b/src/core/send-commercial-metrics.spec.ts @@ -47,6 +47,14 @@ const defaultMetrics = { ], }; +const expectedOptions = { + method: 'POST', + body: JSON.stringify(defaultMetrics), + keepalive: true, + cache: 'no-store', + mode: 'no-cors', +}; + const tcfv2AllConsent: ConsentState = { tcfv2: { consents: { @@ -124,7 +132,7 @@ afterEach(() => { }); describe('send commercial metrics', () => { - Object.defineProperty(navigator, 'sendBeacon', { + Object.defineProperty(window, 'fetch', { configurable: true, enumerable: true, value: jest.fn(), @@ -147,8 +155,8 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('visibilitychange')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([ - [Endpoints.PROD, JSON.stringify(defaultMetrics)], + expect((window.fetch as jest.Mock).mock.calls).toEqual([ + [Endpoints.PROD, expectedOptions], ]); }); @@ -166,7 +174,7 @@ describe('send commercial metrics', () => { setVisibility('visible'); global.dispatchEvent(new Event('visibilitychange')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([]); + expect((window.fetch as jest.Mock).mock.calls).toEqual([]); }); it('does not send metrics when user is not in sampling group', async () => { @@ -183,7 +191,7 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('visibilitychange')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([]); + expect((window.fetch as jest.Mock).mock.calls).toEqual([]); }); it('does not send metrics when consent does not include purpose 8', async () => { @@ -200,7 +208,7 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('visibilitychange')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([]); + expect((window.fetch as jest.Mock).mock.calls).toEqual([]); }); it('sends metrics when non-TCFv2 user (i.e. USA or Australia) consents', async () => { @@ -217,8 +225,8 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('visibilitychange')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([ - [Endpoints.PROD, JSON.stringify(defaultMetrics)], + expect((window.fetch as jest.Mock).mock.calls).toEqual([ + [Endpoints.PROD, expectedOptions], ]); }); @@ -236,8 +244,8 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('visibilitychange')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([ - [Endpoints.PROD, JSON.stringify(defaultMetrics)], + expect((window.fetch as jest.Mock).mock.calls).toEqual([ + [Endpoints.PROD, expectedOptions], ]); }); @@ -280,8 +288,8 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('visibilitychange')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([ - [Endpoints.PROD, JSON.stringify(defaultMetrics)], + expect((window.fetch as jest.Mock).mock.calls).toEqual([ + [Endpoints.PROD, expectedOptions], ]); }); @@ -300,7 +308,7 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('visibilitychange')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([]); + expect((window.fetch as jest.Mock).mock.calls).toEqual([]); }); it('expects to be initialised before calling bypassCoreWebVitalsSampling', async () => { @@ -333,16 +341,22 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('visibilitychange')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([ + expect((window.fetch as jest.Mock).mock.calls).toEqual([ [ Endpoints.CODE, - JSON.stringify({ - ...defaultMetrics, - properties: [ - { name: 'isDev', value: 'testurl.theguardian.com' }, - { name: 'adBlockerInUse', value: 'false' }, - ], - }), + { + ...expectedOptions, + body: JSON.stringify({ + ...defaultMetrics, + properties: [ + { + name: 'isDev', + value: 'testurl.theguardian.com', + }, + { name: 'adBlockerInUse', value: 'false' }, + ], + }), + }, ], ]); }); @@ -368,17 +382,20 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('visibilitychange')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([ + expect((window.fetch as jest.Mock).mock.calls).toEqual([ [ Endpoints.PROD, - JSON.stringify({ - ...defaultMetrics, - properties: [ - { name: 'downlink', value: '1' }, - { name: 'effectiveType', value: '4g' }, - { name: 'adBlockerInUse', value: 'false' }, - ], - }), + { + ...expectedOptions, + body: JSON.stringify({ + ...defaultMetrics, + properties: [ + { name: 'downlink', value: '1' }, + { name: 'effectiveType', value: '4g' }, + { name: 'adBlockerInUse', value: 'false' }, + ], + }), + }, ], ]); }); @@ -404,18 +421,24 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('pagehide')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([ + expect((window.fetch as jest.Mock).mock.calls).toEqual([ [ Endpoints.CODE, - JSON.stringify({ - ...defaultMetrics, - properties: [ - { name: 'downlink', value: '1' }, - { name: 'effectiveType', value: '4g' }, - { name: 'isDev', value: 'testurl.theguardian.com' }, - { name: 'adBlockerInUse', value: 'false' }, - ], - }), + { + ...expectedOptions, + body: JSON.stringify({ + ...defaultMetrics, + properties: [ + { name: 'downlink', value: '1' }, + { name: 'effectiveType', value: '4g' }, + { + name: 'isDev', + value: 'testurl.theguardian.com', + }, + { name: 'adBlockerInUse', value: 'false' }, + ], + }), + }, ], ]); }); @@ -467,17 +490,23 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('pagehide')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([ + expect((window.fetch as jest.Mock).mock.calls).toEqual([ [ Endpoints.CODE, - JSON.stringify({ - ...defaultMetrics, - properties: [ - { name: 'downlink', value: '1' }, - { name: 'effectiveType', value: '4g' }, - { name: 'isDev', value: 'testurl.theguardian.com' }, - ], - }), + { + ...expectedOptions, + body: JSON.stringify({ + ...defaultMetrics, + properties: [ + { name: 'downlink', value: '1' }, + { name: 'effectiveType', value: '4g' }, + { + name: 'isDev', + value: 'testurl.theguardian.com', + }, + ], + }), + }, ], ]); }); @@ -498,17 +527,23 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('pagehide')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([ + expect((window.fetch as jest.Mock).mock.calls).toEqual([ [ Endpoints.CODE, - JSON.stringify({ - ...defaultMetrics, - properties: [ - { name: 'adSlotsInline', value: '5' }, - { name: 'adSlotsTotal', value: '10' }, - { name: 'isDev', value: 'testurl.theguardian.com' }, - ], - }), + { + ...expectedOptions, + body: JSON.stringify({ + ...defaultMetrics, + properties: [ + { name: 'adSlotsInline', value: '5' }, + { name: 'adSlotsTotal', value: '10' }, + { + name: 'isDev', + value: 'testurl.theguardian.com', + }, + ], + }), + }, ], ]); }); @@ -531,13 +566,16 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('pagehide')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([ + expect((window.fetch as jest.Mock).mock.calls).toEqual([ [ Endpoints.PROD, - JSON.stringify({ - ...defaultMetrics, - metrics: [{ name: 'offlineCount', value: 3 }], - }), + { + ...expectedOptions, + body: JSON.stringify({ + ...defaultMetrics, + metrics: [{ name: 'offlineCount', value: 3 }], + }), + }, ], ]); }); @@ -558,13 +596,16 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('pagehide')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([ + expect((window.fetch as jest.Mock).mock.calls).toEqual([ [ Endpoints.PROD, - JSON.stringify({ - ...defaultMetrics, - metrics: [{ name: 'offlineCount', value: 0 }], - }), + { + ...expectedOptions, + body: JSON.stringify({ + ...defaultMetrics, + metrics: [{ name: 'offlineCount', value: 0 }], + }), + }, ], ]); }); @@ -585,13 +626,16 @@ describe('send commercial metrics', () => { setVisibility('hidden'); global.dispatchEvent(new Event('pagehide')); - expect((navigator.sendBeacon as jest.Mock).mock.calls).toEqual([ + expect((window.fetch as jest.Mock).mock.calls).toEqual([ [ Endpoints.PROD, - JSON.stringify({ - ...defaultMetrics, - metrics: [], - }), + { + ...expectedOptions, + body: JSON.stringify({ + ...defaultMetrics, + metrics: [], + }), + }, ], ]); }); diff --git a/src/core/send-commercial-metrics.ts b/src/core/send-commercial-metrics.ts index 93c16f6d9..b93337178 100644 --- a/src/core/send-commercial-metrics.ts +++ b/src/core/send-commercial-metrics.ts @@ -115,10 +115,13 @@ function sendMetrics() { commercialMetricsPayload, ); - return navigator.sendBeacon( - endpoint, - JSON.stringify(commercialMetricsPayload), - ); + void fetch(endpoint, { + method: 'POST', + body: JSON.stringify(commercialMetricsPayload), + keepalive: true, + cache: 'no-store', + mode: 'no-cors', + }); } type ArrayMetric = [key: string, value: string | number];