diff --git a/AISKU/src/Initialization.ts b/AISKU/src/Initialization.ts index e9992bc23..80a55b5cf 100644 --- a/AISKU/src/Initialization.ts +++ b/AISKU/src/Initialization.ts @@ -7,7 +7,7 @@ import { arrForEach, isString, isFunction, isNullOrUndefined, addEventHandler, isArray, throwError, ICookieMgr, safeGetCookieMgr } from "@microsoft/applicationinsights-core-js"; import { ApplicationInsights } from "@microsoft/applicationinsights-analytics-js"; -import { Sender } from "@microsoft/applicationinsights-channel-js"; +import { Sender, Statsbeat } from "@microsoft/applicationinsights-channel-js"; import { PropertiesPlugin } from "@microsoft/applicationinsights-properties-js"; import { AjaxPlugin as DependenciesPlugin, IDependenciesPlugin } from '@microsoft/applicationinsights-dependencies-js'; import { @@ -143,7 +143,7 @@ export class Initialization implements IApplicationInsights { _self.properties = new PropertiesPlugin(); _self.dependencies = new DependenciesPlugin(); _self.core = new AppInsightsCore(); - _self._sender = new Sender(); + _self._sender = new Sender(new Statsbeat()); _self.snippet = snippet; _self.config = config; diff --git a/AISKULight/index.ts b/AISKULight/index.ts index 0e11b40ba..34d12101c 100644 --- a/AISKULight/index.ts +++ b/AISKULight/index.ts @@ -13,7 +13,7 @@ import { throwError } from "@microsoft/applicationinsights-core-js"; import { IConfig } from "@microsoft/applicationinsights-common"; -import { Sender } from "@microsoft/applicationinsights-channel-js"; +import { Sender, Statsbeat } from "@microsoft/applicationinsights-channel-js"; "use strict"; @@ -52,7 +52,8 @@ export class ApplicationInsights { public initialize(): void { this.core = new AppInsightsCore(); const extensions = []; - const appInsightsChannel: Sender = new Sender(); + const statsbeat = new Statsbeat(); + const appInsightsChannel: Sender = new Sender(statsbeat); extensions.push(appInsightsChannel); diff --git a/channels/applicationinsights-channel-js/Tests/Unit/src/Statsbeat.tests.ts b/channels/applicationinsights-channel-js/Tests/Unit/src/Statsbeat.tests.ts new file mode 100644 index 000000000..bab68f437 --- /dev/null +++ b/channels/applicationinsights-channel-js/Tests/Unit/src/Statsbeat.tests.ts @@ -0,0 +1,147 @@ +import { AITestClass } from "@microsoft/ai-test-framework"; +import { Sender } from "../../../src/Sender"; +import { Metric, IMetricTelemetry, urlParseUrl } from "@microsoft/applicationinsights-common"; +import { AppInsightsCore, _InternalMessageId } from "@microsoft/applicationinsights-core-js"; +import { Statsbeat } from "../../../src/Statsbeat"; +import { StatsbeatCounter } from "../../../src/Constants"; + +const INSTRUMENTATION_KEY = "c4a29126-a7cb-47e5-b348-11414998b11e"; +var strEmpty = ""; +const endpoint = "https://dc.services.visualstudio.com/v2/track"; +const endpointHost = urlParseUrl(endpoint).hostname; +export class StatsbeatTests extends AITestClass { + private _sender: Sender; + private _statsbeat: Statsbeat; + + public testInitialize() { + this._statsbeat = new Statsbeat(); + this._sender = new Sender(this._statsbeat); + } + + public testCleanup() { + this._statsbeat = null; + this._sender = null; + } + + public registerTests() { + this.testCase({ + name: "Statsbeat by default is enabled and is initialized while sender is initialized.", + test: () => { + const spy = this.sandbox.spy(this._statsbeat, "initialize"); + this._sender.initialize( + { + instrumentationKey: 'abc', + }, new AppInsightsCore(), [] + ); + + QUnit.assert.equal(true, spy.calledOnce, "Statsbeat is enabled by default."); + QUnit.assert.equal(true, this._statsbeat.isInitialized()); + } + }); + + this.testCase({ + name: "Statsbeat is disabled if customer configured to disable statsbeat.", + test: () => { + this._sender.initialize( + { + instrumentationKey: 'abc', + disableStatsbeat: true, + }, new AppInsightsCore(), [] + ); + + const spy = this.sandbox.spy(this._statsbeat, "initialize"); + QUnit.assert.equal(false, spy.calledOnce, "Statsbeat is disabled with customer configuration. When disableStatsbeat sets to true, statsbeat is not initialized thus initialize method is not called."); + } + }); + + this.testCase({ + name: "It adds correct network properties to custom metric.", + test: () => { + // the first xhr gets created when _sender calls initialize; the second xhr gest created when statsbeat's sender calls initialize + this._sender.initialize({ instrumentationKey: "1aa11111-bbbb-1ccc-8ddd-eeeeffff3333" }, new AppInsightsCore(), []); + // the third xhr gets created when track is called and the current _sender creates a xhr to send data + this._statsbeat.trackShortIntervalStatsbeats(); + + QUnit.assert.equal(1, this._getXhrRequests().length, "xhr sender is called"); + + // we only care the last object that's about to send + const arr = JSON.parse(this._getXhrRequests()[0].requestBody); + let fakeReq = arr[arr.length - 1]; + console.log(fakeReq); + QUnit.assert.equal(fakeReq.name, "Microsoft.ApplicationInsights." + INSTRUMENTATION_KEY.replace(/-/g, strEmpty) + ".Metric"); + QUnit.assert.equal(fakeReq.iKey, INSTRUMENTATION_KEY); + QUnit.assert.equal(fakeReq.data.baseType, Metric.dataType); + + let baseData: IMetricTelemetry = fakeReq.data.baseData; + QUnit.assert.equal(baseData.properties["cikey"], "1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"); + QUnit.assert.equal(baseData.properties["host"], endpointHost); + } + }); + + this.testCase({ + name: "Track duration.", + test: () => { + this._sender.initialize({ instrumentationKey: "1aa11111-bbbb-1ccc-8ddd-eeeeffff3333" }, new AppInsightsCore(), []); + this._statsbeat.countRequest(endpointHost, 1000, true); + this._statsbeat.countRequest(endpointHost, 500, false); + this._statsbeat.trackShortIntervalStatsbeats(); + + QUnit.assert.equal(1, this._getXhrRequests().length, "xhr sender is called"); + + // we only care the last object that's about to send + const arr = JSON.parse(this._getXhrRequests()[0].requestBody); + let fakeReq = arr[arr.length - 1]; + console.log(fakeReq); + QUnit.assert.equal(fakeReq.name, "Microsoft.ApplicationInsights." + INSTRUMENTATION_KEY.replace(/-/g, strEmpty) + ".Metric"); + QUnit.assert.equal(fakeReq.iKey, INSTRUMENTATION_KEY); + QUnit.assert.equal(fakeReq.data.baseType, Metric.dataType); + + let baseData: IMetricTelemetry = fakeReq.data.baseData; + QUnit.assert.equal(baseData.properties["cikey"], "1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"); + QUnit.assert.equal(baseData.properties["host"], endpointHost); + QUnit.assert.equal(baseData.properties[StatsbeatCounter.REQUEST_DURATION], "750"); + QUnit.assert.equal(baseData.properties[StatsbeatCounter.REQUEST_SUCCESS], "1"); + } + }); + + this.testCase({ + name: "Track counts.", + test: () => { + this._sender.initialize({ instrumentationKey: "1aa11111-bbbb-1ccc-8ddd-eeeeffff3333" }, new AppInsightsCore(), []); + this._statsbeat.countRequest(endpointHost, 1, true); + this._statsbeat.countRequest(endpointHost, 1, true); + this._statsbeat.countRequest(endpointHost, 1, true); + this._statsbeat.countRequest(endpointHost, 1, true); + this._statsbeat.countRequest(endpointHost, 1, false); + this._statsbeat.countRequest(endpointHost, 1, false); + this._statsbeat.countRequest(endpointHost, 1, false); + this._statsbeat.countRetry(endpointHost); + this._statsbeat.countRetry(endpointHost); + this._statsbeat.countThrottle(endpointHost); + this._statsbeat.countException(endpointHost); + this._statsbeat.trackShortIntervalStatsbeats(); + + QUnit.assert.equal(1, this._getXhrRequests().length, "xhr sender is called"); + + // we only care the last object that's about to send + const arr = JSON.parse(this._getXhrRequests()[0].requestBody); + let fakeReq = arr[arr.length - 1]; + console.log(fakeReq); + QUnit.assert.equal(fakeReq.name, "Microsoft.ApplicationInsights." + INSTRUMENTATION_KEY.replace(/-/g, strEmpty) + ".Metric"); + QUnit.assert.equal(fakeReq.iKey, INSTRUMENTATION_KEY); + QUnit.assert.equal(fakeReq.data.baseType, Metric.dataType); + + let baseData: IMetricTelemetry = fakeReq.data.baseData; + + QUnit.assert.equal(baseData.properties["cikey"], "1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"); + QUnit.assert.equal(baseData.properties["host"], endpointHost); + QUnit.assert.equal(baseData.properties[StatsbeatCounter.REQUEST_DURATION], "1"); + QUnit.assert.equal(baseData.properties[StatsbeatCounter.REQUEST_SUCCESS], "4"); + QUnit.assert.equal(baseData.properties[StatsbeatCounter.REQUEST_FAILURE], "3"); + QUnit.assert.equal(baseData.properties[StatsbeatCounter.RETRY_COUNT], "2"); + QUnit.assert.equal(baseData.properties[StatsbeatCounter.THROTTLE_COUNT], "1"); + QUnit.assert.equal(baseData.properties[StatsbeatCounter.EXCEPTION_COUNT], "1"); + } + }); + } +} \ No newline at end of file diff --git a/channels/applicationinsights-channel-js/Tests/Unit/src/aichannel.tests.ts b/channels/applicationinsights-channel-js/Tests/Unit/src/aichannel.tests.ts index 807b0d875..02e9ec960 100644 --- a/channels/applicationinsights-channel-js/Tests/Unit/src/aichannel.tests.ts +++ b/channels/applicationinsights-channel-js/Tests/Unit/src/aichannel.tests.ts @@ -1,7 +1,9 @@ import { SenderTests } from "./Sender.tests"; import { SampleTests } from "./Sample.tests"; +import { StatsbeatTests } from "./Statsbeat.tests"; export function runTests() { new SenderTests().registerTests(); new SampleTests().registerTests(); + new StatsbeatTests().registerTests(); } \ No newline at end of file diff --git a/channels/applicationinsights-channel-js/src/Constants.ts b/channels/applicationinsights-channel-js/src/Constants.ts new file mode 100644 index 000000000..0a7c7850f --- /dev/null +++ b/channels/applicationinsights-channel-js/src/Constants.ts @@ -0,0 +1,8 @@ +export const StatsbeatCounter = { + REQUEST_SUCCESS: "Request Success Count", + REQUEST_FAILURE: "Requests Failure Count", + REQUEST_DURATION: "Request Duration", + RETRY_COUNT: "Retry Count", + THROTTLE_COUNT: "Throttle Count", + EXCEPTION_COUNT: "Exception Count", +} \ No newline at end of file diff --git a/channels/applicationinsights-channel-js/src/EnvelopeCreator.ts b/channels/applicationinsights-channel-js/src/EnvelopeCreator.ts index 7ecc40d4a..d8262f502 100644 --- a/channels/applicationinsights-channel-js/src/EnvelopeCreator.ts +++ b/channels/applicationinsights-channel-js/src/EnvelopeCreator.ts @@ -266,7 +266,7 @@ export class MetricEnvelopeCreator extends EnvelopeCreator { Create(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { super.Init(logger, telemetryItem); - const baseData = telemetryItem[strBaseData]; + const baseData = telemetryItem[strBaseData] || {}; const props = baseData[strProperties] || {}; const measurements = baseData.measurements || {}; EnvelopeCreator.extractPropsAndMeasurements(telemetryItem.data, props, measurements); diff --git a/channels/applicationinsights-channel-js/src/NetworkStatsbeat.ts b/channels/applicationinsights-channel-js/src/NetworkStatsbeat.ts new file mode 100644 index 000000000..7810ab428 --- /dev/null +++ b/channels/applicationinsights-channel-js/src/NetworkStatsbeat.ts @@ -0,0 +1,41 @@ +import { dateNow } from "@microsoft/applicationinsights-core-js"; +export class NetworkStatsbeat { + + public time: number; + + public lastTime: number; + + public host: string; + + public totalRequestCount: number; + + public lastRequestCount: number; + + public totalSuccesfulRequestCount: number; + + public totalFailedRequestCount: number; + + public retryCount: number; + + public exceptionCount: number; + + public throttleCount: number; + + public intervalRequestExecutionTime: number; + + public lastIntervalRequestExecutionTime: number; + + constructor(host: string) { + this.host = host; + this.totalRequestCount = 0; + this.totalSuccesfulRequestCount = 0; + this.totalFailedRequestCount = 0; + this.retryCount = 0; + this.exceptionCount = 0; + this.throttleCount = 0; + this.intervalRequestExecutionTime = 0; + this.lastIntervalRequestExecutionTime = 0; + this.lastTime = dateNow(); + this.lastRequestCount = 0; + } +} \ No newline at end of file diff --git a/channels/applicationinsights-channel-js/src/Sender.ts b/channels/applicationinsights-channel-js/src/Sender.ts index a16919464..3d3185de0 100644 --- a/channels/applicationinsights-channel-js/src/Sender.ts +++ b/channels/applicationinsights-channel-js/src/Sender.ts @@ -10,7 +10,7 @@ import { DisabledPropertyName, RequestHeaders, IEnvelope, PageView, Event, Trace, Exception, Metric, PageViewPerformance, RemoteDependencyData, IChannelControlsAI, IConfig, ProcessLegacy, BreezeChannelIdentifier, - SampleRate, isInternalApplicationInsightsEndpoint, utlCanUseSessionStorage, isBeaconApiSupported + SampleRate, isInternalApplicationInsightsEndpoint, utlCanUseSessionStorage, isBeaconApiSupported, urlParseUrl } from '@microsoft/applicationinsights-common'; import { ITelemetryItem, IProcessTelemetryContext, IConfiguration, @@ -21,13 +21,14 @@ import { import { Offline } from './Offline'; import { Sample } from './TelemetryProcessors/Sample' import dynamicProto from '@microsoft/dynamicproto-js'; +import { Statsbeat } from './Statsbeat'; declare var XDomainRequest: { prototype: IXDomainRequest; new(): IXDomainRequest; }; -export type SenderFunction = (payload: string[], isAsync: boolean) => void; +export type SenderFunction = (payload: string[], isAsync: boolean, startTime?: number) => void; function _getResponseText(xhr: XMLHttpRequest | IXDomainRequest) { try { @@ -142,7 +143,9 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { protected _sample: Sample; - constructor() { + private _statsbeat: Statsbeat; + + constructor(statsbeat?: Statsbeat) { super(); /** @@ -176,6 +179,8 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { throwError("Method not implemented."); } + _self._statsbeat = statsbeat; + _self.pause = _notImplemented; _self.resume = _notImplemented; @@ -264,6 +269,12 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { } } } + + // initialize statsbeat instance last after sender config is fully populated + if (_self._statsbeat && config.disableStatsbeat !== true) { + var endpointHost = urlParseUrl(_self._senderConfig.endpointUrl()).hostname; + _self._statsbeat.initialize(config, core, extensions, pluginChain, endpointHost); + } }; _self.processTelemetry = (telemetryItem: ITelemetryItem, itemCtx?: IProcessTelemetryContext) => { @@ -373,9 +384,9 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { /** * xhr state changes */ - _self._xhrReadyStateChange = (xhr: XMLHttpRequest, payload: string[], countOfItemsInPayload: number) => { + _self._xhrReadyStateChange = (xhr: XMLHttpRequest, payload: string[], countOfItemsInPayload: number, startTime: number) => { if (xhr.readyState === 4) { - _checkResponsStatus(xhr.status, payload, xhr.responseURL, countOfItemsInPayload, _formatErrorMessageXhr(xhr), _getResponseText(xhr) || xhr.response); + _checkResponsStatus(xhr.status, payload, xhr.responseURL, countOfItemsInPayload, _formatErrorMessageXhr(xhr), _getResponseText(xhr) || xhr.response, startTime); } }; @@ -393,12 +404,14 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { const payload = _self._buffer.getItems(); _notifySendRequest(sendReason||SendRequestReason.Undefined, async); + + let startTime = dateNow(); // invoke send if (forcedSender) { - forcedSender.call(this, payload, async); + forcedSender.call(this, payload, async, startTime); } else { - _self._sender(payload, async); + _self._sender(payload, async, startTime); } } @@ -435,6 +448,10 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { { message }); _self._buffer.clearSent(payload); + if (_self._statsbeat && _self._statsbeat.isInitialized()) { + var endpointHost = urlParseUrl(_self._senderConfig.endpointUrl()).hostname; + _self._statsbeat.countException(endpointHost); + } }; /** @@ -461,11 +478,11 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { } if (failed.length > 0) { - _self._onError(failed, _formatErrorMessageXhr(null, ['partial success', results.itemsAccepted, 'of', results.itemsReceived].join(' '))); + _self._onError(failed, _formatErrorMessageXhr(null, ['partial success', results.itemsAccepted, 'of', results.itemsReceived].join(' ')), null); } if (retry.length > 0) { - _resendPayload(retry); + _resendPayload(retry, 1); _self.diagLog().throwInternal( LoggingSeverity.WARNING, @@ -485,11 +502,16 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { /** * xdr state changes */ - _self._xdrOnLoad = (xdr: IXDomainRequest, payload: string[]) => { + _self._xdrOnLoad = (xdr: IXDomainRequest, payload: string[], startTime: number) => { const responseText = _getResponseText(xdr); if (xdr && (responseText + "" === "200" || responseText === "")) { _consecutiveErrors = 0; _self._onSuccess(payload, 0); + if (_self._statsbeat && _self._statsbeat.isInitialized()) { + let endTime = dateNow(); + var endpointHost = urlParseUrl(_self._senderConfig.endpointUrl()).hostname; + _self._statsbeat.countRequest(endpointHost, endTime - startTime, true); + } } else { const results = _parseResponse(responseText); @@ -497,7 +519,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { && !_self._senderConfig.isRetryDisabled()) { _self._onPartialSuccess(payload, results); } else { - _self._onError(payload, _formatErrorMessageXdr(xdr)); + _self._onError(payload, _formatErrorMessageXdr(xdr), null); } } }; @@ -506,7 +528,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { return _self._sample.isSampledIn(envelope); } - function _checkResponsStatus(status: number, payload: string[], responseUrl: string, countOfItemsInPayload: number, errorMessage: string, res: any) { + function _checkResponsStatus(status: number, payload: string[], responseUrl: string, countOfItemsInPayload: number, errorMessage: string, res: any, startTime: number) { let response: IBackendResponse = null; if (!_self._appId) { @@ -522,31 +544,35 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { // Updates the end point url before retry if(status === 301 || status === 307 || status === 308) { if(!_checkAndUpdateEndPointUrl(responseUrl)) { - _self._onError(payload, errorMessage); + _self._onError(payload, errorMessage, null); return; } } if (!_self._senderConfig.isRetryDisabled() && _isRetriable(status)) { - _resendPayload(payload); + _resendPayload(payload, 1, status); _self.diagLog().throwInternal( LoggingSeverity.WARNING, _InternalMessageId.TransmissionFailed, ". " + "Response code " + status + ". Will retry to send " + payload.length + " items."); } else { - _self._onError(payload, errorMessage); + _self._onError(payload, errorMessage, null); } } else if (Offline.isOffline()) { // offline // Note: Don't check for status == 0, since adblock gives this code if (!_self._senderConfig.isRetryDisabled()) { const offlineBackOffMultiplier = 10; // arbritrary number - _resendPayload(payload, offlineBackOffMultiplier); + _resendPayload(payload, offlineBackOffMultiplier, status); _self.diagLog().throwInternal( LoggingSeverity.WARNING, _InternalMessageId.TransmissionFailed, `. Offline - Response Code: ${status}. Offline status: ${Offline.isOffline()}. Will retry to send ${payload.length} items.`); } } else { + let endTime = dateNow(); + if (_self._statsbeat && _self._statsbeat.isInitialized()) { + _self._statsbeat.countRequest(responseUrl, endTime - startTime, status === 200); + } // check if the xhr's responseURL or fetch's response.url is same as endpoint url // TODO after 10 redirects force send telemetry with 'redirect=false' as query parameter. @@ -560,7 +586,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { if (response && !_self._senderConfig.isRetryDisabled()) { _self._onPartialSuccess(payload, response); } else { - _self._onError(payload, errorMessage); + _self._onError(payload, errorMessage, null); } } else { _consecutiveErrors = 0; @@ -593,7 +619,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { * Note: Beacon API does not support custom headers and we are not able to get * appId from the backend for the correct correlation. */ - function _beaconSender(payload: string[], isAsync: boolean) { + function _beaconSender(payload: string[], isAsync: boolean, startTime: number) { const url = _self._senderConfig.endpointUrl(); const batch = _self._buffer.batchPayloads(payload); @@ -608,9 +634,16 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { _self._buffer.markAsSent(payload); // no response from beaconSender, clear buffer _self._onSuccess(payload, payload.length); + if (_self._statsbeat && _self._statsbeat.isInitialized()) { + let endTime = dateNow(); + _self._statsbeat.countRequest(url, endTime - startTime, true); + } } else { - _xhrSender(payload, true); + _xhrSender(payload, true, startTime); _self.diagLog().throwInternal(LoggingSeverity.WARNING, _InternalMessageId.TransmissionFailed, ". " + "Failed to send telemetry with Beacon API, retried with xhrSender."); + if (_self._statsbeat && _self._statsbeat.isInitialized()) { + _self._statsbeat.countException(url); + } } } @@ -619,7 +652,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { * @param payload {string} - The data payload to be sent. * @param isAsync {boolean} - Indicates if the request should be sent asynchronously */ - function _xhrSender(payload: string[], isAsync: boolean) { + function _xhrSender(payload: string[], isAsync: boolean, startTime: number) { const xhr = new XMLHttpRequest(); const endPointUrl = _self._senderConfig.endpointUrl(); try { @@ -639,8 +672,8 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { arrForEach(objKeys(_headers), (headerName) => { xhr.setRequestHeader(headerName, _headers[headerName]); }); - - xhr.onreadystatechange = () => _self._xhrReadyStateChange(xhr, payload, payload.length); + + xhr.onreadystatechange = () => _self._xhrReadyStateChange(xhr, payload, payload.length, startTime); xhr.onerror = (event: ErrorEvent|any) => _self._onError(payload, _formatErrorMessageXhr(xhr), event); // compose an array of payloads @@ -655,7 +688,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { * @param payload {string} - The data payload to be sent. * @param isAsync {boolean} - not used */ - function _fetchSender(payload: string[], isAsync: boolean) { + function _fetchSender(payload: string[], isAsync: boolean, startTime: number) { const endPointUrl = _self._senderConfig.endpointUrl(); const batch = _self._buffer.batchPayloads(payload); const plainTextBatch = new Blob([batch], { type: 'text/plain;charset=UTF-8' }); @@ -684,12 +717,12 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { throw Error(response.statusText); } else { response.text().then(text => { - _checkResponsStatus(response.status, payload, response.url, payload.length, response.statusText, text); + _checkResponsStatus(response.status, payload, response.url, payload.length, response.statusText, text, startTime); }); _self._buffer.markAsSent(payload); } }).catch((error: Error) => { - _self._onError(payload, error.message) + _self._onError(payload, error.message, null); }); } @@ -724,11 +757,19 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { * Resend payload. Adds payload back to the send buffer and setup a send timer (with exponential backoff). * @param payload */ - function _resendPayload(payload: string[], linearFactor: number = 1) { + function _resendPayload(payload: string[], linearFactor: number = 1, status?: number) { if (!payload || payload.length === 0) { return; } + var endpointHost = urlParseUrl(_self._senderConfig.endpointUrl()).hostname; + if (_self._statsbeat && _self._statsbeat.isInitialized()) { + _self._statsbeat.countRetry(endpointHost); + if (status && status === 429) { + _self._statsbeat.countThrottle(endpointHost); + } + } + _self._buffer.clearSent(payload); _consecutiveErrors++; @@ -809,10 +850,10 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { * Note: XDomainRequest does not support custom headers and we are not able to get * appId from the backend for the correct correlation. */ - function _xdrSender(payload: string[], isAsync: boolean) { + function _xdrSender(payload: string[], isAsync: boolean, startTime: number) { let _window = getWindow(); const xdr = new XDomainRequest(); - xdr.onload = () => _self._xdrOnLoad(xdr, payload); + xdr.onload = () => _self._xdrOnLoad(xdr, payload, startTime); xdr.onerror = (event: ErrorEvent|any) => _self._onError(payload, _formatErrorMessageXdr(xdr), event); // XDomainRequest requires the same protocol as the hosting page. @@ -827,7 +868,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { _self._buffer.clear(); return; } - + const endpointUrl = _self._senderConfig.endpointUrl().replace(/^(https?:)/, ""); xdr.open('POST', endpointUrl); @@ -919,7 +960,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { /** * xhr state changes */ - public _xhrReadyStateChange(xhr: XMLHttpRequest, payload: string[], countOfItemsInPayload: number) { + public _xhrReadyStateChange(xhr: XMLHttpRequest, payload: string[], countOfItemsInPayload: number, startTime: number) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } @@ -956,7 +997,11 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { /** * xdr state changes */ - public _xdrOnLoad(xdr: IXDomainRequest, payload: string[]) { + public _xdrOnLoad(xdr: IXDomainRequest, payload: string[], startTime: number) { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } + + public _xdrOpen(xdr: IXDomainRequest, startTime: number) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } diff --git a/channels/applicationinsights-channel-js/src/Statsbeat.ts b/channels/applicationinsights-channel-js/src/Statsbeat.ts new file mode 100644 index 000000000..95e29556a --- /dev/null +++ b/channels/applicationinsights-channel-js/src/Statsbeat.ts @@ -0,0 +1,218 @@ +import { NetworkStatsbeat } from "./NetworkStatsbeat"; +import { Sender } from "./Sender"; +import { StatsbeatCounter } from "./Constants"; +import { IConfig, Metric } from "@microsoft/applicationinsights-common"; +import { IConfiguration, IAppInsightsCore, IPlugin, ITelemetryPluginChain, ITelemetryItem, objKeys, dateNow, } from '@microsoft/applicationinsights-core-js'; +import dynamicProto from '@microsoft/dynamicproto-js'; +import { EnvelopeCreator } from "./EnvelopeCreator"; + +const INSTRUMENTATION_KEY = "c4a29126-a7cb-47e5-b348-11414998b11e"; +const STATS_COLLECTION_SHORT_INTERVAL: number = 900000; // 15 minutes +const NETWORK = "Network"; +const STATSBEAT_LANGUAGE = "JavaScript"; +const STATSBEAT_TYPE = "Browser"; + +export class Statsbeat { + constructor() { + let _networkCounter: NetworkStatsbeat; + let _sender: Sender; + let _handle: any; + let _statsbeatMetrics: { properties?: {} }; + let _config: IConfiguration & IConfig; + let _isEnabled: boolean; + + // Custom dimensions + let _cikey: string; + let _language: string; + let _sdkVersion: string; + let _os: string; + let _runTimeVersion: string; + dynamicProto(Statsbeat, this, (_self, _base) => { + _self.initialize = (config: IConfiguration & IConfig, core: IAppInsightsCore, extensions: IPlugin[], pluginChain?:ITelemetryPluginChain, endpoint?: string) => { + _networkCounter = new NetworkStatsbeat(endpoint); + _sender = new Sender(undefined); + let senderConfig = {...config}; + senderConfig.instrumentationKey = INSTRUMENTATION_KEY; + _sender.initialize(senderConfig, core, extensions, pluginChain); + _statsbeatMetrics = {}; + _config = config; + _isEnabled = true; + if (_isEnabled) { + _getCustomProperties(); + if (!_handle) { + _handle = setInterval(() => { + this.trackShortIntervalStatsbeats(); + }, STATS_COLLECTION_SHORT_INTERVAL); + } + } + } + + _self.isInitialized = (): boolean => { + return !!_isEnabled; + } + + _self.countRequest = (endpoint: string, duration: number, success: boolean) => { + if (!_isEnabled) { + return; + } + let counter: NetworkStatsbeat = _getNetworkStatsbeatCounter(endpoint); + counter.totalRequestCount++; + counter.intervalRequestExecutionTime += duration; + if (success === false) { + counter.totalFailedRequestCount++; + } + else { + counter.totalSuccesfulRequestCount++; + } + } + + _self.countException = (endpoint: string) => { + if (!_isEnabled) { + return; + } + let counter: NetworkStatsbeat = _getNetworkStatsbeatCounter(endpoint); + counter.exceptionCount++; + } + + _self.countThrottle = (endpoint: string) => { + if (!_isEnabled) { + return; + } + let counter: NetworkStatsbeat = _getNetworkStatsbeatCounter(endpoint); + counter.throttleCount++; + } + + _self.countRetry = (endpoint: string) => { + if (!_isEnabled) { + return; + } + let counter: NetworkStatsbeat = _getNetworkStatsbeatCounter(endpoint); + counter.retryCount++; + } + + _self.trackShortIntervalStatsbeats = (): void => { + _trackRequestDuration(); + _trackRequestsCount(); + _sendStatsbeats(); + } + + function _getCustomProperties() { + _cikey = _config.instrumentationKey; + _language = STATSBEAT_LANGUAGE; + _os = STATSBEAT_TYPE; + _runTimeVersion = STATSBEAT_TYPE; + _sdkVersion = EnvelopeCreator.Version; + } + + function _getNetworkStatsbeatCounter(host: string): NetworkStatsbeat { + // Check if counter is available + if (_networkCounter && _networkCounter.host === host) { + return _networkCounter; + } + // Create a new one if not found + let newCounter = new NetworkStatsbeat(host); + return newCounter; + } + + function _sendStatsbeats() { + // Add extra properties + let networkProperties = { + "cikey": _cikey, + "runtimeVersion": _runTimeVersion, + "language": _language, + "version": _sdkVersion, + "os": _os, + } + if (objKeys(_statsbeatMetrics)) { + let statsbeat: ITelemetryItem = { + iKey: INSTRUMENTATION_KEY, + name: NETWORK, + baseData: { + name: NETWORK, + average: 0, + properties: {"host": _networkCounter.host, ..._statsbeatMetrics.properties, ...networkProperties}, + }, + baseType: Metric.dataType + }; + _sender.processTelemetry(statsbeat); + } + _statsbeatMetrics = {}; + _sender.triggerSend(); + } + + function _trackRequestDuration() { + var currentCounter = _networkCounter; + currentCounter.time = dateNow(); + var intervalRequests = (currentCounter.totalRequestCount - currentCounter.lastRequestCount) || 0; + var elapsedMs = currentCounter.time - currentCounter.lastTime; + var averageRequestExecutionTime = ((currentCounter.intervalRequestExecutionTime - currentCounter.lastIntervalRequestExecutionTime) / intervalRequests) || 0; + currentCounter.lastIntervalRequestExecutionTime = currentCounter.intervalRequestExecutionTime; // reset + if (elapsedMs > 0 && intervalRequests > 0) { + _statsbeatMetrics.properties = _statsbeatMetrics.properties || {}; + _statsbeatMetrics.properties[StatsbeatCounter.REQUEST_DURATION] = averageRequestExecutionTime; + } + // Set last counters + currentCounter.lastRequestCount = currentCounter.totalRequestCount; + currentCounter.lastTime = currentCounter.time; + } + + function _trackRequestsCount() { + var currentCounter = _networkCounter; + if (currentCounter.totalSuccesfulRequestCount > 0) { + _statsbeatMetrics.properties = _statsbeatMetrics.properties || {}; + _statsbeatMetrics.properties[StatsbeatCounter.REQUEST_SUCCESS] = currentCounter.totalSuccesfulRequestCount; + currentCounter.totalSuccesfulRequestCount = 0; //Reset + } + if (currentCounter.totalFailedRequestCount > 0) { + _statsbeatMetrics.properties = _statsbeatMetrics.properties || {}; + _statsbeatMetrics.properties[StatsbeatCounter.REQUEST_FAILURE] = currentCounter.totalFailedRequestCount; + currentCounter.totalFailedRequestCount = 0; //Reset + } + if (currentCounter.retryCount > 0) { + _statsbeatMetrics.properties = _statsbeatMetrics.properties || {}; + _statsbeatMetrics.properties[StatsbeatCounter.RETRY_COUNT] = currentCounter.retryCount; + currentCounter.retryCount = 0; //Reset + } + if (currentCounter.throttleCount > 0) { + _statsbeatMetrics.properties = _statsbeatMetrics.properties || {}; + _statsbeatMetrics.properties[StatsbeatCounter.THROTTLE_COUNT] = currentCounter.throttleCount; + currentCounter.throttleCount = 0; //Reset + } + if (currentCounter.exceptionCount > 0) { + _statsbeatMetrics.properties = _statsbeatMetrics.properties || {}; + _statsbeatMetrics.properties[StatsbeatCounter.EXCEPTION_COUNT] = currentCounter.exceptionCount; + currentCounter.exceptionCount = 0; //Reset + } + } + }) + } + + public initialize(config: IConfiguration & IConfig, core: IAppInsightsCore, extensions: IPlugin[], pluginChain?:ITelemetryPluginChain, endpoint?: string) { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } + + public isInitialized(): boolean { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + return false; + } + + public countRequest(endpoint: string, duration: number, success: boolean) { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } + + public countException(endpoint: string) { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } + + public countThrottle(endpoint: string) { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } + + public countRetry(endpoint: string) { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } + + public trackShortIntervalStatsbeats() { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } +} diff --git a/channels/applicationinsights-channel-js/src/applicationinsights-channel-js.ts b/channels/applicationinsights-channel-js/src/applicationinsights-channel-js.ts index 01f58b6c9..fdfe2ea23 100644 --- a/channels/applicationinsights-channel-js/src/applicationinsights-channel-js.ts +++ b/channels/applicationinsights-channel-js/src/applicationinsights-channel-js.ts @@ -1 +1,2 @@ -export { Sender } from "./Sender"; \ No newline at end of file +export { Sender } from "./Sender"; +export { Statsbeat } from "./Statsbeat"; \ No newline at end of file diff --git a/common/Tests/Framework/src/AITestClass.ts b/common/Tests/Framework/src/AITestClass.ts index 571c75345..f9ab95e52 100644 --- a/common/Tests/Framework/src/AITestClass.ts +++ b/common/Tests/Framework/src/AITestClass.ts @@ -12,6 +12,7 @@ export interface FakeXMLHttpRequest extends XMLHttpRequest { url?: string; method?: string; requestHeaders?: any; + requestBody?: string; respond: (status: number, headers: any, body: string) => void; } diff --git a/shared/AppInsightsCore/src/JavaScriptSDK.Enums/LoggingEnums.ts b/shared/AppInsightsCore/src/JavaScriptSDK.Enums/LoggingEnums.ts index 837e754b6..a38220118 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK.Enums/LoggingEnums.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK.Enums/LoggingEnums.ts @@ -99,6 +99,6 @@ export const _InternalMessageId = { CannotParseAiBlobValue: 101, InvalidContentBlob: 102, TrackPageActionEventFailed: 103, - FailedAddingCustomDefinedRequestContext: 104 + FailedAddingCustomDefinedRequestContext: 104, }; export type _InternalMessageId = number | typeof _InternalMessageId; diff --git a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts index ea9bff958..8896787df 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts @@ -149,4 +149,6 @@ export interface IConfiguration { * cookieDomain and disableCookiesUsage values. */ cookieCfg?: ICookieMgrConfig; + + disableStatsbeat?: boolean; } \ No newline at end of file