/*
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
* Licensed under the MIT License.
|
*/
|
|
import {
|
AccountInfo,
|
Constants,
|
InProgressPerformanceEvent,
|
IPerformanceClient,
|
Logger,
|
PerformanceClient,
|
PerformanceEvent,
|
SubMeasurement,
|
} from "@azure/msal-common/browser";
|
import { Configuration } from "../config/Configuration.js";
|
import { name, version } from "../packageMetadata.js";
|
import { BrowserCacheLocation } from "../utils/BrowserConstants.js";
|
import * as BrowserCrypto from "../crypto/BrowserCrypto.js";
|
import { BROWSER_PERF_ENABLED_KEY } from "../cache/CacheKeys.js";
|
import { getNetworkInfo } from "../utils/MsalFrameStatsUtils.js";
|
|
/**
|
* Returns browser performance measurement module if session flag is enabled. Returns undefined otherwise.
|
*/
|
function getPerfMeasurementModule() {
|
let sessionStorage: Storage | undefined;
|
try {
|
sessionStorage = window[BrowserCacheLocation.SessionStorage];
|
const perfEnabled = sessionStorage?.getItem(BROWSER_PERF_ENABLED_KEY);
|
if (Number(perfEnabled) === 1) {
|
return import("./BrowserPerformanceMeasurement.js");
|
}
|
// Mute errors if it's a non-browser environment or cookies are blocked.
|
} catch (e) {}
|
|
return undefined;
|
}
|
|
/**
|
* Returns boolean, indicating whether browser supports window.performance.now() function.
|
*/
|
function supportsBrowserPerformanceNow(): boolean {
|
return (
|
typeof window !== "undefined" &&
|
typeof window.performance !== "undefined" &&
|
typeof window.performance.now === "function"
|
);
|
}
|
|
/**
|
* Returns event duration in milliseconds using window performance API if available. Returns undefined otherwise.
|
* @param startTime {DOMHighResTimeStamp | undefined}
|
* @returns {number | undefined}
|
*/
|
function getPerfDurationMs(
|
startTime: DOMHighResTimeStamp | undefined
|
): number | undefined {
|
if (!startTime || !supportsBrowserPerformanceNow()) {
|
return undefined;
|
}
|
|
return Math.round(window.performance.now() - startTime);
|
}
|
|
export class BrowserPerformanceClient
|
extends PerformanceClient
|
implements IPerformanceClient
|
{
|
constructor(configuration: Configuration, intFields?: Set<string>) {
|
super(
|
configuration.auth.clientId,
|
configuration.auth.authority || `${Constants.DEFAULT_AUTHORITY}`,
|
new Logger(
|
configuration.system?.loggerOptions || {},
|
name,
|
version
|
),
|
name,
|
version,
|
configuration.telemetry?.application || {
|
appName: "",
|
appVersion: "",
|
},
|
intFields
|
);
|
}
|
|
generateId(): string {
|
return BrowserCrypto.createNewGuid();
|
}
|
|
private getPageVisibility(): string | null {
|
return document.visibilityState?.toString() || null;
|
}
|
|
private getOnlineStatus(): boolean | null {
|
return typeof navigator !== "undefined" ? navigator.onLine : null;
|
}
|
|
private deleteIncompleteSubMeasurements(
|
inProgressEvent: InProgressPerformanceEvent
|
): void {
|
void getPerfMeasurementModule()?.then((module) => {
|
const rootEvent = this.eventsByCorrelationId.get(
|
inProgressEvent.event.correlationId
|
);
|
const isRootEvent =
|
rootEvent &&
|
rootEvent.eventId === inProgressEvent.event.eventId;
|
const incompleteMeasurements: SubMeasurement[] = [];
|
if (isRootEvent && rootEvent?.incompleteSubMeasurements) {
|
rootEvent.incompleteSubMeasurements.forEach(
|
(subMeasurement: SubMeasurement) => {
|
incompleteMeasurements.push({ ...subMeasurement });
|
}
|
);
|
}
|
// Clean up remaining marks for incomplete sub-measurements
|
module.BrowserPerformanceMeasurement.flushMeasurements(
|
inProgressEvent.event.correlationId,
|
incompleteMeasurements
|
);
|
});
|
}
|
|
/**
|
* Starts measuring performance for a given operation. Returns a function that should be used to end the measurement.
|
* Also captures browser page visibilityState.
|
*
|
* @param {PerformanceEvents} measureName
|
* @param {?string} [correlationId]
|
* @returns {((event?: Partial<PerformanceEvent>) => PerformanceEvent| null)}
|
*/
|
startMeasurement(
|
measureName: string,
|
correlationId?: string
|
): InProgressPerformanceEvent {
|
// Capture page visibilityState and then invoke start/end measurement
|
const startPageVisibility = this.getPageVisibility();
|
const startOnlineStatus = this.getOnlineStatus();
|
const inProgressEvent = super.startMeasurement(
|
measureName,
|
correlationId
|
);
|
const startTime: number | undefined = supportsBrowserPerformanceNow()
|
? window.performance.now()
|
: undefined;
|
|
const browserMeasurement = getPerfMeasurementModule()?.then(
|
(module) => {
|
return new module.BrowserPerformanceMeasurement(
|
measureName,
|
inProgressEvent.event.correlationId
|
);
|
}
|
);
|
void browserMeasurement?.then((measurement) =>
|
measurement.startMeasurement()
|
);
|
|
return {
|
...inProgressEvent,
|
end: (
|
event?: Partial<PerformanceEvent>,
|
error?: unknown,
|
account?: AccountInfo
|
): PerformanceEvent | null => {
|
const networkInfo = getNetworkInfo();
|
const res = inProgressEvent.end(
|
{
|
...event,
|
startPageVisibility,
|
startOnlineStatus,
|
endPageVisibility: this.getPageVisibility(),
|
durationMs: getPerfDurationMs(startTime),
|
networkEffectiveType: networkInfo.effectiveType,
|
networkRtt: networkInfo.rtt,
|
},
|
error,
|
account
|
);
|
void browserMeasurement?.then((measurement) =>
|
measurement.endMeasurement()
|
);
|
this.deleteIncompleteSubMeasurements(inProgressEvent);
|
|
return res;
|
},
|
discard: () => {
|
inProgressEvent.discard();
|
void browserMeasurement?.then((measurement) =>
|
measurement.flushMeasurement()
|
);
|
this.deleteIncompleteSubMeasurements(inProgressEvent);
|
},
|
};
|
}
|
}
|