UNPKG

@microsoft/applicationinsights-core-js

Version:

Microsoft Application Insights Core Javascript SDK

350 lines (348 loc) • 16.8 kB
/* * 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