Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to optionally configure the events used for detecting and handling when page unload and flushing occurs #1683 #1684

Merged
merged 2 commits into from
Oct 7, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
Comment on lines +71 to +74
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curios, why are we doing this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We attempt to "listen" to any and all unload / show events, however, if the config provides a list of events that would "exclude" events (based on the current runtime environment (browser)) then we would end up listening to nothing.

So the "exclude" events is treated as a best effort.

  • If at least one event can be hooked (excluding the requested), then we are happy and continue
  • If no event can be hooked (because they all got excluded or the environment doesn't support then), then we try and hook all of the original events (including the requested excluded set)

}

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