From 0560a8e80e31b9a0e1ea9a2d830d54e299537cba Mon Sep 17 00:00:00 2001 From: Ajay Benno Date: Mon, 26 Feb 2024 21:19:47 -0500 Subject: [PATCH] EventsSDK: Beacon fallback Support falling back to beacon if fetch w/keepaliave is unsupported by the browser. TEST=manual,auto Ran test-site locally and confirmed that beacon was used for firefox and fetch w/keepalive was used in chrome. Confirmed all events still worked as expected --- package-lock.json | 4 +-- package.json | 2 +- src/infra/ChatAnalyticsReporter.ts | 4 +-- src/infra/HttpRequester.ts | 38 ++++++++++++++++++++-------- src/infra/SearchAnalyticsReporter.ts | 2 +- src/utils/Browser.ts | 13 ++++++++++ test-site/package-lock.json | 2 +- test-site/src/index.ts | 2 +- tests/infra/ChatAnalyticsReporter.ts | 20 +++++++++------ 9 files changed, 61 insertions(+), 26 deletions(-) create mode 100644 src/utils/Browser.ts diff --git a/package-lock.json b/package-lock.json index 314e44ec..8109bacb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@yext/analytics", - "version": "0.6.5", + "version": "0.6.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@yext/analytics", - "version": "0.6.4", + "version": "0.6.6", "license": "BSD-3-Clause", "dependencies": { "cross-fetch": "^3.1.5", diff --git a/package.json b/package.json index a48fb1aa..0e6e043a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@yext/analytics", - "version": "0.6.5", + "version": "0.6.6", "description": "An analytics library for Yext", "author": "slapshot@yext.com", "license": "BSD-3-Clause", diff --git a/src/infra/ChatAnalyticsReporter.ts b/src/infra/ChatAnalyticsReporter.ts index 8397b640..278e5a52 100644 --- a/src/infra/ChatAnalyticsReporter.ts +++ b/src/infra/ChatAnalyticsReporter.ts @@ -57,7 +57,6 @@ export class ChatAnalyticsReporter { */ public async report(event: ChatEventPayLoad): Promise { const headers: Record = { - Authorization: `KEY ${this.apiKey}`, 'Content-Type': 'application/json', }; @@ -68,7 +67,8 @@ export class ChatAnalyticsReporter { this.endpoint, { ...event, - sessionId + sessionId, + authorization: `KEY ${this.apiKey}` }, headers); diff --git a/src/infra/HttpRequester.ts b/src/infra/HttpRequester.ts index e11bc21c..74643275 100644 --- a/src/infra/HttpRequester.ts +++ b/src/infra/HttpRequester.ts @@ -1,6 +1,7 @@ import { HttpRequesterService } from '../services'; import { AnalyticsPayload, EventPayload } from '../models'; import fetch from 'cross-fetch'; +import { isFirefox } from '../utils/Browser'; /** * Responsible for making web requests. @@ -14,20 +15,36 @@ export class HttpRequester implements HttpRequesterService { ): Promise { const data = JSON.stringify(body); - const fetchInit: RequestInit = { - method: 'POST', - headers, - body: data, - keepalive: true - }; + if (isFirefox()) { + // send via beacon if the browser is using firefox since it does not support fetch w/keepalive + var enqueuedEvent = navigator.sendBeacon(url, data); + if (enqueuedEvent) { + return Promise.resolve(new Response(null, { status: 204 })); + } else { + // If there was a failure enqueing the event just reject + // with a Response that indicates an error. + // Fetch by default does not reject promises and instead just + // handles errors with a successful promise with an error that + // indicates an error + return Promise.resolve(Response.error()); + } + } else { + const fetchInit: RequestInit = { + method: 'POST', + headers, + body: data, + keepalive: true + }; - if (typeof(window) !== 'undefined' && window.fetch) { - return window.fetch(url, fetchInit); + if (typeof (window) !== 'undefined' && window.fetch) { + return window.fetch(url, fetchInit); + } + return fetch(url, fetchInit); } - - return fetch(url, fetchInit); } + + get(url: string): Promise { const fetchInit: RequestInit = { method: 'GET', @@ -41,4 +58,5 @@ export class HttpRequester implements HttpRequesterService { return fetch(url, fetchInit); } + } \ No newline at end of file diff --git a/src/infra/SearchAnalyticsReporter.ts b/src/infra/SearchAnalyticsReporter.ts index b5182f2c..438b44b6 100644 --- a/src/infra/SearchAnalyticsReporter.ts +++ b/src/infra/SearchAnalyticsReporter.ts @@ -52,7 +52,7 @@ export class SearchAnalyticsReporter implements SearchAnalyticsService { const res = await this.httpRequesterService.post( this._endpoint, { data, ...additionalRequestAttributes } ); - if (res.status !== 200) { + if (!res.ok) { const errorMessage = await res.text(); throw new Error(errorMessage); } diff --git a/src/utils/Browser.ts b/src/utils/Browser.ts new file mode 100644 index 00000000..878737cc --- /dev/null +++ b/src/utils/Browser.ts @@ -0,0 +1,13 @@ + +/** + * Detects if the browser is firefox and return true if so + * + */ +export function isFirefox( + ): boolean { + // keepAlive is not supported in Firefox or Firefox for Android + return ( + !!navigator.userAgent && + navigator.userAgent.toLowerCase().includes('firefox') + ); +} \ No newline at end of file diff --git a/test-site/package-lock.json b/test-site/package-lock.json index 42698ff7..72421b56 100644 --- a/test-site/package-lock.json +++ b/test-site/package-lock.json @@ -23,7 +23,7 @@ }, "..": { "name": "@yext/analytics", - "version": "0.6.4", + "version": "0.6.5", "license": "BSD-3-Clause", "dependencies": { "cross-fetch": "^3.1.5", diff --git a/test-site/src/index.ts b/test-site/src/index.ts index 21c55aeb..79c567f7 100644 --- a/test-site/src/index.ts +++ b/test-site/src/index.ts @@ -84,7 +84,7 @@ export function fireListings() { } const chat = provideChatAnalytics({ - apiKey: process.env.CHAT_API_KEY, + apiKey: 'd0e7eab25eb91c1eafe037d61eadd869', }); export function fireChatEvent() { diff --git a/tests/infra/ChatAnalyticsReporter.ts b/tests/infra/ChatAnalyticsReporter.ts index 038641f5..0971b8ad 100644 --- a/tests/infra/ChatAnalyticsReporter.ts +++ b/tests/infra/ChatAnalyticsReporter.ts @@ -9,7 +9,6 @@ const prodConfig: ChatAnalyticsConfig = { }; const expectedHeaders: Record = { - Authorization: 'KEY mock-api-key', 'Content-Type': 'application/json', }; @@ -21,6 +20,11 @@ const payload: ChatEventPayLoad = { sessionId: 'mocked-ulid-value' }; +const expectedPayload = { + ...payload, + authorization: 'KEY mock-api-key' +} + beforeEach(() => { jest.spyOn(ulidxLib, 'ulid').mockReturnValue('mocked-ulid-value'); }); @@ -35,7 +39,7 @@ it('should send events to the prod domain when configured', async () => { expect(response).toEqual(mockedResponse); expect(mockService.post).toBeCalledWith( expectedUrl, - payload, + expectedPayload, expectedHeaders ); }); @@ -51,7 +55,7 @@ it('should send events to the custom endpoint when configured', async () => { expect(response).toEqual(mockedResponse); expect(mockService.post).toBeCalledWith( expectedUrl, - payload, + expectedPayload, expectedHeaders ); }); @@ -114,7 +118,7 @@ describe('sessionId handling', () => { await reporter.report(payload); expect(mockService.post).toBeCalledWith( expect.any(String), - payload, + expectedPayload, expect.any(Object) ); }); @@ -130,7 +134,7 @@ describe('sessionId handling', () => { expect(mockService.post).toBeCalledWith( expect.any(String), { - ...payload, + ...expectedPayload, sessionId: undefined }, expect.any(Object) @@ -150,7 +154,7 @@ describe('sessionId handling', () => { expect(mockService.post).toBeCalledWith( expect.any(String), { - ...payload, + ...expectedPayload, sessionId: 'custom-ulid-value', }, expect.any(Object) @@ -168,7 +172,7 @@ describe('sessionId handling', () => { expect(mockService.post).toBeCalledWith( expect.any(String), { - ...payload, + ...expectedPayload, sessionId: undefined }, expect.any(Object) @@ -182,7 +186,7 @@ describe('sessionId handling', () => { expect(mockService.post).toBeCalledWith( expect.any(String), { - ...payload, + ...expectedPayload, sessionId: undefined }, expect.any(Object)