From 85e64bf3211e1f7ce31245421a0c035f9a20a8e7 Mon Sep 17 00:00:00 2001 From: Nev Wylie <54870357+MSNev@users.noreply.github.com> Date: Tue, 5 Oct 2021 13:53:02 -0700 Subject: [PATCH] Add support to optionally configure the events used for detecting and handling when page unload and flushing occurs #1683 --- AISKU/src/Initialization.ts | 23 ++-- .../src/Interfaces/IConfig.ts | 19 ++- .../IConfiguration.ts | 14 +++ .../src/JavaScriptSDK/CoreUtils.ts | 116 ++++++++++++++++++ .../src/applicationinsights-core-js.ts | 3 +- 5 files changed, 161 insertions(+), 14 deletions(-) diff --git a/AISKU/src/Initialization.ts b/AISKU/src/Initialization.ts index f4e6acb00..4d58a418d 100644 --- a/AISKU/src/Initialization.ts +++ b/AISKU/src/Initialization.ts @@ -4,7 +4,7 @@ import { IConfiguration, AppInsightsCore, IAppInsightsCore, LoggingSeverity, _InternalMessageId, ITelemetryItem, ICustomProperties, IChannelControls, hasWindow, hasDocument, isReactNative, doPerf, IDiagnosticLogger, INotificationManager, objForEachKey, proxyAssign, - arrForEach, isString, isFunction, isNullOrUndefined, addEventHandler, isArray, throwError, ICookieMgr, safeGetCookieMgr + arrForEach, isString, isFunction, isNullOrUndefined, isArray, throwError, ICookieMgr, addPageUnloadEventListener, addPageHideEventListener } from "@microsoft/applicationinsights-core-js"; import { ApplicationInsights } from "@microsoft/applicationinsights-analytics-js"; import { Sender } from "@microsoft/applicationinsights-channel-js"; @@ -497,13 +497,16 @@ export class Initialization implements IApplicationInsights { }); }; + let added = false; + let excludePageUnloadEvents = appInsightsInstance.appInsights.config.disablePageUnloadEvents; + if (!appInsightsInstance.appInsights.config.disableFlushOnBeforeUnload) { // Hook the unload event for the document, window and body to ensure that the client events are flushed to the server - // As just hooking the window does not always fire (on chrome) for page navigations. - let added = addEventHandler('beforeunload', performHousekeeping); - added = addEventHandler('unload', performHousekeeping) || added; - added = addEventHandler('pagehide', performHousekeeping) || added; - added = addEventHandler('visibilitychange', performHousekeeping) || added; + // As just hooking the window does not always fire (on chrome) for page navigation's. + added = addPageUnloadEventListener(performHousekeeping, excludePageUnloadEvents); + + // We also need to hook the pagehide and visibilitychange events as not all versions of Safari support load/unload events. + added = addPageHideEventListener(performHousekeeping, excludePageUnloadEvents) || added; // A reactNative app may not have a window and therefore the beforeunload/pagehide events -- so don't // log the failure in this case @@ -515,11 +518,9 @@ export class Initialization implements IApplicationInsights { } } - // We also need to hook the pagehide and visibilitychange events as not all versions of Safari support load/unload events. - if (!appInsightsInstance.appInsights.config.disableFlushOnUnload) { - // Not adding any telemetry as pagehide as it's not supported on all browsers - addEventHandler('pagehide', performHousekeeping); - addEventHandler('visibilitychange', performHousekeeping); + if (!added && !appInsightsInstance.appInsights.config.disableFlushOnUnload) { + // If we didn't add the normal set then attempt to add the pagehide and visibilitychange only + addPageHideEventListener(performHousekeeping, excludePageUnloadEvents); } } } diff --git a/shared/AppInsightsCommon/src/Interfaces/IConfig.ts b/shared/AppInsightsCommon/src/Interfaces/IConfig.ts index 41cdf0a90..ccd9d2f70 100644 --- a/shared/AppInsightsCommon/src/Interfaces/IConfig.ts +++ b/shared/AppInsightsCommon/src/Interfaces/IConfig.ts @@ -140,15 +140,30 @@ export interface IConfig { correlationHeaderExcludedDomains?: string[]; /** - * Default false. If true, flush method will not be called when onBeforeUnload event triggers. + * Default false. If true, flush method will not be called when onBeforeUnload, onUnload, onPageHide or onVisibilityChange (hidden state) event(s) trigger. */ disableFlushOnBeforeUnload?: boolean; /** - * Default value of {@link #disableFlushOnBeforeUnload}. If true, flush method will not be called when onUnload event triggers. + * Default value of {@link #disableFlushOnBeforeUnload}. If true, flush method will not be called when onPageHide or onVisibilityChange (hidden state) event(s) trigger. */ disableFlushOnUnload?: boolean; + /** + * [Optional] An array of the page unload events that you would like to be ignored, special note there must be at least one valid unload + * event hooked, if you list all or the runtime environment only supports a listed "disabled" event it will still be hooked if required by the SDK. + * (Some page unload functionality may be disabled via disableFlushOnBeforeUnload or disableFlushOnUnload config entries) + * Unload events include "beforeunload", "unload", "visibilitychange" (with 'hidden' state) and "pagehide" + */ + disablePageUnloadEvents?: string[]; + + /** + * [Optional] An array of page show events that you would like to be ignored, special note there must be at lease one valid show event + * hooked, if you list all or the runtime environment only supports a listed (disabled) event it will STILL be hooked if required by the SDK. + * Page Show events include "pageshow" and "visibilitychange" (with 'visible' state) + */ + disablePageShowEvents?: string[]; + /** * If true, the buffer with all unsent telemetry is stored in session storage. The buffer is restored on page load. Default is true. * @defaultValue true diff --git a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts index 69d3b42ee..3b04b2708 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts @@ -149,4 +149,18 @@ export interface IConfiguration { * cookieDomain and disableCookiesUsage values. */ cookieCfg?: ICookieMgrConfig; + + /** + * [Optional] An array of the page unload events that you would like to be ignored, special note there must be at least one valid unload + * event hooked, if you list all or the runtime environment only supports a listed "disabled" event it will still be hooked, if required by the SDK. + * Unload events include "beforeunload", "unload", "visibilitychange" (with 'hidden' state) and "pagehide" + */ + disablePageUnloadEvents?: string[]; + + /** + * [Optional] An array of page show events that you would like to be ignored, special note there must be at lease one valid show event + * hooked, if you list all or the runtime environment only supports a listed (disabled) event it will STILL be hooked, if required by the SDK. + * Page Show events include "pageshow" and "visibilitychange" (with 'visible' state) + */ + disablePageShowEvents?: string[]; } \ No newline at end of file diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/CoreUtils.ts b/shared/AppInsightsCore/src/JavaScriptSDK/CoreUtils.ts index b725c165f..abefcff7a 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/CoreUtils.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/CoreUtils.ts @@ -14,6 +14,10 @@ import { } from "./HelperFuncs"; import { randomValue, random32, mwcRandomSeed, mwcRandom32 } from "./RandomHelper"; +const strVisibilityChangeEvt: string = "visibilitychange"; +const strPageHide: string = "pagehide"; +const strPageShow: string = "pageshow"; + let _cookieMgrs: ICookieMgr[] = null; let _canUseCookies: boolean; // legacy supported config @@ -42,6 +46,118 @@ export function addEventHandler(eventName: string, callback: any): boolean { return result; } +/** + * Bind the listener to the array of events + * @param events An string array of event names to bind the listener to + * @param listener The event callback to call when the event is triggered + * @param excludeEvents - [Optional] An array of events that should not be hooked (if possible), unless no other events can be. + * @returns true - when at least one of the events was registered otherwise false + */ + export function addEventListeners(events: string[], listener: any, excludeEvents?: string[]): boolean { + let added = false; + + if (listener && events && isArray(events)) { + let excluded: string[] = []; + arrForEach(events, (name) => { + if (isString(name)) { + if (!excludeEvents || arrIndexOf(excludeEvents, name) === -1) { + added = addEventHandler(name, listener) || added; + } else { + excluded.push(name); + } + } + }); + + if (!added && excluded.length > 0) { + // Failed to add any listeners and we excluded some, so just attempt to add the excluded events + added = addEventListeners(excluded, listener); + } + } + + return added; +} + +/** + * Listen to the 'beforeunload', 'unload' and 'pagehide' events which indicates a page unload is occurring, + * this does NOT listen to the 'visibilitychange' event as while it does indicate that the page is being hidden + * it does not *necessarily* mean that the page is being completely unloaded, it can mean that the user is + * just navigating to a different Tab and may come back (without unloading the page). As such you may also + * need to listen to the 'addPageHideEventListener' and 'addPageShowEventListener' events. + * @param listener - The event callback to call when a page unload event is triggered + * @param excludeEvents - [Optional] An array of events that should not be hooked, unless no other events can be. + * @returns true - when at least one of the events was registered otherwise false + */ +export function addPageUnloadEventListener(listener: any, excludeEvents?: string[]): boolean { + // Hook the unload event for the document, window and body to ensure that the client events are flushed to the server + // As just hooking the window does not always fire (on chrome) for page navigation's. + return addEventListeners(["beforeunload", "unload", "pagehide"], listener, excludeEvents); +} + +/** + * Listen to the pagehide and visibility changing to 'hidden' events + * @param listener - The event callback to call when a page hide event is triggered + * @param excludeEvents - [Optional] An array of events that should not be hooked (if possible), unless no other events can be. + * Suggestion: pass as true if you are also calling addPageUnloadEventListener as that also hooks pagehide + * @returns true - when at least one of the events was registered otherwise false + */ + export function addPageHideEventListener(listener: any, excludeEvents?: string[]): boolean { + + function _handlePageVisibility(evt: any) { + let doc = getDocument(); + if (listener && doc && doc.visibilityState === 'hidden') { + listener(evt); + } + } + + let pageUnloadAdded = false; + if (!excludeEvents || arrIndexOf(excludeEvents, strPageHide) === -1) { + pageUnloadAdded = addEventHandler(strPageHide, listener); + } + + if (!excludeEvents || arrIndexOf(excludeEvents, strVisibilityChangeEvt) === -1) { + pageUnloadAdded = addEventHandler(strVisibilityChangeEvt, _handlePageVisibility) || pageUnloadAdded; + } + + if (!pageUnloadAdded && excludeEvents) { + // Failed to add any listeners and we where requested to exclude some, so just call again without excluding anything + pageUnloadAdded = addPageHideEventListener(listener); + } + + return pageUnloadAdded; +} + +/** + * Listen to the pageshow and visibility changing to 'visible' events + * @param listener - The event callback to call when a page is show event is triggered + * @param excludeEvents - [Optional] An array of events that should not be hooked (if possible), unless no other events can be. + * @returns true - when at least one of the events was registered otherwise false + */ + export function addPageShowEventListener(listener: any, excludeEvents?: string[]): boolean { + + function _handlePageVisibility(evt: any) { + let doc = getDocument(); + if (listener && doc && doc.visibilityState === 'visible') { + listener(evt); + } + } + + let pageShowAdded = false; + if (!excludeEvents || arrIndexOf(excludeEvents, strPageShow) === -1) { + pageShowAdded = addEventHandler(strPageShow, listener); + } + + if (!excludeEvents || arrIndexOf(excludeEvents, strVisibilityChangeEvt) === -1) { + pageShowAdded = addEventHandler(strVisibilityChangeEvt, _handlePageVisibility) || pageShowAdded; + } + + if (!pageShowAdded && excludeEvents) { + // Failed to add any listeners and we where requested to exclude some, so just call again without excluding anything + pageShowAdded = addPageShowEventListener(listener); + } + + return pageShowAdded; +} + export function newGuid(): string { function randomHexDigit() { return randomValue(15); // Get a random value from 0..15 diff --git a/shared/AppInsightsCore/src/applicationinsights-core-js.ts b/shared/AppInsightsCore/src/applicationinsights-core-js.ts index 310b2ddd2..b157c56c6 100644 --- a/shared/AppInsightsCore/src/applicationinsights-core-js.ts +++ b/shared/AppInsightsCore/src/applicationinsights-core-js.ts @@ -18,7 +18,8 @@ export { BaseTelemetryPlugin } from './JavaScriptSDK/BaseTelemetryPlugin'; export { randomValue, random32, mwcRandomSeed, mwcRandom32 } from './JavaScriptSDK/RandomHelper'; export { CoreUtils, ICoreUtils, EventHelper, IEventHelper, Undefined, addEventHandler, newGuid, perfNow, newId, generateW3CId, - disableCookies, canUseCookies, getCookie, setCookie, deleteCookie, _legacyCookieMgr + disableCookies, canUseCookies, getCookie, setCookie, deleteCookie, _legacyCookieMgr, addEventListeners, addPageUnloadEventListener, + addPageHideEventListener, addPageShowEventListener } from "./JavaScriptSDK/CoreUtils"; export { isTypeof, isUndefined, isNullOrUndefined, hasOwnProperty, isObject, isFunction, attachEvent, detachEvent, normalizeJsName,