diff --git a/package.json b/package.json index 42e0ae3b..40696e96 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ }, "dependencies": { "escape-string-regexp": "^4.0.0", - "launchdarkly-js-sdk-common": "5.0.0" + "launchdarkly-js-sdk-common": "5.0.1" }, "repository": { "type": "git", diff --git a/src/__tests__/LDClient-test.js b/src/__tests__/LDClient-test.js index 0c195e52..2d0554e0 100644 --- a/src/__tests__/LDClient-test.js +++ b/src/__tests__/LDClient-test.js @@ -173,12 +173,18 @@ describe('LDClient', () => { expect(server.requests[1].async).toBe(true); }); - async function setupClientAndTriggerUnload() { + async function setupClientAndTriggerPageHide() { const config = { bootstrap: {}, flushInterval: 100000, fetchGoals: false, diagnosticOptOut: true }; const client = LDClient.initialize(envName, user, config); await client.waitForInitialization(); - window.dispatchEvent(new window.Event('beforeunload')); + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + document.dispatchEvent(new window.Event('visibilitychange')); return client; } @@ -188,7 +194,7 @@ describe('LDClient', () => { it('in ' + desc, async () => { window.navigator.__defineGetter__('userAgent', () => ua); - await setupClientAndTriggerUnload(); + await setupClientAndTriggerPageHide(); expect(server.requests.length).toEqual(2); // ignore first request because it's just a side effect of calling browserPlatform.httpAllowsPost() @@ -211,7 +217,7 @@ describe('LDClient', () => { it('in ' + desc, async () => { window.navigator.__defineGetter__('userAgent', () => ua); - const client = await setupClientAndTriggerUnload(); + const client = await setupClientAndTriggerPageHide(); expect(server.requests.length).toEqual(2); // ignore first request because it's just a side effect of calling browserPlatform.httpAllowsPost() expect(server.requests[1].async).toBe(false); // events @@ -237,7 +243,7 @@ describe('LDClient', () => { it('in ' + desc, async () => { window.navigator.__defineGetter__('userAgent', () => ua); - await setupClientAndTriggerUnload(); + await setupClientAndTriggerPageHide(); window.dispatchEvent(new window.Event('beforeunload')); diff --git a/src/browserPlatform.js b/src/browserPlatform.js index fb2dc42f..cc2dad7c 100644 --- a/src/browserPlatform.js +++ b/src/browserPlatform.js @@ -5,7 +5,7 @@ export default function makeBrowserPlatform(options) { userAgentHeaderName: 'X-LaunchDarkly-User-Agent', }; - ret.synchronousFlush = false; // this will be set to true by index.js if the page is closing + ret.synchronousFlush = false; // this will be set to true by index.js if the page is hidden // XMLHttpRequest may not exist if we're running in a server-side rendering context if (window.XMLHttpRequest) { diff --git a/src/index.js b/src/index.js index e4084c84..2ac4556c 100644 --- a/src/index.js +++ b/src/index.js @@ -43,20 +43,33 @@ export function initialize(env, user, options = {}) { clientVars.start(); } - // We'll attempt to flush events via synchronous HTTP if the page is about to close, to improve - // the chance that the events will really be delivered, although synchronous requests aren't - // supported in all browsers (see httpRequest.js). We will do it for both beforeunload and - // unload, in case any events got generated by code that ran in another beforeunload handler. - // We will not call client.close() though, since in the case of a beforeunload event the page - // might not actually get closed, and with an unload event we know everything will get discarded - // anyway. - const syncFlushHandler = () => { + const syncFlush = () => { + // Synchronous events are not available in all browsers, but where they + // are we should attempt to use them. This increases the chance of the events + // being delivered. platform.synchronousFlush = true; client.flush().catch(() => {}); platform.synchronousFlush = false; }; - window.addEventListener('beforeunload', syncFlushHandler); - window.addEventListener('unload', syncFlushHandler); + + // 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') { + syncFlush(); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + window.addEventListener('pagehide', syncFlush); return client; }