Skip to content

Commit

Permalink
Add support to optionally configure the events used for detecting and…
Browse files Browse the repository at this point in the history
… handling when page unload and flushing occurs #1683 (#1684)
  • Loading branch information
MSNev committed Oct 7, 2021
1 parent 25e8375 commit fa911a0
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 14 deletions.
23 changes: 12 additions & 11 deletions AISKU/src/Initialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}
}
Expand Down
19 changes: 17 additions & 2 deletions shared/AppInsightsCommon/src/Interfaces/IConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
116 changes: 116 additions & 0 deletions shared/AppInsightsCore/src/JavaScriptSDK/CoreUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion shared/AppInsightsCore/src/applicationinsights-core-js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit fa911a0

Please sign in to comment.