From 564f5b83e610ce2e2d2cfff85a04c0d725f5ca00 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:38:27 -0700 Subject: [PATCH 1/6] WIP --- packages/sdk/browser/src/BrowserClient.ts | 3 +++ .../sdk/browser/src/BrowserStateDetector.ts | 20 +++++++++++++++++++ packages/sdk/browser/src/options.ts | 8 ++++++++ .../common/src/api/platform/Requests.ts | 5 +++++ .../common/src/internal/events/EventSender.ts | 3 +++ .../shared/sdk-client/src/api/LDOptions.ts | 4 ++-- 6 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 packages/sdk/browser/src/BrowserStateDetector.ts diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index cef2729ab..f4a9ee2ce 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -21,6 +21,7 @@ import GoalManager from './goals/GoalManager'; import { Goal, isClick } from './goals/Goals'; import validateOptions, { BrowserOptions, filterToBaseOptions } from './options'; import BrowserPlatform from './platform/BrowserPlatform'; +import { registerStateDetection } from './BrowserStateDetector'; /** * @@ -211,6 +212,8 @@ export class BrowserClient extends LDClientImpl implements LDClient { // which emits the event, and assign its promise to a member. The "waitForGoalsReady" function // would return that promise. this.goalManager.initialize(); + + registerStateDetection(() => this.flush()); } } diff --git a/packages/sdk/browser/src/BrowserStateDetector.ts b/packages/sdk/browser/src/BrowserStateDetector.ts new file mode 100644 index 000000000..e49cd1a72 --- /dev/null +++ b/packages/sdk/browser/src/BrowserStateDetector.ts @@ -0,0 +1,20 @@ +export function registerStateDetection(requestFlush: () => void) { + // When the visibility of the page changes to hidden we want to flush any pending events. + // + // This is handled with visibility, instead of beforeunload/unload + // because those events are not compatible with the bfcache and are unlikely + // to be called in many situations. For more information see: https://developer.chrome.com/blog/page-lifecycle-api/ + // + // Redundancy is included by using both the visibilitychange handler as well as + // pagehide, because different browsers, and versions have different bugs with each. + // This also may provide more opportunity for the events to get flushed. + // + const handleVisibilityChange = () => { + if (document.visibilityState === 'hidden') { + requestFlush(); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + window.addEventListener('pagehide', requestFlush); +} diff --git a/packages/sdk/browser/src/options.ts b/packages/sdk/browser/src/options.ts index 7a6acce14..1022b8327 100644 --- a/packages/sdk/browser/src/options.ts +++ b/packages/sdk/browser/src/options.ts @@ -6,6 +6,8 @@ import { TypeValidators, } from '@launchdarkly/js-client-sdk-common'; +const DEFAULT_FLUSH_INTERVAL_SECONDS = 2; + /** * Initialization options for the LaunchDarkly browser SDK. */ @@ -66,8 +68,14 @@ export function filterToBaseOptions(opts: BrowserOptions): LDOptionsBase { return baseOptions; } +function applyBrowserDefaults(opts: BrowserOptions) { + // eslint-disable-next-line no-param-reassign + opts.flushInterval ??= DEFAULT_FLUSH_INTERVAL_SECONDS; +} + export default function validateOptions(opts: BrowserOptions, logger: LDLogger): ValidatedOptions { const output: ValidatedOptions = { ...optDefaults }; + applyBrowserDefaults(output); Object.entries(validators).forEach((entry) => { const [key, validator] = entry as [keyof BrowserOptions, TypeValidator]; diff --git a/packages/shared/common/src/api/platform/Requests.ts b/packages/shared/common/src/api/platform/Requests.ts index 219d775f4..8b0438d20 100644 --- a/packages/shared/common/src/api/platform/Requests.ts +++ b/packages/shared/common/src/api/platform/Requests.ts @@ -76,6 +76,11 @@ export interface Options { method?: string; body?: string; timeout?: number; + /** + * For use in browser environments. Platform support will be best effort for this field. + * https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#keepalive + */ + keepalive?: boolean; } export interface EventSourceCapabilities { diff --git a/packages/shared/common/src/internal/events/EventSender.ts b/packages/shared/common/src/internal/events/EventSender.ts index 3d11a14b3..3bb585024 100644 --- a/packages/shared/common/src/internal/events/EventSender.ts +++ b/packages/shared/common/src/internal/events/EventSender.ts @@ -65,6 +65,9 @@ export default class EventSender implements LDEventSender { headers, body: JSON.stringify(events), method: 'POST', + // When sending events from browser environments the request should be completed even + // if the user is navigating away from the page. + keepalive: true, }); const serverDate = Date.parse(resHeaders.get('date') || ''); diff --git a/packages/shared/sdk-client/src/api/LDOptions.ts b/packages/shared/sdk-client/src/api/LDOptions.ts index 5e2c11513..e72c1a8ec 100644 --- a/packages/shared/sdk-client/src/api/LDOptions.ts +++ b/packages/shared/sdk-client/src/api/LDOptions.ts @@ -111,9 +111,9 @@ export interface LDOptions { eventsUri?: string; /** - * Controls how often the SDK flushes events. + * The interval in between flushes of the analytics events queue, in seconds. * - * @defaultValue 30s. + * @defaultValue 2s for browser implementations 30s for others. */ flushInterval?: number; From 270bca4f692dcd226d259b18ec38196c46951524 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:55:30 -0700 Subject: [PATCH 2/6] Refactoring --- .../__tests__/BrowserStateDetector.test.ts | 0 packages/sdk/browser/src/BrowserApi.ts | 118 ++++++++++++++++++ packages/sdk/browser/src/BrowserClient.ts | 9 +- .../sdk/browser/src/BrowserStateDetector.ts | 15 ++- packages/sdk/browser/src/goals/GoalManager.ts | 3 +- packages/sdk/browser/src/goals/GoalTracker.ts | 17 +-- .../sdk/browser/src/goals/LocationWatcher.ts | 10 +- packages/sdk/browser/src/options.ts | 13 ++ .../sdk/browser/src/platform/BrowserCrypto.ts | 3 +- 9 files changed, 167 insertions(+), 21 deletions(-) create mode 100644 packages/sdk/browser/__tests__/BrowserStateDetector.test.ts create mode 100644 packages/sdk/browser/src/BrowserApi.ts diff --git a/packages/sdk/browser/__tests__/BrowserStateDetector.test.ts b/packages/sdk/browser/__tests__/BrowserStateDetector.test.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/sdk/browser/src/BrowserApi.ts b/packages/sdk/browser/src/BrowserApi.ts new file mode 100644 index 000000000..a9c233b7a --- /dev/null +++ b/packages/sdk/browser/src/BrowserApi.ts @@ -0,0 +1,118 @@ +/** + * All access to browser specific APIs should be limited to this file. + * Care should be taken to ensure that any given method will work in the service worker API. So if + * something isn't available in the service worker API attempt to provide reasonable defaults. + */ + +export function isDocument() { + return typeof document !== undefined; +} + +export function isWindow() { + return typeof window !== undefined; +} + +/** + * Register an event handler on the document. If there is no document, such as when running in + * a service worker, then no operation is performed. + * + * @param type The event type to register a handler for. + * @param listener The handler to register. + * @param options Event registration options. + * @returns a function which unregisters the handler. + */ +export function addDocumentEventListener( + type: string, + listener: (this: Document, ev: Event) => any, + options?: boolean | AddEventListenerOptions, +): () => void { + if (isDocument()) { + document.addEventListener(type, listener, options); + return () => { + document.removeEventListener(type, listener, options); + }; + } + // No document, so no need to unregister anything. + return () => {}; +} + +/** + * Register an event handler on the window. If there is no window, such as when running in + * a service worker, then no operation is performed. + * + * @param type The event type to register a handler for. + * @param listener The handler to register. + * @param options Event registration options. + * @returns a function which unregisters the handler. + */ +export function addWindowEventListener( + type: string, + listener: (this: Document, ev: Event) => any, + options?: boolean | AddEventListenerOptions, +): () => void { + if (isDocument()) { + window.addEventListener(type, listener, options); + return () => { + window.removeEventListener(type, listener, options); + }; + } + // No document, so no need to unregister anything. + return () => {}; +} + +/** + * For non-window code this will always be an empty string. + */ +export function getHref(): string { + if (isWindow()) { + return window.location.href; + } + return ''; +} + +/** + * For non-window code this will always be an empty string. + */ +export function getLocationSearch(): string { + if (isWindow()) { + return window.location.search; + } + return ''; +} + +/** + * For non-window code this will always be an empty string. + */ +export function getLocationHash(): string { + if (isWindow()) { + return window.location.hash; + } + return ''; +} + +export function getCrypto(): Crypto { + if (typeof crypto !== undefined) { + return crypto; + } + // This would indicate running in an environment that doesn't have window.crypto or self.crypto. + throw Error('Access to a web crypto API is required'); +} + +/** + * Get the visibility state. For non-documents this will always be 'invisible'. + * + * @returns The document visibility. + */ +export function getVisibility(): string { + if (isDocument()) { + return document.visibilityState; + } + return 'visibile'; +} + +export function querySelectorAll(selector: string): NodeListOf | undefined { + if (isDocument()) { + return document.querySelectorAll(selector); + } + return undefined; +} diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index f4a9ee2ce..e0ac2b846 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -17,11 +17,12 @@ import { import BrowserDataManager from './BrowserDataManager'; import { BrowserIdentifyOptions as LDIdentifyOptions } from './BrowserIdentifyOptions'; +import { registerStateDetection } from './BrowserStateDetector'; import GoalManager from './goals/GoalManager'; import { Goal, isClick } from './goals/Goals'; import validateOptions, { BrowserOptions, filterToBaseOptions } from './options'; import BrowserPlatform from './platform/BrowserPlatform'; -import { registerStateDetection } from './BrowserStateDetector'; +import { getHref } from './BrowserApi'; /** * @@ -162,7 +163,7 @@ export class BrowserClient extends LDClientImpl implements LDClient { event.data, event.metricValue, event.samplingRatio, - eventUrlTransformer(window.location.href), + eventUrlTransformer(getHref()), ), }, ); @@ -213,7 +214,9 @@ export class BrowserClient extends LDClientImpl implements LDClient { // would return that promise. this.goalManager.initialize(); - registerStateDetection(() => this.flush()); + if (validatedBrowserOptions.automaticBackgroundHandling) { + registerStateDetection(() => this.flush()); + } } } diff --git a/packages/sdk/browser/src/BrowserStateDetector.ts b/packages/sdk/browser/src/BrowserStateDetector.ts index e49cd1a72..f554a8420 100644 --- a/packages/sdk/browser/src/BrowserStateDetector.ts +++ b/packages/sdk/browser/src/BrowserStateDetector.ts @@ -1,4 +1,6 @@ -export function registerStateDetection(requestFlush: () => void) { +import { addDocumentEventListener, addWindowEventListener, getVisibility } from './BrowserApi'; + +export function registerStateDetection(requestFlush: () => void): () => void { // When the visibility of the page changes to hidden we want to flush any pending events. // // This is handled with visibility, instead of beforeunload/unload @@ -10,11 +12,16 @@ export function registerStateDetection(requestFlush: () => void) { // This also may provide more opportunity for the events to get flushed. // const handleVisibilityChange = () => { - if (document.visibilityState === 'hidden') { + if (getVisibility() === 'hidden') { requestFlush(); } }; - document.addEventListener('visibilitychange', handleVisibilityChange); - window.addEventListener('pagehide', requestFlush); + const removeDocListener = addDocumentEventListener('visibilitychange', handleVisibilityChange); + const removeWindowListener = addWindowEventListener('pagehide', requestFlush); + + return () => { + removeDocListener(); + removeWindowListener(); + }; } diff --git a/packages/sdk/browser/src/goals/GoalManager.ts b/packages/sdk/browser/src/goals/GoalManager.ts index 50932dd0c..4da92261c 100644 --- a/packages/sdk/browser/src/goals/GoalManager.ts +++ b/packages/sdk/browser/src/goals/GoalManager.ts @@ -3,6 +3,7 @@ import { LDUnexpectedResponseError, Requests } from '@launchdarkly/js-client-sdk import { Goal } from './Goals'; import GoalTracker from './GoalTracker'; import { DefaultLocationWatcher, LocationWatcher } from './LocationWatcher'; +import { getHref } from '../BrowserApi'; export default class GoalManager { private goals?: Goal[] = []; @@ -47,7 +48,7 @@ export default class GoalManager { this.tracker?.close(); if (this.goals && this.goals.length) { this.tracker = new GoalTracker(this.goals, (goal) => { - this.reportGoal(window.location.href, goal); + this.reportGoal(getHref(), goal); }); } } diff --git a/packages/sdk/browser/src/goals/GoalTracker.ts b/packages/sdk/browser/src/goals/GoalTracker.ts index 71e7d555f..623a612dc 100644 --- a/packages/sdk/browser/src/goals/GoalTracker.ts +++ b/packages/sdk/browser/src/goals/GoalTracker.ts @@ -1,5 +1,6 @@ import escapeStringRegexp from 'escape-string-regexp'; +import { addDocumentEventListener, getHref, getLocationHash, getLocationSearch, querySelectorAll } from '../BrowserApi'; import { ClickGoal, Goal, Matcher } from './Goals'; type EventHandler = (goal: Goal) => void; @@ -37,11 +38,11 @@ function findGoalsForClick(event: Event, clickGoals: ClickGoal[]) { clickGoals.forEach((goal) => { let target: Node | null = event.target as Node; const { selector } = goal; - const elements = document.querySelectorAll(selector); + const elements = querySelectorAll(selector); // Traverse from the target of the event up the page hierarchy. // If there are no element that match the selector, then no need to check anything. - while (target && elements.length) { + while (target && elements?.length) { // The elements are a NodeList, so it doesn't have the array functions. For performance we // do not convert it to an array. for (let elementIndex = 0; elementIndex < elements.length; elementIndex += 1) { @@ -64,11 +65,11 @@ function findGoalsForClick(event: Event, clickGoals: ClickGoal[]) { * Tracks the goals on an individual "page" (combination of route, query params, and hash). */ export default class GoalTracker { - private clickHandler?: (event: Event) => void; + private cleanup?: () => void; constructor(goals: Goal[], onEvent: EventHandler) { const goalsMatchingUrl = goals.filter((goal) => goal.urls?.some((matcher) => - matchesUrl(matcher, window.location.href, window.location.search, window.location.hash), + matchesUrl(matcher, getHref(), getLocationSearch(), getLocationHash()), ), ); @@ -80,12 +81,12 @@ export default class GoalTracker { if (clickGoals.length) { // Click handler is not a member function in order to avoid having to bind it for the event // handler and then track a reference to that bound handler. - this.clickHandler = (event: Event) => { + const clickHandler = (event: Event) => { findGoalsForClick(event, clickGoals).forEach((clickGoal) => { onEvent(clickGoal); }); }; - document.addEventListener('click', this.clickHandler); + this.cleanup = addDocumentEventListener('click', clickHandler); } } @@ -93,8 +94,8 @@ export default class GoalTracker { * Close the tracker which stops listening to any events. */ close() { - if (this.clickHandler) { - document.removeEventListener('click', this.clickHandler); + if (this.cleanup) { + this.cleanup(); } } } diff --git a/packages/sdk/browser/src/goals/LocationWatcher.ts b/packages/sdk/browser/src/goals/LocationWatcher.ts index 7b6c5e3de..75aceb306 100644 --- a/packages/sdk/browser/src/goals/LocationWatcher.ts +++ b/packages/sdk/browser/src/goals/LocationWatcher.ts @@ -1,3 +1,5 @@ +import { addWindowEventListener, getHref } from '../BrowserApi'; + export const LOCATION_WATCHER_INTERVAL_MS = 300; // Using any for the timer handle because the type is not the same for all @@ -24,9 +26,9 @@ export class DefaultLocationWatcher { * @param callback Callback that is executed whenever a URL change is detected. */ constructor(callback: () => void) { - this.previousLocation = window.location.href; + this.previousLocation = getHref(); const checkUrl = () => { - const currentLocation = window.location.href; + const currentLocation = getHref(); if (currentLocation !== this.previousLocation) { this.previousLocation = currentLocation; @@ -41,10 +43,10 @@ export class DefaultLocationWatcher { */ this.watcherHandle = setInterval(checkUrl, LOCATION_WATCHER_INTERVAL_MS); - window.addEventListener('popstate', checkUrl); + const removeListener = addWindowEventListener('popstate', checkUrl); this.cleanupListeners = () => { - window.removeEventListener('popstate', checkUrl); + removeListener(); }; } diff --git a/packages/sdk/browser/src/options.ts b/packages/sdk/browser/src/options.ts index 1022b8327..3cb4e3568 100644 --- a/packages/sdk/browser/src/options.ts +++ b/packages/sdk/browser/src/options.ts @@ -37,12 +37,25 @@ export interface BrowserOptions extends Omit string; streaming?: boolean; + automaticBackgroundHandling?: boolean; } const optDefaults = { diff --git a/packages/sdk/browser/src/platform/BrowserCrypto.ts b/packages/sdk/browser/src/platform/BrowserCrypto.ts index e241bc50a..29695655c 100644 --- a/packages/sdk/browser/src/platform/BrowserCrypto.ts +++ b/packages/sdk/browser/src/platform/BrowserCrypto.ts @@ -1,11 +1,12 @@ import { Crypto } from '@launchdarkly/js-client-sdk-common'; +import { getCrypto } from '../BrowserApi'; import BrowserHasher from './BrowserHasher'; import randomUuidV4 from './randomUuidV4'; export default class BrowserCrypto implements Crypto { createHash(algorithm: string): BrowserHasher { - return new BrowserHasher(window.crypto, algorithm); + return new BrowserHasher(getCrypto(), algorithm); } randomUUID(): string { From da3ea00b00583ed88e10a1d76ea3c613309f4547 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:23:34 -0700 Subject: [PATCH 3/6] Fix goals test, remove state detector tests. --- .../sdk/browser/__tests__/BrowserStateDetector.test.ts | 0 packages/sdk/browser/__tests__/goals/GoalTracker.test.ts | 8 ++++++-- 2 files changed, 6 insertions(+), 2 deletions(-) delete mode 100644 packages/sdk/browser/__tests__/BrowserStateDetector.test.ts diff --git a/packages/sdk/browser/__tests__/BrowserStateDetector.test.ts b/packages/sdk/browser/__tests__/BrowserStateDetector.test.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/sdk/browser/__tests__/goals/GoalTracker.test.ts b/packages/sdk/browser/__tests__/goals/GoalTracker.test.ts index 23491abf2..fbad973f4 100644 --- a/packages/sdk/browser/__tests__/goals/GoalTracker.test.ts +++ b/packages/sdk/browser/__tests__/goals/GoalTracker.test.ts @@ -78,7 +78,7 @@ it('should add click event listener for click goals', () => { new GoalTracker(goals, mockOnEvent); - expect(document.addEventListener).toHaveBeenCalledWith('click', expect.any(Function)); + expect(document.addEventListener).toHaveBeenCalledWith('click', expect.any(Function), undefined); }); it('should not add click event listener if no click goals', () => { @@ -175,7 +175,11 @@ it('should remove click event listener on close', () => { const tracker = new GoalTracker(goals, mockOnEvent); tracker.close(); - expect(document.removeEventListener).toHaveBeenCalledWith('click', expect.any(Function)); + expect(document.removeEventListener).toHaveBeenCalledWith( + 'click', + expect.any(Function), + undefined, + ); }); it('should trigger the click goal for parent elements which match the selector', () => { From cabf104f6f8304bca50ae580983588dd27be8135 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:31:10 -0700 Subject: [PATCH 4/6] Lint --- packages/sdk/browser/src/BrowserClient.ts | 2 +- packages/sdk/browser/src/goals/GoalManager.ts | 2 +- packages/sdk/browser/src/goals/GoalTracker.ts | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index e0ac2b846..43ec5eb88 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -15,6 +15,7 @@ import { Platform, } from '@launchdarkly/js-client-sdk-common'; +import { getHref } from './BrowserApi'; import BrowserDataManager from './BrowserDataManager'; import { BrowserIdentifyOptions as LDIdentifyOptions } from './BrowserIdentifyOptions'; import { registerStateDetection } from './BrowserStateDetector'; @@ -22,7 +23,6 @@ import GoalManager from './goals/GoalManager'; import { Goal, isClick } from './goals/Goals'; import validateOptions, { BrowserOptions, filterToBaseOptions } from './options'; import BrowserPlatform from './platform/BrowserPlatform'; -import { getHref } from './BrowserApi'; /** * diff --git a/packages/sdk/browser/src/goals/GoalManager.ts b/packages/sdk/browser/src/goals/GoalManager.ts index 4da92261c..1862cfaa8 100644 --- a/packages/sdk/browser/src/goals/GoalManager.ts +++ b/packages/sdk/browser/src/goals/GoalManager.ts @@ -1,9 +1,9 @@ import { LDUnexpectedResponseError, Requests } from '@launchdarkly/js-client-sdk-common'; +import { getHref } from '../BrowserApi'; import { Goal } from './Goals'; import GoalTracker from './GoalTracker'; import { DefaultLocationWatcher, LocationWatcher } from './LocationWatcher'; -import { getHref } from '../BrowserApi'; export default class GoalManager { private goals?: Goal[] = []; diff --git a/packages/sdk/browser/src/goals/GoalTracker.ts b/packages/sdk/browser/src/goals/GoalTracker.ts index 623a612dc..08cda05bd 100644 --- a/packages/sdk/browser/src/goals/GoalTracker.ts +++ b/packages/sdk/browser/src/goals/GoalTracker.ts @@ -1,6 +1,12 @@ import escapeStringRegexp from 'escape-string-regexp'; -import { addDocumentEventListener, getHref, getLocationHash, getLocationSearch, querySelectorAll } from '../BrowserApi'; +import { + addDocumentEventListener, + getHref, + getLocationHash, + getLocationSearch, + querySelectorAll, +} from '../BrowserApi'; import { ClickGoal, Goal, Matcher } from './Goals'; type EventHandler = (goal: Goal) => void; From 1ab7c0f04388b970bf2400ce31c7b395e38e6c0b Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:34:53 -0700 Subject: [PATCH 5/6] Account for keepalive in common tests. --- .../common/__tests__/internal/events/EventSender.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/shared/common/__tests__/internal/events/EventSender.test.ts b/packages/shared/common/__tests__/internal/events/EventSender.test.ts index 2e4c3f3dd..63a6130dc 100644 --- a/packages/shared/common/__tests__/internal/events/EventSender.test.ts +++ b/packages/shared/common/__tests__/internal/events/EventSender.test.ts @@ -135,6 +135,7 @@ describe('given an event sender', () => { body: JSON.stringify(testEventData1), headers: analyticsHeaders(uuid), method: 'POST', + keepalive: true, }); }); @@ -151,6 +152,7 @@ describe('given an event sender', () => { body: JSON.stringify(testEventData1), headers: analyticsHeaders(uuid), method: 'POST', + keepalive: true, }); expect(mockFetch).toHaveBeenNthCalledWith( 2, @@ -159,6 +161,7 @@ describe('given an event sender', () => { body: JSON.stringify(testEventData2), headers: diagnosticHeaders, method: 'POST', + keepalive: true, }, ); }); From 77564da153d661cdd1d51483790970cd8d3c55d3 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:39:03 -0700 Subject: [PATCH 6/6] Make cleanup smaller. --- packages/sdk/browser/src/goals/GoalTracker.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/sdk/browser/src/goals/GoalTracker.ts b/packages/sdk/browser/src/goals/GoalTracker.ts index 08cda05bd..268d0b68a 100644 --- a/packages/sdk/browser/src/goals/GoalTracker.ts +++ b/packages/sdk/browser/src/goals/GoalTracker.ts @@ -100,8 +100,6 @@ export default class GoalTracker { * Close the tracker which stops listening to any events. */ close() { - if (this.cleanup) { - this.cleanup(); - } + this.cleanup?.(); } }