@microsoft/applicationinsights-core-js
Version:
Microsoft Application Insights Core Javascript SDK
350 lines (348 loc) • 16.8 kB
JavaScript
/*
* Application Insights JavaScript SDK - Core, 3.3.9
* Copyright (c) Microsoft and contributors. All rights reserved.
*/
import { arrForEach, isNumber, makeGlobRegex, objDefineProps, scheduleTimeout, strIndexOf, strLower, utcNow } from "@nevware21/ts-utils";
import { onConfigChange } from "../Config/DynamicConfig";
import { _DYN_CANCEL, _DYN_EXCEPTION, _DYN_FAILURE, _DYN_INDEX_OF, _DYN_IS_INITIALIZED, _DYN_REQUEST_DURATION, _DYN_THROTTLE, _DYN_TOTAL_REQUEST, _DYN_TYPE } from "../__DynamicConstants";
import { _throwInternal, safeGetLogger } from "./DiagnosticLogger";
import { isFeatureEnabled } from "./HelperFuncs";
import { STR_EMPTY } from "./InternalConstants";
var STATS_COLLECTION_SHORT_INTERVAL = 900000; // 15 minutes
var STATS_MIN_INTERVAL_SECONDS = 60; // 1 minute
var STATSBEAT_LANGUAGE = "JavaScript";
var STATSBEAT_TYPE = "Browser";
/**
* This function checks if the provided endpoint matches the provided urlMatch. It
* compares the endpoint with the urlMatch in a case-insensitive manner and also checks
* if the endpoint is a substring of the urlMatch. The urlMatch can also be a regex
* pattern, in which case it will be checked against the endpoint using regex.
* @param endpoint - The endpoint to check against the URL.
* @param urlMatch - The URL to check against the endpoint.
* @returns true if the URL matches the endpoint, false otherwise.
*/
function _isMatchEndpoint(endpoint, urlMatch) {
var lwrUrl = strLower(urlMatch);
// Check if the endpoint is a substring of the URL
if (strIndexOf(endpoint, lwrUrl) !== -1) {
return true;
}
// If it looks like a regex pattern, check if the endpoint matches the regex
if (strIndexOf(lwrUrl, "*") != -1 || strIndexOf(lwrUrl, "?") != -1) {
// Check if the endpoint is a regex pattern
var regex = makeGlobRegex(lwrUrl);
if (regex.test(endpoint)) {
return true;
}
}
return false;
}
/**
* Creates a new INetworkStatsbeat instance with the specified host.
* @param host - The host for the INetworkStatsbeat instance.
* @returns A new INetworkStatsbeat instance.
*/
function _createNetworkStatsbeat(host) {
return {
host: host,
totalRequest: 0,
success: 0,
throttle: {},
failure: {},
retry: {},
exception: {},
requestDuration: 0
};
}
/**
* Creates a new IStatsBeat instance with the specified manager callbacks and statsbeat state.
* @param mgr - The manager callbacks to use for the IStatsBeat instance.
* @param statsBeatStats - The statsbeat state to use for the IStatsBeat instance.
* @returns A new IStatsBeat instance.
*/
function _createStatsBeat(mgr, statsBeatStats) {
var _networkCounter = _createNetworkStatsbeat(statsBeatStats.endpoint);
var _timeoutHandle; // Handle to the timer for sending telemetry. This way, we would not send telemetry when system sleep.
var _isEnabled = true; // Flag to check if statsbeat is enabled or not
function _setupTimer() {
if (_isEnabled && !_timeoutHandle) {
_timeoutHandle = mgr.start(function () {
_timeoutHandle = null;
trackStatsbeats();
});
}
}
function trackStatsbeats() {
if (_isEnabled) {
_trackSendRequestDuration();
_trackSendRequestsCount();
_networkCounter = _createNetworkStatsbeat(_networkCounter.host);
_timeoutHandle && _timeoutHandle[_DYN_CANCEL /* @min:%2ecancel */]();
_timeoutHandle = null;
}
}
/**
* This is a simple helper that checks if the currently reporting endpoint is the same as this instance was
* created with. This is used to ensure that we only send statsbeat events to the endpoint that was used
* when the instance was created. This is important as the endpoint can change during the lifetime of the
* instance and we don't want to send statsbeat events to the wrong endpoint.
* @param endpoint
* @returns true if the endpoint is the same as the one used to create the instance, false otherwise
*/
function _checkEndpoint(endpoint) {
return _networkCounter.host === endpoint;
}
/**
* Attempt to send statsbeat events to the server. This is done by creating a new event and sending it to the core.
* The event is created with the name and value passed in, and any additional properties are added to the event as well.
* This will only send the event when
* - the statsbeat is enabled
* - the statsbeat key is set for the current endpoint
* - the value is greater than 0
* @param name - The name of the event to send
* @param val - The value of the event to send
* @param properties - Optional additional properties to add to the event
*/
function _sendStatsbeats(name, val, properties) {
if (_isEnabled && val && val > 0) {
// Add extra properties
var baseProperties = {
"rp": "unknown",
"attach": "Manual",
"cikey": statsBeatStats.cKey,
"os": STATSBEAT_TYPE,
"language": STATSBEAT_LANGUAGE,
"version": statsBeatStats.sdkVer || "unknown",
"endpoint": "breeze",
"host": _networkCounter.host
};
// Manually merge properties instead of using spread syntax
var combinedProps = { "host": _networkCounter.host };
// Add properties if present
if (properties) {
for (var key in properties) {
if (properties.hasOwnProperty(key)) {
combinedProps[key] = properties[key];
}
}
}
// Add base properties
for (var key in baseProperties) {
if (baseProperties.hasOwnProperty(key)) {
combinedProps[key] = baseProperties[key];
}
}
var statsbeatEvent = {
name: name,
baseData: {
name: name,
average: val,
properties: combinedProps
},
baseType: "MetricData"
};
mgr.track(statsBeat, statsbeatEvent);
}
}
function _trackSendRequestDuration() {
var totalRequest = _networkCounter[_DYN_TOTAL_REQUEST /* @min:%2etotalRequest */];
if (_networkCounter[_DYN_TOTAL_REQUEST /* @min:%2etotalRequest */] > 0) {
var averageRequestExecutionTime = _networkCounter[_DYN_REQUEST_DURATION /* @min:%2erequestDuration */] / totalRequest;
_sendStatsbeats("Request_Duration", averageRequestExecutionTime);
}
}
function _trackSendRequestsCount() {
var currentCounter = _networkCounter;
_sendStatsbeats("Request_Success_Count", currentCounter.success);
for (var code in currentCounter[_DYN_FAILURE /* @min:%2efailure */]) {
var count = currentCounter[_DYN_FAILURE /* @min:%2efailure */][code];
_sendStatsbeats("failure", count, { statusCode: code });
}
for (var code in currentCounter.retry) {
var count = currentCounter.retry[code];
_sendStatsbeats("retry", count, { statusCode: code });
}
for (var code in currentCounter[_DYN_EXCEPTION /* @min:%2eexception */]) {
var count = currentCounter[_DYN_EXCEPTION /* @min:%2eexception */][code];
_sendStatsbeats("exception", count, { exceptionType: code });
}
for (var code in currentCounter[_DYN_THROTTLE /* @min:%2ethrottle */]) {
var count = currentCounter[_DYN_THROTTLE /* @min:%2ethrottle */][code];
_sendStatsbeats("Throttle_Count", count, { statusCode: code });
}
}
function _setEnabled(isEnabled) {
_isEnabled = isEnabled;
if (!_isEnabled) {
if (_timeoutHandle) {
_timeoutHandle[_DYN_CANCEL /* @min:%2ecancel */]();
_timeoutHandle = null;
}
}
}
// THE statsbeat instance being created and returned
var statsBeat = {
enabled: !!_isEnabled,
endpoint: STR_EMPTY,
type: 0 /* eStatsType.SDK */,
count: function (status, payloadData, endpoint) {
if (_isEnabled && _checkEndpoint(endpoint)) {
if (payloadData && payloadData["statsBeatData"] && payloadData["statsBeatData"]["startTime"]) {
_networkCounter[_DYN_TOTAL_REQUEST /* @min:%2etotalRequest */] = (_networkCounter[_DYN_TOTAL_REQUEST /* @min:%2etotalRequest */] || 0) + 1;
_networkCounter[_DYN_REQUEST_DURATION /* @min:%2erequestDuration */] += utcNow() - payloadData["statsBeatData"]["startTime"];
}
var retryArray = [401, 403, 408, 429, 500, 502, 503, 504];
var throttleArray = [402, 439];
if (status >= 200 && status < 300) {
_networkCounter.success++;
}
else if (retryArray[_DYN_INDEX_OF /* @min:%2eindexOf */](status) !== -1) {
_networkCounter.retry[status] = (_networkCounter.retry[status] || 0) + 1;
}
else if (throttleArray[_DYN_INDEX_OF /* @min:%2eindexOf */](status) !== -1) {
_networkCounter[_DYN_THROTTLE /* @min:%2ethrottle */][status] = (_networkCounter[_DYN_THROTTLE /* @min:%2ethrottle */][status] || 0) + 1;
}
else if (status !== 307 && status !== 308) {
_networkCounter[_DYN_FAILURE /* @min:%2efailure */][status] = (_networkCounter[_DYN_FAILURE /* @min:%2efailure */][status] || 0) + 1;
}
_setupTimer();
}
},
countException: function (endpoint, exceptionType) {
if (_isEnabled && _checkEndpoint(endpoint)) {
_networkCounter[_DYN_EXCEPTION /* @min:%2eexception */][exceptionType] = (_networkCounter[_DYN_EXCEPTION /* @min:%2eexception */][exceptionType] || 0) + 1;
_setupTimer();
}
}
};
// Make the properties readonly / reactive to changes
return objDefineProps(statsBeat, {
enabled: { g: function () { return _isEnabled; }, s: _setEnabled },
type: { g: function () { return statsBeatStats[_DYN_TYPE /* @min:%2etype */]; } },
endpoint: { g: function () { return _networkCounter.host; } }
});
}
function _getEndpointCfg(statsBeatConfig, type) {
var endpointCfg = null;
if (statsBeatConfig && statsBeatConfig.endCfg) {
arrForEach(statsBeatConfig.endCfg, function (value) {
if (value[_DYN_TYPE /* @min:%2etype */] === type) {
endpointCfg = value;
return -1; // Stop the loop if we found a match
}
});
}
return endpointCfg;
}
/**
* This function retrieves the stats instrumentation key (iKey) for the given endpoint from
* the statsBeatConfig. It iterates through the keys in the statsBeatConfig and checks if
* the endpoint matches any of the URLs associated with that key. If a match is found, it
* returns the corresponding iKey.
* @param statsBeatConfig - The configuration object for StatsBeat.
* @param endpoint - The endpoint to check against the URLs in the configuration.
* @returns The iKey associated with the matching endpoint, or null if no match is found.
*/
function _getIKey(endpointCfg, endpoint) {
var statsKey = null;
if (endpointCfg.keyMap) {
arrForEach(endpointCfg.keyMap, function (keyMap) {
if (keyMap.match) {
arrForEach(keyMap.match, function (url) {
if (_isMatchEndpoint(url, endpoint)) {
statsKey = keyMap.key || null;
// Stop the loop if we found a match
return -1;
}
});
}
if (statsKey) {
// Stop the loop if we found a match
return -1;
}
});
}
return statsKey;
}
export function createStatsMgr() {
var _isMgrEnabled = false; // Flag to check if statsbeat is enabled or not
var _core; // The core instance that is used to send telemetry
var _shortInterval = STATS_COLLECTION_SHORT_INTERVAL;
var _statsBeatConfig;
// Lazily initialize the manager and start listening for configuration changes
// This is also required to handle "unloading" and then re-initializing again
function _init(core, statsConfig, featureName) {
if (_core) {
// If the core is already set, then just return with an empty unload hook
_throwInternal(safeGetLogger(core), 2 /* eLoggingSeverity.WARNING */, 113 /* _eInternalMessageId.StatsBeatManagerException */, "StatsBeat manager is already initialized");
return null;
}
_core = core;
if (core && core[_DYN_IS_INITIALIZED /* @min:%2eisInitialized */]()) {
// Start listening for configuration changes from the core config, within a config change handler
// This will support the scenario where the config is changed after the statsbeat has been created
return onConfigChange(core.config, function (details) {
// Check the feature state again to see if it has changed
_isMgrEnabled = false;
if (statsConfig && isFeatureEnabled(statsConfig.feature, details.cfg, false) === true) {
// Call the getCfg function to get the latest configuration for the statsbeat instance
// This should also evaluate the throttling level and other settings for the statsbeat instance
// to determine if it should be enabled or not.
_statsBeatConfig = statsConfig.getCfg(core, details.cfg);
if (_statsBeatConfig) {
_isMgrEnabled = true;
_shortInterval = STATS_COLLECTION_SHORT_INTERVAL; // Reset to the default in-case the config is removed / changed
if (isNumber(_statsBeatConfig.shrtInt) && _statsBeatConfig.shrtInt > STATS_MIN_INTERVAL_SECONDS) {
_shortInterval = _statsBeatConfig.shrtInt * 1000; // Convert to milliseconds
}
}
}
});
}
}
function _track(statsBeat, statsBeatEvent) {
if (_isMgrEnabled && _statsBeatConfig) {
var endpoint = statsBeat.endpoint;
var sendEvt = !!statsBeat[_DYN_TYPE /* @min:%2etype */];
// Fetching the stats key for the endpoint here to support the scenario where the endpoint is changed
// after the statsbeat instance is created. This will ensure that the correct stats key is used for the endpoint.
// It also avoids the tracking of the statsbeat event if the endpoint is not in the config.
var endpointCfg = _getEndpointCfg(_statsBeatConfig, statsBeat[_DYN_TYPE /* @min:%2etype */]);
if (endpointCfg) {
// Check for key remapping
var statsKey = _getIKey(endpointCfg, endpoint);
if (statsKey) {
// Using this iKey for the statsbeat event
statsBeatEvent.iKey = statsKey;
// We have specific config for this endpoint, so we can send the event
sendEvt = true;
}
if (sendEvt) {
_core.track(statsBeatEvent);
}
}
}
}
function _createInstance(state) {
var instance = null;
if (_isMgrEnabled) {
var callbacks = {
start: function (cb) {
return scheduleTimeout(cb, _shortInterval);
},
track: _track
};
instance = _createStatsBeat(callbacks, state);
}
return instance;
}
var theMgr = {
enabled: false,
newInst: _createInstance,
init: _init
};
return objDefineProps(theMgr, {
"enabled": { g: function () { return _isMgrEnabled; } }
});
}
//# sourceMappingURL=StatsBeat.js.map