diff --git a/AISKU/Tests/AISKUSize.Tests.ts b/AISKU/Tests/AISKUSize.Tests.ts index cde4240a8..21d8ce587 100644 --- a/AISKU/Tests/AISKUSize.Tests.ts +++ b/AISKU/Tests/AISKUSize.Tests.ts @@ -45,7 +45,7 @@ export class AISKUSizeCheck extends TestClass { return; } else { return response.text().then(text => { - let size = Math.ceil(pako.deflate(text).length/1024); + let size = Math.ceil((pako.deflate(text).length/1024) * 100) / 100.0; Assert.ok(size <= this.MAX_DEFLATE_SIZE ,`max ${this.MAX_DEFLATE_SIZE} KB, current deflate size is: ${size} KB`); }).catch((error: Error) => { Assert.ok(false, `AISKU${postfix} response error: ${error}`); diff --git a/AISKU/Tests/Selenium/appinsights-sdk.tests.ts b/AISKU/Tests/Selenium/appinsights-sdk.tests.ts index d7fcdadcf..db6f24552 100644 --- a/AISKU/Tests/Selenium/appinsights-sdk.tests.ts +++ b/AISKU/Tests/Selenium/appinsights-sdk.tests.ts @@ -1,5 +1,6 @@ import { AISKUSizeCheck } from "../AISKUSize.Tests"; import { ApplicationInsightsTests } from '../applicationinsights.e2e.tests'; +import { ApplicationInsightsFetchTests } from '../applicationinsights.e2e.fetch.tests'; import { SanitizerE2ETests } from '../sanitizer.e2e.tests'; import { ValidateE2ETests } from '../validate.e2e.tests'; import { SenderE2ETests } from '../sender.e2e.tests'; @@ -10,6 +11,7 @@ import { SnippetInitializationTests } from '../SnippetInitialization.Tests'; new AISKUSizeCheck().registerTests(); new ApplicationInsightsTests().registerTests(); +new ApplicationInsightsFetchTests().registerTests(); new ApplicationInsightsDeprecatedTests().registerTests(); new SanitizerE2ETests().registerTests(); new ValidateE2ETests().registerTests(); diff --git a/AISKU/Tests/applicationinsights.e2e.fetch.tests.ts b/AISKU/Tests/applicationinsights.e2e.fetch.tests.ts new file mode 100644 index 000000000..63d54fe66 --- /dev/null +++ b/AISKU/Tests/applicationinsights.e2e.fetch.tests.ts @@ -0,0 +1,30 @@ +import { DistributedTracingModes } from '@microsoft/applicationinsights-common'; +import { ApplicationInsightsTests } from './applicationinsights.e2e.tests'; + +const _instrumentationKey = 'b7170927-2d1c-44f1-acec-59f4e1751c11'; +const _connectionString = `InstrumentationKey=${_instrumentationKey}`; + +export class ApplicationInsightsFetchTests extends ApplicationInsightsTests { + + constructor() { + super("ApplicationInsightsFetchTests-XHR Disabled"); + } + + protected _getTestConfig(sessionPrefix: string) { + return { + connectionString: _connectionString, + disableAjaxTracking: false, + disableFetchTracking: false, + disableXhr: true, // Disable XHR support + enableRequestHeaderTracking: true, + enableResponseHeaderTracking: true, + maxBatchInterval: 2500, + disableExceptionTracking: false, + namePrefix: sessionPrefix, + enableCorsCorrelation: true, + distributedTracingMode: DistributedTracingModes.AI_AND_W3C, + samplingPercentage: 50, + convertUndefined: "test-value" + }; + } +} diff --git a/AISKU/Tests/applicationinsights.e2e.tests.ts b/AISKU/Tests/applicationinsights.e2e.tests.ts index 4d9484fac..e4ffd4383 100644 --- a/AISKU/Tests/applicationinsights.e2e.tests.ts +++ b/AISKU/Tests/applicationinsights.e2e.tests.ts @@ -43,7 +43,7 @@ export class ApplicationInsightsTests extends TestClass { private successSpy: SinonSpy; private loggingSpy: SinonSpy; private userSpy: SinonSpy; - private sessionPrefix: string = Util.newId(); + private _sessionPrefix: string = Util.newId(); private trackSpy: SinonSpy; private envelopeConstructorSpy: SinonSpy; @@ -52,28 +52,32 @@ export class ApplicationInsightsTests extends TestClass { private _config; private _appId: string; - constructor() { - super("ApplicationInsightsTests"); + constructor(testName?: string) { + super(testName || "ApplicationInsightsTests"); } + protected _getTestConfig(sessionPrefix: string) { + return { + connectionString: ApplicationInsightsTests._connectionString, + disableAjaxTracking: false, + disableFetchTracking: false, + enableRequestHeaderTracking: true, + enableResponseHeaderTracking: true, + maxBatchInterval: 2500, + disableExceptionTracking: false, + namePrefix: sessionPrefix, + enableCorsCorrelation: true, + distributedTracingMode: DistributedTracingModes.AI_AND_W3C, + samplingPercentage: 50, + convertUndefined: "test-value" + }; + } + public testInitialize() { try { this.isFetchPolyfill = fetch["polyfill"]; this.useFakeServer = false; - this._config = { - connectionString: ApplicationInsightsTests._connectionString, - disableAjaxTracking: false, - disableFetchTracking: false, - enableRequestHeaderTracking: true, - enableResponseHeaderTracking: true, - maxBatchInterval: 2500, - disableExceptionTracking: false, - namePrefix: this.sessionPrefix, - enableCorsCorrelation: true, - distributedTracingMode: DistributedTracingModes.AI_AND_W3C, - samplingPercentage: 50, - convertUndefined: "test-value" - }; + this._config = this._getTestConfig(this._sessionPrefix); const init = new ApplicationInsights({ config: this._config diff --git a/README.md b/README.md index c9b91aa0d..5670f1010 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,9 @@ Most configuration fields are named such that they can be defaulted to falsey. A | isRetryDisabled | boolean | false | Default false. If false, retry on 206 (partial success), 408 (timeout), 429 (too many requests), 500 (internal server error), 503 (service unavailable), and 0 (offline, only if detected) | | isStorageUseDisabled | boolean | false | If true, the SDK will not store or read any data from local and session storage. Default is false. | | isBeaconApiDisabled | boolean | true | If false, the SDK will send all telemetry using the [Beacon API](https://www.w3.org/TR/beacon) | +| disableXhr | boolean | false | Don't use XMLHttpRequest or XDomainRequest (for IE < 9) by default instead attempt to use fetch() or sendBeacon. If no other transport is available it will still use XMLHttpRequest | | onunloadDisableBeacon | boolean | false | Default false. when tab is closed, the SDK will send all remaining telemetry using the [Beacon API](https://www.w3.org/TR/beacon) | +| onunloadDisableFetch | boolean | false | If fetch keepalive is supported do not use it for sending events during unload, it may still fallback to fetch() without keepalive | | sdkExtension | string | null | Sets the sdk extension name. Only alphabetic characters are allowed. The extension name is added as a prefix to the 'ai.internal.sdkVersion' tag (e.g. 'ext_javascript:2.0.0'). Default is null. | | isBrowserLinkTrackingEnabled | boolean | false | Default is false. If true, the SDK will track all [Browser Link](https://docs.microsoft.com/en-us/aspnet/core/client-side/using-browserlink) requests. | | appId | string | null | AppId is used for the correlation between AJAX dependencies happening on the client-side with the server-side requests. When Beacon API is enabled, it cannot be used automatically, but can be set manually in the configuration. Default is null | diff --git a/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts b/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts index 10e387e18..c1b8ac7e9 100644 --- a/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts +++ b/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts @@ -282,6 +282,54 @@ export class SenderTests extends AITestClass { } }); + this.testCase({ + name: 'FetchAPI is used when isBeaconApiDisabled flag is true and disableXhr flag is true , use fetch sender.', + test: () => { + let window = getGlobalInst("window"); + let fakeXMLHttpRequest = (window as any).XMLHttpRequest; + let fetchstub = this.sandbox.stub((window as any), "fetch"); + + let sendBeaconCalled = false; + this.hookSendBeacon((url: string) => { + sendBeaconCalled = true; + return false; + }); + + const sender = new Sender(); + const cr = new AppInsightsCore(); + + sender.initialize({ + instrumentationKey: 'abc', + isBeaconApiDisabled: true, + disableXhr: true + }, cr, []); + + const telemetryItem: ITelemetryItem = { + name: 'fake item', + iKey: 'iKey', + baseType: 'some type', + baseData: {} + }; + + QUnit.assert.ok(Util.IsBeaconApiSupported(), "Beacon API is supported"); + QUnit.assert.equal(false, sendBeaconCalled, "Beacon API was not called before"); + QUnit.assert.equal(0, this._getXhrRequests().length, "xhr sender was not called before"); + + try { + sender.processTelemetry(telemetryItem, null); + sender.flush(); + } catch(e) { + QUnit.assert.ok(false); + } + + QUnit.assert.equal(false, sendBeaconCalled, "Beacon API is disabled, Beacon API is not called"); + QUnit.assert.equal(0, this._getXhrRequests().length, "xhr sender is not called"); + QUnit.assert.ok(fetchstub.called, "fetch sender is called"); + // store it back + (window as any).XMLHttpRequest = fakeXMLHttpRequest; + } + }); + this.testCase({ name: 'FetchAPI is used when isBeaconApiDisabled flag is true and XMLHttpRequest is not supported, use fetch sender.', test: () => { diff --git a/channels/applicationinsights-channel-js/src/EnvelopeCreator.ts b/channels/applicationinsights-channel-js/src/EnvelopeCreator.ts index 7ecc40d4a..fb5fd3a95 100644 --- a/channels/applicationinsights-channel-js/src/EnvelopeCreator.ts +++ b/channels/applicationinsights-channel-js/src/EnvelopeCreator.ts @@ -22,367 +22,334 @@ function _setValueIf(target:T, field:keyof T, value:any) { return setValue(target, field, value, isTruthy); } -export abstract class EnvelopeCreator { - public static Version = "2.7.0-beta.1"; - - protected static extractPropsAndMeasurements(data: { [key: string]: any }, properties: { [key: string]: any }, measurements: { [key: string]: any }) { - if (!isNullOrUndefined(data)) { - objForEachKey(data, (key, value) => { - if (isNumber(value)) { - measurements[key] = value; - } else if (isString(value)) { - properties[key] = value; - } else if (hasJSON()) { - properties[key] = getJSON().stringify(value); - } - }); - } +/* + * Maps Part A data from CS 4.0 + */ +function _extractPartAExtensions(item: ITelemetryItem, env: IEnvelope) { + // todo: switch to keys from common in this method + let envTags = env.tags = env.tags || {}; + let itmExt = item.ext = item.ext || {}; + let itmTags = item.tags = item.tags || []; + + let extUser = itmExt.user; + if (extUser) { + _setValueIf(envTags, CtxTagKeys.userAuthUserId, extUser.authId); + _setValueIf(envTags, CtxTagKeys.userId, extUser.id || extUser.localId); } - protected static convertPropsUndefinedToCustomDefinedValue(properties: { [key: string]: any }, customUndefinedValue: any) { - if (!isNullOrUndefined(properties)) { - objForEachKey(properties, (key, value) => { - properties[key] = value || customUndefinedValue; - }); - } + let extApp = itmExt.app; + if (extApp) { + _setValueIf(envTags, CtxTagKeys.sessionId, extApp.sesId); } - // TODO: Do we want this to take logger as arg or use this._logger as nonstatic? - protected static createEnvelope(logger: IDiagnosticLogger, envelopeType: string, telemetryItem: ITelemetryItem, data: Data): IEnvelope { - const envelope = new Envelope(logger, data, envelopeType); - - _setValueIf(envelope, 'sampleRate', telemetryItem[SampleRate]); - if ((telemetryItem[strBaseData] || {}).startTime) { - envelope.time = toISOString(telemetryItem[strBaseData].startTime); - } - envelope.iKey = telemetryItem.iKey; - const iKeyNoDashes = telemetryItem.iKey.replace(/-/g, ""); - envelope.name = envelope.name.replace("{0}", iKeyNoDashes); + let extDevice = itmExt.device; + if (extDevice) { + _setValueIf(envTags, CtxTagKeys.deviceId, extDevice.id || extDevice.localId); + _setValueIf(envTags, CtxTagKeys.deviceType, extDevice.deviceClass); + _setValueIf(envTags, CtxTagKeys.deviceIp, extDevice.ip); + _setValueIf(envTags, CtxTagKeys.deviceModel, extDevice.model); + _setValueIf(envTags, CtxTagKeys.deviceType, extDevice.deviceType); + } - // extract all extensions from ctx - EnvelopeCreator.extractPartAExtensions(telemetryItem, envelope); + const web: IWeb = item.ext.web as IWeb; + if (web) { + _setValueIf(envTags, CtxTagKeys.deviceLanguage, web.browserLang); + _setValueIf(envTags, CtxTagKeys.deviceBrowserVersion, web.browserVer); + _setValueIf(envTags, CtxTagKeys.deviceBrowser, web.browser); - // loop through the envelope tags (extension of Part A) and pick out the ones that should go in outgoing envelope tags - telemetryItem.tags = telemetryItem.tags || []; + let envData = env.data = env.data || {}; + let envBaseData = envData[strBaseData] = envData[strBaseData] || {}; + let envProps = envBaseData[strProperties] = envBaseData[strProperties] || {}; - return optimizeObject(envelope); + _setValueIf(envProps, 'domain', web.domain); + _setValueIf(envProps, 'isManual', web.isManual ? strTrue : null); + _setValueIf(envProps, 'screenRes', web.screenRes); + _setValueIf(envProps, 'userConsent', web.userConsent ? strTrue : null); } - /* - * Maps Part A data from CS 4.0 - */ - private static extractPartAExtensions(item: ITelemetryItem, env: IEnvelope) { - // todo: switch to keys from common in this method - let envTags = env.tags = env.tags || {}; - let itmExt = item.ext = item.ext || {}; - let itmTags = item.tags = item.tags || []; - - let extUser = itmExt.user; - if (extUser) { - _setValueIf(envTags, CtxTagKeys.userAuthUserId, extUser.authId); - _setValueIf(envTags, CtxTagKeys.userId, extUser.id || extUser.localId); - } - - let extApp = itmExt.app; - if (extApp) { - _setValueIf(envTags, CtxTagKeys.sessionId, extApp.sesId); - } - - let extDevice = itmExt.device; - if (extDevice) { - _setValueIf(envTags, CtxTagKeys.deviceId, extDevice.id || extDevice.localId); - _setValueIf(envTags, CtxTagKeys.deviceType, extDevice.deviceClass); - _setValueIf(envTags, CtxTagKeys.deviceIp, extDevice.ip); - _setValueIf(envTags, CtxTagKeys.deviceModel, extDevice.model); - _setValueIf(envTags, CtxTagKeys.deviceType, extDevice.deviceType); - } + let extOs = itmExt.os; + if (extOs) { + _setValueIf(envTags, CtxTagKeys.deviceOS, extOs.name); + } - const web: IWeb = item.ext.web as IWeb; - if (web) { - _setValueIf(envTags, CtxTagKeys.deviceLanguage, web.browserLang); - _setValueIf(envTags, CtxTagKeys.deviceBrowserVersion, web.browserVer); - _setValueIf(envTags, CtxTagKeys.deviceBrowser, web.browser); + // No support for mapping Trace.traceState to 2.0 as it is currently empty - let envData = env.data = env.data || {}; - let envBaseData = envData[strBaseData] = envData[strBaseData] || {}; - let envProps = envBaseData[strProperties] = envBaseData[strProperties] || {}; + let extTrace = itmExt.trace; + if (extTrace) { + _setValueIf(envTags, CtxTagKeys.operationParentId, extTrace.parentID); + _setValueIf(envTags, CtxTagKeys.operationName, extTrace.name); + _setValueIf(envTags, CtxTagKeys.operationId, extTrace.traceID); + } - _setValueIf(envProps, 'domain', web.domain); - _setValueIf(envProps, 'isManual', web.isManual ? strTrue : null); - _setValueIf(envProps, 'screenRes', web.screenRes); - _setValueIf(envProps, 'userConsent', web.userConsent ? strTrue : null); - } + // Sample 4.0 schema + // { + // "time" : "2018-09-05T22:51:22.4936Z", + // "name" : "MetricWithNamespace", + // "iKey" : "ABC-5a4cbd20-e601-4ef5-a3c6-5d6577e4398e", + // "ext": { "cloud": { + // "role": "WATSON3", + // "roleInstance": "CO4AEAP00000260" + // }, + // "device": {}, "correlation": {} }, + // "tags": [ + // { "amazon.region" : "east2" }, + // { "os.expid" : "wp:02df239" } + // ] + // } + + const tgs = {}; + // deals with tags.push({object}) + for(let i = itmTags.length - 1; i >= 0; i--){ + const tg = itmTags[i]; + objForEachKey(tg, (key, value) => { + tgs[key] = value; + }); - let extOs = itmExt.os; - if (extOs) { - _setValueIf(envTags, CtxTagKeys.deviceOS, extOs.name); - } + itmTags.splice(i, 1); + } - // No support for mapping Trace.traceState to 2.0 as it is currently empty + // deals with tags[key]=value (and handles hasOwnProperty) + objForEachKey(itmTags, (tg, value) => { + tgs[tg] = value; + }); - let extTrace = itmExt.trace; - if (extTrace) { - _setValueIf(envTags, CtxTagKeys.operationParentId, extTrace.parentID); - _setValueIf(envTags, CtxTagKeys.operationName, extTrace.name); - _setValueIf(envTags, CtxTagKeys.operationId, extTrace.traceID); - } + let theTags = { ...envTags, ...tgs }; + if(!theTags[CtxTagKeys.internalSdkVersion]) { + // Append a version in case it is not already set + theTags[CtxTagKeys.internalSdkVersion] = `javascript:${EnvelopeCreator.Version}`; + } + + env.tags = optimizeObject(theTags); +} - // Sample 4.0 schema - // { - // "time" : "2018-09-05T22:51:22.4936Z", - // "name" : "MetricWithNamespace", - // "iKey" : "ABC-5a4cbd20-e601-4ef5-a3c6-5d6577e4398e", - // "ext": { "cloud": { - // "role": "WATSON3", - // "roleInstance": "CO4AEAP00000260" - // }, - // "device": {}, "correlation": {} }, - // "tags": [ - // { "amazon.region" : "east2" }, - // { "os.expid" : "wp:02df239" } - // ] - // } - - const tgs = {}; - // deals with tags.push({object}) - for(let i = itmTags.length - 1; i >= 0; i--){ - const tg = itmTags[i]; - objForEachKey(tg, (key, value) => { - tgs[key] = value; - }); - - itmTags.splice(i, 1); - } +function _extractPropsAndMeasurements(data: { [key: string]: any }, properties: { [key: string]: any }, measurements: { [key: string]: any }) { + if (!isNullOrUndefined(data)) { + objForEachKey(data, (key, value) => { + if (isNumber(value)) { + measurements[key] = value; + } else if (isString(value)) { + properties[key] = value; + } else if (hasJSON()) { + properties[key] = getJSON().stringify(value); + } + }); + } +} - // deals with tags[key]=value (and handles hasOwnProperty) - objForEachKey(itmTags, (tg, value) => { - tgs[tg] = value; +function _convertPropsUndefinedToCustomDefinedValue(properties: { [key: string]: any }, customUndefinedValue: any) { + if (!isNullOrUndefined(properties)) { + objForEachKey(properties, (key, value) => { + properties[key] = value || customUndefinedValue; }); + } +} - let theTags = { ...envTags, ...tgs }; - if(!theTags[CtxTagKeys.internalSdkVersion]) { - // Append a version in case it is not already set - theTags[CtxTagKeys.internalSdkVersion] = `javascript:${EnvelopeCreator.Version}`; - } - - env.tags = optimizeObject(theTags); +// TODO: Do we want this to take logger as arg or use this._logger as nonstatic? +function _createEnvelope(logger: IDiagnosticLogger, envelopeType: string, telemetryItem: ITelemetryItem, data: Data): IEnvelope { + const envelope = new Envelope(logger, data, envelopeType); + + _setValueIf(envelope, 'sampleRate', telemetryItem[SampleRate]); + if ((telemetryItem[strBaseData] || {}).startTime) { + envelope.time = toISOString(telemetryItem[strBaseData].startTime); } + envelope.iKey = telemetryItem.iKey; + const iKeyNoDashes = telemetryItem.iKey.replace(/-/g, ""); + envelope.name = envelope.name.replace("{0}", iKeyNoDashes); - protected _logger: IDiagnosticLogger; + // extract all extensions from ctx + _extractPartAExtensions(telemetryItem, envelope); - abstract Create(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope; + // loop through the envelope tags (extension of Part A) and pick out the ones that should go in outgoing envelope tags + telemetryItem.tags = telemetryItem.tags || []; - protected Init(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem) { - this._logger = logger; - if (isNullOrUndefined(telemetryItem[strBaseData])) { - this._logger.throwInternal( - LoggingSeverity.CRITICAL, - _InternalMessageId.TelemetryEnvelopeInvalid, "telemetryItem.baseData cannot be null."); - } + return optimizeObject(envelope); +} + +function EnvelopeCreatorInit(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem) { + if (isNullOrUndefined(telemetryItem[strBaseData])) { + logger.throwInternal( + LoggingSeverity.CRITICAL, + _InternalMessageId.TelemetryEnvelopeInvalid, "telemetryItem.baseData cannot be null."); } } -export class DependencyEnvelopeCreator extends EnvelopeCreator { - static DependencyEnvelopeCreator = new DependencyEnvelopeCreator(); +export const EnvelopeCreator = { + Version: "2.7.0-beta.1" +}; - Create(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { - super.Init(logger, telemetryItem); +export function DependencyEnvelopeCreator(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { + EnvelopeCreatorInit(logger, telemetryItem); - const customMeasurements = telemetryItem[strBaseData].measurements || {}; - const customProperties = telemetryItem[strBaseData][strProperties] || {}; - EnvelopeCreator.extractPropsAndMeasurements(telemetryItem.data, customProperties, customMeasurements); - if (!isNullOrUndefined(customUndefinedValue)) { - EnvelopeCreator.convertPropsUndefinedToCustomDefinedValue(customProperties, customUndefinedValue); - } - const bd = telemetryItem[strBaseData] as IDependencyTelemetry; - if (isNullOrUndefined(bd)) { - logger.warnToConsole("Invalid input for dependency data"); - return null; - } - - const method = bd[strProperties] && bd[strProperties][HttpMethod] ? bd[strProperties][HttpMethod] : "GET"; - const remoteDepData = new RemoteDependencyData(logger, bd.id, bd.target, bd.name, bd.duration, bd.success, bd.responseCode, method, bd.type, bd.correlationContext, customProperties, customMeasurements); - const data = new Data(RemoteDependencyData.dataType, remoteDepData); - return EnvelopeCreator.createEnvelope(logger, RemoteDependencyData.envelopeType, telemetryItem, data); + const customMeasurements = telemetryItem[strBaseData].measurements || {}; + const customProperties = telemetryItem[strBaseData][strProperties] || {}; + _extractPropsAndMeasurements(telemetryItem.data, customProperties, customMeasurements); + if (!isNullOrUndefined(customUndefinedValue)) { + _convertPropsUndefinedToCustomDefinedValue(customProperties, customUndefinedValue); + } + const bd = telemetryItem[strBaseData] as IDependencyTelemetry; + if (isNullOrUndefined(bd)) { + logger.warnToConsole("Invalid input for dependency data"); + return null; } -} -export class EventEnvelopeCreator extends EnvelopeCreator { - static EventEnvelopeCreator = new EventEnvelopeCreator(); + const method = bd[strProperties] && bd[strProperties][HttpMethod] ? bd[strProperties][HttpMethod] : "GET"; + const remoteDepData = new RemoteDependencyData(logger, bd.id, bd.target, bd.name, bd.duration, bd.success, bd.responseCode, method, bd.type, bd.correlationContext, customProperties, customMeasurements); + const data = new Data(RemoteDependencyData.dataType, remoteDepData); + return _createEnvelope(logger, RemoteDependencyData.envelopeType, telemetryItem, data); +} - Create(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { - super.Init(logger, telemetryItem); +export function EventEnvelopeCreator(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { + EnvelopeCreatorInit(logger, telemetryItem); - let customProperties = {}; - let customMeasurements = {}; - if (telemetryItem[strBaseType] !== Event.dataType) { - customProperties['baseTypeSource'] = telemetryItem[strBaseType]; // save the passed in base type as a property - } + let customProperties = {}; + let customMeasurements = {}; + if (telemetryItem[strBaseType] !== Event.dataType) { + customProperties['baseTypeSource'] = telemetryItem[strBaseType]; // save the passed in base type as a property + } - if (telemetryItem[strBaseType] === Event.dataType) { // take collection - customProperties = telemetryItem[strBaseData][strProperties] || {}; - customMeasurements = telemetryItem[strBaseData].measurements || {}; - } else { // if its not a known type, convert to custom event - if (telemetryItem[strBaseData]) { - EnvelopeCreator.extractPropsAndMeasurements(telemetryItem[strBaseData], customProperties, customMeasurements); - } + if (telemetryItem[strBaseType] === Event.dataType) { // take collection + customProperties = telemetryItem[strBaseData][strProperties] || {}; + customMeasurements = telemetryItem[strBaseData].measurements || {}; + } else { // if its not a known type, convert to custom event + if (telemetryItem[strBaseData]) { + _extractPropsAndMeasurements(telemetryItem[strBaseData], customProperties, customMeasurements); } + } - // Extract root level properties from part C telemetryItem.data - EnvelopeCreator.extractPropsAndMeasurements(telemetryItem.data, customProperties, customMeasurements); - if (!isNullOrUndefined(customUndefinedValue)) { - EnvelopeCreator.convertPropsUndefinedToCustomDefinedValue(customProperties, customUndefinedValue); - } - const eventName = telemetryItem[strBaseData].name; - const eventData = new Event(logger, eventName, customProperties, customMeasurements); - const data = new Data(Event.dataType, eventData); - return EnvelopeCreator.createEnvelope(logger, Event.envelopeType, telemetryItem, data); + // Extract root level properties from part C telemetryItem.data + _extractPropsAndMeasurements(telemetryItem.data, customProperties, customMeasurements); + if (!isNullOrUndefined(customUndefinedValue)) { + _convertPropsUndefinedToCustomDefinedValue(customProperties, customUndefinedValue); } + const eventName = telemetryItem[strBaseData].name; + const eventData = new Event(logger, eventName, customProperties, customMeasurements); + const data = new Data(Event.dataType, eventData); + return _createEnvelope(logger, Event.envelopeType, telemetryItem, data); } -export class ExceptionEnvelopeCreator extends EnvelopeCreator { - static ExceptionEnvelopeCreator = new ExceptionEnvelopeCreator(); +export function ExceptionEnvelopeCreator(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { + EnvelopeCreatorInit(logger, telemetryItem); - Create(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { - super.Init(logger, telemetryItem); - - // Extract root level properties from part C telemetryItem.data - const customMeasurements = telemetryItem[strBaseData].measurements || {}; - const customProperties = telemetryItem[strBaseData][strProperties] || {}; - EnvelopeCreator.extractPropsAndMeasurements(telemetryItem.data, customProperties, customMeasurements); - if (!isNullOrUndefined(customUndefinedValue)) { - EnvelopeCreator.convertPropsUndefinedToCustomDefinedValue(customProperties, customUndefinedValue); - } - const bd = telemetryItem[strBaseData] as IExceptionInternal; - const exData = Exception.CreateFromInterface(logger, bd, customProperties, customMeasurements); - const data = new Data(Exception.dataType, exData); - return EnvelopeCreator.createEnvelope(logger, Exception.envelopeType, telemetryItem, data); + // Extract root level properties from part C telemetryItem.data + const customMeasurements = telemetryItem[strBaseData].measurements || {}; + const customProperties = telemetryItem[strBaseData][strProperties] || {}; + _extractPropsAndMeasurements(telemetryItem.data, customProperties, customMeasurements); + if (!isNullOrUndefined(customUndefinedValue)) { + _convertPropsUndefinedToCustomDefinedValue(customProperties, customUndefinedValue); } + const bd = telemetryItem[strBaseData] as IExceptionInternal; + const exData = Exception.CreateFromInterface(logger, bd, customProperties, customMeasurements); + const data = new Data(Exception.dataType, exData); + return _createEnvelope(logger, Exception.envelopeType, telemetryItem, data); } -export class MetricEnvelopeCreator extends EnvelopeCreator { - static MetricEnvelopeCreator = new MetricEnvelopeCreator(); +export function MetricEnvelopeCreator(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { + EnvelopeCreatorInit(logger, telemetryItem); - Create(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { - super.Init(logger, telemetryItem); - - const baseData = telemetryItem[strBaseData]; - const props = baseData[strProperties] || {}; - const measurements = baseData.measurements || {}; - EnvelopeCreator.extractPropsAndMeasurements(telemetryItem.data, props, measurements); - if (!isNullOrUndefined(customUndefinedValue)) { - EnvelopeCreator.convertPropsUndefinedToCustomDefinedValue(props, customUndefinedValue); - } - const baseMetricData = new Metric(logger, baseData.name, baseData.average, baseData.sampleCount, baseData.min, baseData.max, props, measurements); - const data = new Data(Metric.dataType, baseMetricData); - return EnvelopeCreator.createEnvelope(logger, Metric.envelopeType, telemetryItem, data); + const baseData = telemetryItem[strBaseData]; + const props = baseData[strProperties] || {}; + const measurements = baseData.measurements || {}; + _extractPropsAndMeasurements(telemetryItem.data, props, measurements); + if (!isNullOrUndefined(customUndefinedValue)) { + _convertPropsUndefinedToCustomDefinedValue(props, customUndefinedValue); } + const baseMetricData = new Metric(logger, baseData.name, baseData.average, baseData.sampleCount, baseData.min, baseData.max, props, measurements); + const data = new Data(Metric.dataType, baseMetricData); + return _createEnvelope(logger, Metric.envelopeType, telemetryItem, data); } -export class PageViewEnvelopeCreator extends EnvelopeCreator { - static PageViewEnvelopeCreator = new PageViewEnvelopeCreator(); - - Create(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { - super.Init(logger, telemetryItem); - - // Since duration is not part of the domain properties in Common Schema, extract it from part C - let strDuration = "duration"; - let duration; - let baseData = telemetryItem[strBaseData]; - if (!isNullOrUndefined(baseData) && - !isNullOrUndefined(baseData[strProperties]) && - !isNullOrUndefined(baseData[strProperties][strDuration])) { // from part B properties - duration = baseData[strProperties][strDuration]; - delete baseData[strProperties][strDuration]; - } else if (!isNullOrUndefined(telemetryItem.data) && - !isNullOrUndefined(telemetryItem.data[strDuration])) { // from custom properties - duration = telemetryItem.data[strDuration]; - delete telemetryItem.data[strDuration]; - } +export function PageViewEnvelopeCreator(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { + EnvelopeCreatorInit(logger, telemetryItem); + + // Since duration is not part of the domain properties in Common Schema, extract it from part C + let strDuration = "duration"; + let duration; + let baseData = telemetryItem[strBaseData]; + if (!isNullOrUndefined(baseData) && + !isNullOrUndefined(baseData[strProperties]) && + !isNullOrUndefined(baseData[strProperties][strDuration])) { // from part B properties + duration = baseData[strProperties][strDuration]; + delete baseData[strProperties][strDuration]; + } else if (!isNullOrUndefined(telemetryItem.data) && + !isNullOrUndefined(telemetryItem.data[strDuration])) { // from custom properties + duration = telemetryItem.data[strDuration]; + delete telemetryItem.data[strDuration]; + } - const bd = telemetryItem[strBaseData] as IPageViewTelemetryInternal; + const bd = telemetryItem[strBaseData] as IPageViewTelemetryInternal; - // special case: pageview.id is grabbed from current operation id. Analytics plugin is decoupled from properties plugin, so this is done here instead. This can be made a default telemetry intializer instead if needed to be decoupled from channel - let currentContextId; - if (((telemetryItem.ext || {}).trace || {}).traceID) { - currentContextId = telemetryItem.ext.trace.traceID; - } - const id = bd.id || currentContextId - const name = bd.name; - const url = bd.uri; - const properties = bd[strProperties] || {}; - const measurements = bd.measurements || {}; - - // refUri is a field that Breeze still does not recognize as part of Part B. For now, put it in Part C until it supports it as a domain property - if (!isNullOrUndefined(bd.refUri)) { - properties["refUri"] = bd.refUri; - } + // special case: pageview.id is grabbed from current operation id. Analytics plugin is decoupled from properties plugin, so this is done here instead. This can be made a default telemetry intializer instead if needed to be decoupled from channel + let currentContextId; + if (((telemetryItem.ext || {}).trace || {}).traceID) { + currentContextId = telemetryItem.ext.trace.traceID; + } + const id = bd.id || currentContextId + const name = bd.name; + const url = bd.uri; + const properties = bd[strProperties] || {}; + const measurements = bd.measurements || {}; + + // refUri is a field that Breeze still does not recognize as part of Part B. For now, put it in Part C until it supports it as a domain property + if (!isNullOrUndefined(bd.refUri)) { + properties["refUri"] = bd.refUri; + } - // pageType is a field that Breeze still does not recognize as part of Part B. For now, put it in Part C until it supports it as a domain property - if (!isNullOrUndefined(bd.pageType)) { - properties["pageType"] = bd.pageType; - } + // pageType is a field that Breeze still does not recognize as part of Part B. For now, put it in Part C until it supports it as a domain property + if (!isNullOrUndefined(bd.pageType)) { + properties["pageType"] = bd.pageType; + } - // isLoggedIn is a field that Breeze still does not recognize as part of Part B. For now, put it in Part C until it supports it as a domain property - if (!isNullOrUndefined(bd.isLoggedIn)) { - properties["isLoggedIn"] = bd.isLoggedIn.toString(); - } + // isLoggedIn is a field that Breeze still does not recognize as part of Part B. For now, put it in Part C until it supports it as a domain property + if (!isNullOrUndefined(bd.isLoggedIn)) { + properties["isLoggedIn"] = bd.isLoggedIn.toString(); + } - // pageTags is a field that Breeze still does not recognize as part of Part B. For now, put it in Part C until it supports it as a domain property - if (!isNullOrUndefined(bd[strProperties])) { - const pageTags = bd[strProperties]; - objForEachKey(pageTags, (key, value) => { - properties[key] = value; - }); - } + // pageTags is a field that Breeze still does not recognize as part of Part B. For now, put it in Part C until it supports it as a domain property + if (!isNullOrUndefined(bd[strProperties])) { + const pageTags = bd[strProperties]; + objForEachKey(pageTags, (key, value) => { + properties[key] = value; + }); + } - EnvelopeCreator.extractPropsAndMeasurements(telemetryItem.data, properties, measurements); - if (!isNullOrUndefined(customUndefinedValue)) { - EnvelopeCreator.convertPropsUndefinedToCustomDefinedValue(properties, customUndefinedValue); - } - const pageViewData = new PageView(logger, name, url, duration, properties, measurements, id); - const data = new Data(PageView.dataType, pageViewData); - return EnvelopeCreator.createEnvelope(logger, PageView.envelopeType, telemetryItem, data); + _extractPropsAndMeasurements(telemetryItem.data, properties, measurements); + if (!isNullOrUndefined(customUndefinedValue)) { + _convertPropsUndefinedToCustomDefinedValue(properties, customUndefinedValue); } + const pageViewData = new PageView(logger, name, url, duration, properties, measurements, id); + const data = new Data(PageView.dataType, pageViewData); + return _createEnvelope(logger, PageView.envelopeType, telemetryItem, data); } -export class PageViewPerformanceEnvelopeCreator extends EnvelopeCreator { - static PageViewPerformanceEnvelopeCreator = new PageViewPerformanceEnvelopeCreator(); - - Create(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { - super.Init(logger, telemetryItem); - - const bd = telemetryItem[strBaseData] as IPageViewPerformanceTelemetry; - const name = bd.name; - const url = bd.uri || (bd as any).url; - const properties = bd[strProperties] || {}; - const measurements = bd.measurements || {}; - EnvelopeCreator.extractPropsAndMeasurements(telemetryItem.data, properties, measurements); - if (!isNullOrUndefined(customUndefinedValue)) { - EnvelopeCreator.convertPropsUndefinedToCustomDefinedValue(properties, customUndefinedValue); - } - const baseData = new PageViewPerformance(logger, name, url, undefined, properties, measurements, bd); - const data = new Data(PageViewPerformance.dataType, baseData); - return EnvelopeCreator.createEnvelope(logger, PageViewPerformance.envelopeType, telemetryItem, data); +export function PageViewPerformanceEnvelopeCreator(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { + EnvelopeCreatorInit(logger, telemetryItem); + + const bd = telemetryItem[strBaseData] as IPageViewPerformanceTelemetry; + const name = bd.name; + const url = bd.uri || (bd as any).url; + const properties = bd[strProperties] || {}; + const measurements = bd.measurements || {}; + _extractPropsAndMeasurements(telemetryItem.data, properties, measurements); + if (!isNullOrUndefined(customUndefinedValue)) { + _convertPropsUndefinedToCustomDefinedValue(properties, customUndefinedValue); } + const baseData = new PageViewPerformance(logger, name, url, undefined, properties, measurements, bd); + const data = new Data(PageViewPerformance.dataType, baseData); + return _createEnvelope(logger, PageViewPerformance.envelopeType, telemetryItem, data); } -export class TraceEnvelopeCreator extends EnvelopeCreator { - static TraceEnvelopeCreator = new TraceEnvelopeCreator(); +export function TraceEnvelopeCreator(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { + EnvelopeCreatorInit(logger, telemetryItem); - Create(logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any): IEnvelope { - super.Init(logger, telemetryItem); - - const message = telemetryItem[strBaseData].message; - const severityLevel = telemetryItem[strBaseData].severityLevel; - const props = telemetryItem[strBaseData][strProperties] || {}; - const measurements = telemetryItem[strBaseData].measurements || {}; - EnvelopeCreator.extractPropsAndMeasurements(telemetryItem.data, props, measurements); - if (!isNullOrUndefined(customUndefinedValue)) { - EnvelopeCreator.convertPropsUndefinedToCustomDefinedValue(props, customUndefinedValue); - } - const baseData = new Trace(logger, message, severityLevel, props, measurements); - const data = new Data(Trace.dataType, baseData); - return EnvelopeCreator.createEnvelope(logger, Trace.envelopeType, telemetryItem, data); + const message = telemetryItem[strBaseData].message; + const severityLevel = telemetryItem[strBaseData].severityLevel; + const props = telemetryItem[strBaseData][strProperties] || {}; + const measurements = telemetryItem[strBaseData].measurements || {}; + _extractPropsAndMeasurements(telemetryItem.data, props, measurements); + if (!isNullOrUndefined(customUndefinedValue)) { + _convertPropsUndefinedToCustomDefinedValue(props, customUndefinedValue); } + const baseData = new Trace(logger, message, severityLevel, props, measurements); + const data = new Data(Trace.dataType, baseData); + return _createEnvelope(logger, Trace.envelopeType, telemetryItem, data); } \ No newline at end of file diff --git a/channels/applicationinsights-channel-js/src/Interfaces.ts b/channels/applicationinsights-channel-js/src/Interfaces.ts index 21bdf0579..9d8ab782c 100644 --- a/channels/applicationinsights-channel-js/src/Interfaces.ts +++ b/channels/applicationinsights-channel-js/src/Interfaces.ts @@ -37,6 +37,17 @@ export interface ISenderConfig { isBeaconApiDisabled: () => boolean; + /** + * Don't use XMLHttpRequest or XDomainRequest (for IE < 9) by default instead attempt to use fetch() or sendBeacon. + * If no other transport is available it will still use XMLHttpRequest + */ + disableXhr: () => boolean; + + /** + * If fetch keepalive is supported do not use it for sending events during unload, it may still fallback to fetch() without keepalive + */ + onunloadDisableFetch: () => boolean; + /** * Is beacon disabled on page unload. * If enabled, flush events through beaconSender. diff --git a/channels/applicationinsights-channel-js/src/Offline.ts b/channels/applicationinsights-channel-js/src/Offline.ts index fd60daf92..bab8182ee 100644 --- a/channels/applicationinsights-channel-js/src/Offline.ts +++ b/channels/applicationinsights-channel-js/src/Offline.ts @@ -1,4 +1,4 @@ -import { EventHelper, getWindow, getDocument, getNavigator, isUndefined, isNullOrUndefined } from '@microsoft/applicationinsights-core-js'; +import { EventHelper, getWindow, getDocument, getNavigator, isUndefined, isNullOrUndefined, attachEvent } from '@microsoft/applicationinsights-core-js'; import dynamicProto from '@microsoft/dynamicproto-js'; /** @@ -19,8 +19,8 @@ export class OfflineListener { dynamicProto(OfflineListener, this, (_self) => { try { if (_window) { - if (EventHelper.Attach(_window, 'online', _setOnline)) { - EventHelper.Attach(_window, 'offline', _setOffline); + if (attachEvent(_window, 'online', _setOnline)) { + attachEvent(_window, 'offline', _setOffline); isListening = true; } } diff --git a/channels/applicationinsights-channel-js/src/SendBuffer.ts b/channels/applicationinsights-channel-js/src/SendBuffer.ts index 8200c18af..53e65b21c 100644 --- a/channels/applicationinsights-channel-js/src/SendBuffer.ts +++ b/channels/applicationinsights-channel-js/src/SendBuffer.ts @@ -14,6 +14,11 @@ export interface ISendBuffer { */ count: () => number; + /** + * Returns the current size of the serialized buffer + */ + size: () => number; + /** * Clears the buffer */ @@ -41,31 +46,54 @@ export interface ISendBuffer { clearSent: (payload: string[]) => void; } -/* - * An array based send buffer. - */ -export class ArraySendBuffer implements ISendBuffer { +abstract class BaseSendBuffer { + + protected _get: () => string[]; + protected _set: (buffer: string[]) => string[]; constructor(config: ISenderConfig) { let _buffer: string[] = []; - dynamicProto(ArraySendBuffer, this, (_self) => { + this._get = () => { + return _buffer; + }; + + this._set = (buffer: string[]) => { + _buffer = buffer; + return _buffer; + }; + + dynamicProto(BaseSendBuffer, this, (_self) => { + _self.enqueue = (payload: string) => { _buffer.push(payload); }; - + _self.count = (): number => { return _buffer.length; }; - + + _self.size = (): number => { + let size = _buffer.length; + for (let lp = 0; lp < _buffer.length; lp++) { + size += _buffer[lp].length; + } + + if (!config.emitLineDelimitedJson()) { + size += 2; + } + + return size; + }; + _self.clear = () => { - _buffer.length = 0; + _buffer = []; }; - + _self.getItems = (): string[] => { - return _buffer.slice(0); + return _buffer.slice(0) }; - + _self.batchPayloads = (payload: string[]): string => { if (payload && payload.length > 0) { const batch = config.emitLineDelimitedJson() ? @@ -77,14 +105,6 @@ export class ArraySendBuffer implements ISendBuffer { return null; }; - - _self.markAsSent = (payload: string[]) => { - _self.clear(); - }; - - _self.clearSent = (payload: string[]) => { - // not supported - }; }); } @@ -97,6 +117,11 @@ export class ArraySendBuffer implements ISendBuffer { return 0; } + public size(): number { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + return 0; + } + public clear() { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } @@ -110,6 +135,27 @@ export class ArraySendBuffer implements ISendBuffer { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging return null; } +} + +/* + * An array based send buffer. + */ +export class ArraySendBuffer extends BaseSendBuffer implements ISendBuffer { + + constructor(config: ISenderConfig) { + super(config); + + dynamicProto(ArraySendBuffer, this, (_self, _base) => { + + _self.markAsSent = (payload: string[]) => { + _base.clear(); + }; + + _self.clearSent = (payload: string[]) => { + // not supported + }; + }); + } public markAsSent(payload: string[]) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging @@ -123,7 +169,7 @@ export class ArraySendBuffer implements ISendBuffer { /* * Session storage buffer holds a copy of all unsent items in the browser session storage. */ -export class SessionStorageSendBuffer implements ISendBuffer { +export class SessionStorageSendBuffer extends BaseSendBuffer implements ISendBuffer { static BUFFER_KEY = "AI_buffer"; static SENT_BUFFER_KEY = "AI_sentBuffer"; @@ -131,34 +177,31 @@ export class SessionStorageSendBuffer implements ISendBuffer { static MAX_BUFFER_SIZE = 2000; constructor(logger: IDiagnosticLogger, config: ISenderConfig) { + super(config); let _bufferFullMessageSent = false; - // An in-memory copy of the buffer. A copy is saved to the session storage on enqueue() and clear(). - // The buffer is restored in a constructor and contains unsent events from a previous page. - let _buffer: string[]; - - dynamicProto(SessionStorageSendBuffer, this, (_self) => { + dynamicProto(SessionStorageSendBuffer, this, (_self, _base) => { const bufferItems = _getBuffer(SessionStorageSendBuffer.BUFFER_KEY); const notDeliveredItems = _getBuffer(SessionStorageSendBuffer.SENT_BUFFER_KEY); - _buffer = bufferItems.concat(notDeliveredItems); + const buffer = _self._set(bufferItems.concat(notDeliveredItems)); // If the buffer has too many items, drop items from the end. - if (_buffer.length > SessionStorageSendBuffer.MAX_BUFFER_SIZE) { - _buffer.length = SessionStorageSendBuffer.MAX_BUFFER_SIZE; + if (buffer.length > SessionStorageSendBuffer.MAX_BUFFER_SIZE) { + buffer.length = SessionStorageSendBuffer.MAX_BUFFER_SIZE; } _setBuffer(SessionStorageSendBuffer.SENT_BUFFER_KEY, []); - _setBuffer(SessionStorageSendBuffer.BUFFER_KEY, _buffer); + _setBuffer(SessionStorageSendBuffer.BUFFER_KEY, buffer); _self.enqueue = (payload: string) => { - if (_buffer.length >= SessionStorageSendBuffer.MAX_BUFFER_SIZE) { + if (_self.count() >= SessionStorageSendBuffer.MAX_BUFFER_SIZE) { // sent internal log only once per page view if (!_bufferFullMessageSent) { logger.throwInternal( LoggingSeverity.WARNING, _InternalMessageId.SessionStorageBufferFull, - "Maximum buffer size reached: " + _buffer.length, + "Maximum buffer size reached: " + _self.count(), true); _bufferFullMessageSent = true; } @@ -166,41 +209,21 @@ export class SessionStorageSendBuffer implements ISendBuffer { return; } - _buffer.push(payload); - _setBuffer(SessionStorageSendBuffer.BUFFER_KEY, _buffer); + _base.enqueue(payload); + _setBuffer(SessionStorageSendBuffer.BUFFER_KEY, _self._get()); }; - - _self.count = (): number => { - return _buffer.length; - }; - + _self.clear = () => { - _buffer = []; - _setBuffer(SessionStorageSendBuffer.BUFFER_KEY, []); + _base.clear(); + _setBuffer(SessionStorageSendBuffer.BUFFER_KEY, _self._get()); _setBuffer(SessionStorageSendBuffer.SENT_BUFFER_KEY, []); _bufferFullMessageSent = false; }; - _self.getItems = (): string[] => { - return _buffer.slice(0) - }; - - _self.batchPayloads = (payload: string[]): string => { - if (payload && payload.length > 0) { - const batch = config.emitLineDelimitedJson() ? - payload.join("\n") : - "[" + payload.join(",") + "]"; - - return batch; - } - - return null; - }; - _self.markAsSent = (payload: string[]) => { - _buffer = _removePayloadsFromBuffer(payload, _buffer); - _setBuffer(SessionStorageSendBuffer.BUFFER_KEY, _buffer); + _setBuffer(SessionStorageSendBuffer.BUFFER_KEY, + _self._set(_removePayloadsFromBuffer(payload, _self._get()))); let sentElements = _getBuffer(SessionStorageSendBuffer.SENT_BUFFER_KEY); if (sentElements instanceof Array && payload instanceof Array) { @@ -291,25 +314,10 @@ export class SessionStorageSendBuffer implements ISendBuffer { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } - public count(): number { - // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging - return 0; - } - public clear() { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } - public getItems(): string[] { - // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging - return null; - } - - public batchPayloads(payload: string[]): string { - // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging - return null; - } - public markAsSent(payload: string[]) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } diff --git a/channels/applicationinsights-channel-js/src/Sender.ts b/channels/applicationinsights-channel-js/src/Sender.ts index b3e2552c3..5e6acf8f5 100644 --- a/channels/applicationinsights-channel-js/src/Sender.ts +++ b/channels/applicationinsights-channel-js/src/Sender.ts @@ -10,18 +10,22 @@ import { DisabledPropertyName, RequestHeaders, IEnvelope, PageView, Event, Trace, Exception, Metric, PageViewPerformance, RemoteDependencyData, IChannelControlsAI, IConfig, ProcessLegacy, BreezeChannelIdentifier, - SampleRate, isInternalApplicationInsightsEndpoint, utlCanUseSessionStorage, isBeaconApiSupported + SampleRate, isInternalApplicationInsightsEndpoint, utlCanUseSessionStorage, + ISample } from '@microsoft/applicationinsights-common'; import { ITelemetryItem, IProcessTelemetryContext, IConfiguration, _InternalMessageId, LoggingSeverity, IDiagnosticLogger, IAppInsightsCore, IPlugin, getWindow, getNavigator, getJSON, BaseTelemetryPlugin, ITelemetryPluginChain, INotificationManager, - SendRequestReason, getGlobalInst, objForEachKey, isNullOrUndefined, arrForEach, dateNow, dumpObj, getExceptionName, getIEVersion, throwError, objKeys, strUndefined + SendRequestReason, objForEachKey, isNullOrUndefined, arrForEach, dateNow, dumpObj, getExceptionName, getIEVersion, throwError, objKeys, + isBeaconsSupported, isFetchSupported, useXDomainRequest, isXhrSupported, isArray } from '@microsoft/applicationinsights-core-js'; import { Offline } from './Offline'; import { Sample } from './TelemetryProcessors/Sample' import dynamicProto from '@microsoft/dynamicproto-js'; +const FetchSyncRequestSizeLimitBytes = 65000; // approx 64kb (the current Edge, Firefox and Chrome max limit) + declare var XDomainRequest: { prototype: IXDomainRequest; new(): IXDomainRequest; @@ -39,6 +43,40 @@ function _getResponseText(xhr: XMLHttpRequest | IXDomainRequest) { return null; } +function _getDefaultAppInsightsChannelConfig(): ISenderConfig { + // set default values + return { + endpointUrl: () => "https://dc.services.visualstudio.com/v2/track", + emitLineDelimitedJson: () => false, + maxBatchInterval: () => 15000, + maxBatchSizeInBytes: () => 102400, // 100kb + disableTelemetry: () => false, + enableSessionStorageBuffer: () => true, + isRetryDisabled: () => false, + isBeaconApiDisabled: () => true, + disableXhr: () => false, + onunloadDisableFetch: () => false, + onunloadDisableBeacon: () => false, + instrumentationKey: () => undefined, // Channel doesn't need iKey, it should be set already + namePrefix: () => undefined, + samplingPercentage: () => 100, + customHeaders: () => undefined, + convertUndefined: () => undefined, + } +} + +type EnvelopeCreator = (logger: IDiagnosticLogger, telemetryItem: ITelemetryItem, customUndefinedValue?: any) => IEnvelope; + +const EnvelopeTypeCreator: { [key:string] : EnvelopeCreator } = { + [Event.dataType]: EventEnvelopeCreator, + [Trace.dataType]: TraceEnvelopeCreator, + [PageView.dataType]: PageViewEnvelopeCreator, + [PageViewPerformance.dataType]: PageViewPerformanceEnvelopeCreator, + [Exception.dataType]: ExceptionEnvelopeCreator, + [Metric.dataType]: MetricEnvelopeCreator, + [RemoteDependencyData.dataType]: DependencyEnvelopeCreator +}; + export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { public static constructEnvelope(orig: ITelemetryItem, iKey: string, logger: IDiagnosticLogger, convertUndefined?: any): IEnvelope { @@ -52,79 +90,25 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { envelope = orig; } - switch (envelope.baseType) { - case Event.dataType: - return EventEnvelopeCreator.EventEnvelopeCreator.Create(logger, envelope, convertUndefined); - case Trace.dataType: - return TraceEnvelopeCreator.TraceEnvelopeCreator.Create(logger, envelope, convertUndefined); - case PageView.dataType: - return PageViewEnvelopeCreator.PageViewEnvelopeCreator.Create(logger, envelope, convertUndefined); - case PageViewPerformance.dataType: - return PageViewPerformanceEnvelopeCreator.PageViewPerformanceEnvelopeCreator.Create(logger, envelope, convertUndefined); - case Exception.dataType: - return ExceptionEnvelopeCreator.ExceptionEnvelopeCreator.Create(logger, envelope, convertUndefined); - case Metric.dataType: - return MetricEnvelopeCreator.MetricEnvelopeCreator.Create(logger, envelope, convertUndefined); - case RemoteDependencyData.dataType: - return DependencyEnvelopeCreator.DependencyEnvelopeCreator.Create(logger, envelope, convertUndefined); - default: - - return EventEnvelopeCreator.EventEnvelopeCreator.Create(logger, envelope, convertUndefined); - } - } + let creator: EnvelopeCreator = EnvelopeTypeCreator[envelope.baseType] || EventEnvelopeCreator; - private static _getDefaultAppInsightsChannelConfig(): ISenderConfig { - // set default values - return { - endpointUrl: () => "https://dc.services.visualstudio.com/v2/track", - emitLineDelimitedJson: () => false, - maxBatchInterval: () => 15000, - maxBatchSizeInBytes: () => 102400, // 100kb - disableTelemetry: () => false, - enableSessionStorageBuffer: () => true, - isRetryDisabled: () => false, - isBeaconApiDisabled: () => true, - onunloadDisableBeacon: () => false, - instrumentationKey: () => undefined, // Channel doesn't need iKey, it should be set already - namePrefix: () => undefined, - samplingPercentage: () => 100, - customHeaders: () => undefined, - convertUndefined: () => undefined, - } - } - - private static _getEmptyAppInsightsChannelConfig(): ISenderConfig { - return { - endpointUrl: undefined, - emitLineDelimitedJson: undefined, - maxBatchInterval: undefined, - maxBatchSizeInBytes: undefined, - disableTelemetry: undefined, - enableSessionStorageBuffer: undefined, - isRetryDisabled: undefined, - isBeaconApiDisabled: undefined, - onunloadDisableBeacon: undefined, - instrumentationKey: undefined, - namePrefix: undefined, - samplingPercentage: undefined, - customHeaders: undefined, - convertUndefined: undefined, - }; + return creator(logger, envelope, convertUndefined); } - public priority: number = 1001; + public readonly priority: number = 1001; - public identifier: string = BreezeChannelIdentifier; + public readonly identifier: string = BreezeChannelIdentifier; /** * The configuration for this sender instance */ - public _senderConfig: ISenderConfig; + public readonly _senderConfig: ISenderConfig; /** * A method which will cause data to be send to the url */ public _sender: SenderFunction; + /** * A send buffer object */ @@ -135,12 +119,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { */ public _appId: string; - /** - * Whether XMLHttpRequest object is supported. Older version of IE (8,9) do not support it. - */ - public _XMLHttpRequestSupported: boolean = false; - - protected _sample: Sample; + protected _sample: ISample; constructor() { super(); @@ -171,7 +150,23 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { let _headers: { [name: string]: string } = {}; + // Keep track of the outstanding sync fetch payload total (as sync fetch has limits) + let _syncFetchPayload = 0; + + /** + * The sender to use if the payload size is too large + */ + let _fallbackSender: SenderFunction; + + /** + * The identified sender to use for the synchronous unload stage + */ + let _syncUnloadSender: SenderFunction; + + this._senderConfig = _getDefaultAppInsightsChannelConfig(); + dynamicProto(Sender, this, (_self, _base) => { + function _notImplemented() { throwError("Method not implemented."); } @@ -192,9 +187,9 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { }; _self.onunloadFlush = () => { - if ((_self._senderConfig.onunloadDisableBeacon() === false || _self._senderConfig.isBeaconApiDisabled() === false) && isBeaconApiSupported()) { + if ((_self._senderConfig.onunloadDisableBeacon() === false || _self._senderConfig.isBeaconApiDisabled() === false) && isBeaconsSupported()) { try { - _self.triggerSend(true, _beaconSender, SendRequestReason.Unload); + _self.triggerSend(true, _doUnloadSend, SendRequestReason.Unload); } catch (e) { _self.diagLog().throwInternal(LoggingSeverity.CRITICAL, _InternalMessageId.FailedToSendQueuedTelemetry, @@ -223,15 +218,14 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { _self._sender = null; _stamp_specific_redirects = 0; - const defaultConfig = Sender._getDefaultAppInsightsChannelConfig(); - _self._senderConfig = Sender._getEmptyAppInsightsChannelConfig(); + const defaultConfig = _getDefaultAppInsightsChannelConfig(); objForEachKey(defaultConfig, (field, value) => { _self._senderConfig[field] = () => ctx.getConfig(identifier, field, value()); }); _self._buffer = (_self._senderConfig.enableSessionStorageBuffer() && utlCanUseSessionStorage()) ? new SessionStorageSendBuffer(_self.diagLog(), _self._senderConfig) : new ArraySendBuffer(_self._senderConfig); - _self._sample = new Sample(_self._senderConfig.samplingPercentage(), _self.diagLog()); + _self._sample = new Sample(_self._senderConfig.samplingPercentage(), _self.diagLog()); if(!_validateInstrumentationKey(config)) { _self.diagLog().throwInternal( @@ -245,32 +239,40 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { }); } - if (!_self._senderConfig.isBeaconApiDisabled() && isBeaconApiSupported()) { - _self._sender = _beaconSender; - } else { - const xhr: any = getGlobalInst("XMLHttpRequest"); - if(xhr) { - const testXhr = new xhr(); - try { - if ("withCredentials" in testXhr) { - _self._sender = _xhrSender; - _self._XMLHttpRequestSupported = true; - } - } catch(e) { - // This can happen with some native browser objects, but should not happen for the type we are checking for - } - - if (!_self._sender && typeof XDomainRequest !== strUndefined) { - _self._sender = _xdrSender; // IE 8 and 9 - } - } + let senderConfig = _self._senderConfig; + let sendPostFunc: SenderFunction = null; + if (!senderConfig.disableXhr() && useXDomainRequest()) { + sendPostFunc = _xdrSender; // IE 8 and 9 + } else if (!senderConfig.disableXhr() && isXhrSupported()) { + sendPostFunc = _xhrSender; + } - if (!_self._sender) { - const fetch: any = getGlobalInst("fetch"); - if (fetch) { - _self._sender = _fetchSender; - } - } + if (!sendPostFunc && isFetchSupported()) { + sendPostFunc = _fetchSender; + } + + // always fallback to XHR + _fallbackSender = sendPostFunc || _xhrSender; + + if (!senderConfig.isBeaconApiDisabled() && isBeaconsSupported()) { + // Config is set to always used beacon sending + sendPostFunc = _beaconSender; + } + + _self._sender = sendPostFunc || _xhrSender; + + if (!senderConfig.onunloadDisableFetch() && isFetchSupported(true)) { + // Try and use the fetch with keepalive + _syncUnloadSender = _fetchKeepAliveSender; + } else if (isBeaconsSupported()) { + // Try and use sendBeacon + _syncUnloadSender = _beaconSender; + } else if (!senderConfig.disableXhr() && useXDomainRequest()) { + _syncUnloadSender = _xdrSender; // IE 8 and 9 + } else if (!senderConfig.disableXhr() && isXhrSupported()) { + _syncUnloadSender = _xhrSender; + } else { + _syncUnloadSender = _fallbackSender; } }; @@ -355,15 +357,15 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { const payload: string = _serializer.serialize(aiEnvelope); // flush if we would exceed the max-size limit by adding this item - const bufferPayload = _self._buffer.getItems(); - const batch = _self._buffer.batchPayloads(bufferPayload); + const buffer = _self._buffer; + const bufferSize = buffer.size(); - if (batch && (batch.length + payload.length > _self._senderConfig.maxBatchSizeInBytes())) { + if ((bufferSize + payload.length) > _self._senderConfig.maxBatchSizeInBytes()) { _self.triggerSend(true, null, SendRequestReason.MaxBatchSize); } // enqueue the payload - _self._buffer.enqueue(payload); + buffer.enqueue(payload); // ensure an invocation timeout is set _setupTimer(); @@ -396,11 +398,13 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { */ _self.triggerSend = (async = true, forcedSender?: SenderFunction, sendReason?: SendRequestReason) => { try { + const buffer = _self._buffer; + // Send data only if disableTelemetry is false if (!_self._senderConfig.disableTelemetry()) { - if (_self._buffer.count() > 0) { - const payload = _self._buffer.getItems(); + if (buffer.count() > 0) { + const payload = buffer.getItems(); _notifySendRequest(sendReason||SendRequestReason.Undefined, async); @@ -415,7 +419,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { // update lastSend time to enable throttling _lastSend = +new Date; } else { - _self._buffer.clear(); + buffer.clear(); } clearTimeout(_timeoutHandle); @@ -596,31 +600,63 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { return false; } - /** - * Send Beacon API request - * @param payload {string} - The data payload to be sent. - * @param isAsync {boolean} - not used - * 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 _doUnloadSend(payload: string[], isAsync: boolean) { + if (_syncUnloadSender) { + // We are unloading so always call the sender with sync set to false + _syncUnloadSender(payload, false); + } else { + // Fallback to the previous beacon Sender (which causes a CORB warning on chrome now) + _beaconSender(payload, isAsync); + } + } + + function _doBeaconSend(payload: string[]) { + const nav = getNavigator(); + const buffer = _self._buffer; const url = _self._senderConfig.endpointUrl(); const batch = _self._buffer.batchPayloads(payload); - + // Chrome only allows CORS-safelisted values for the sendBeacon data argument // see: https://bugs.chromium.org/p/chromium/issues/detail?id=720283 const plainTextBatch = new Blob([batch], { type: 'text/plain;charset=UTF-8' }); // The sendBeacon method returns true if the user agent is able to successfully queue the data for transfer. Otherwise it returns false. - const queued = getNavigator().sendBeacon(url, plainTextBatch); - + const queued = nav.sendBeacon(url, plainTextBatch); if (queued) { - _self._buffer.markAsSent(payload); + buffer.markAsSent(payload); // no response from beaconSender, clear buffer _self._onSuccess(payload, payload.length); - } else { - _xhrSender(payload, true); - _self.diagLog().throwInternal(LoggingSeverity.WARNING, _InternalMessageId.TransmissionFailed, ". " + "Failed to send telemetry with Beacon API, retried with xhrSender."); + } + + return queued; + } + /** + * Send Beacon API request + * @param payload {string} - The data payload to be sent. + * @param isAsync {boolean} - not used + * 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) { + if (isArray(payload) && payload.length > 0) { + // The sendBeacon method returns true if the user agent is able to successfully queue the data for transfer. Otherwise it returns false. + if (!_doBeaconSend(payload)) { + // Failed to send entire payload so try and split data and try to send as much events as possible + let droppedPayload: string[] = []; + for (let lp = 0; lp < payload.length; lp++) { + const thePayload = payload[lp]; + + if (!_doBeaconSend([thePayload])) { + // Can't send anymore, so split the batch and drop the rest + droppedPayload.push(thePayload); + } + } + + if (droppedPayload.length > 0) { + _fallbackSender(droppedPayload, true); + _self.diagLog().throwInternal(LoggingSeverity.WARNING, _InternalMessageId.TransmissionFailed, ". " + "Failed to send telemetry with Beacon API, retried with normal sender."); + } + } } } @@ -660,47 +696,105 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { _self._buffer.markAsSent(payload); } + function _fetchKeepAliveSender(payload: string[], isAsync: boolean) { + if (isArray(payload)) { + let payloadSize = payload.length; + for (let lp = 0; lp < payload.length; lp++) { + payloadSize += payload[lp].length; + } + + if ((_syncFetchPayload + payloadSize) <= FetchSyncRequestSizeLimitBytes) { + _doFetchSender(payload, false); + } else if (isBeaconsSupported()) { + // Fallback to beacon sender as we at least get told which events can't be scheduled + _beaconSender(payload, isAsync); + } else { + // Payload is going to be too big so just try and send via XHR + _fallbackSender && _fallbackSender(payload, true); + _self.diagLog().throwInternal(LoggingSeverity.WARNING, _InternalMessageId.TransmissionFailed, ". " + "Failed to send telemetry with Beacon API, retried with xhrSender."); + } + } + } + /** * Send fetch API request * @param payload {string} - The data payload to be sent. * @param isAsync {boolean} - not used */ function _fetchSender(payload: string[], isAsync: boolean) { + _doFetchSender(payload, true); + } + + /** + * Send fetch API request + * @param payload {string} - The data payload to be sent. + * @param isAsync {boolean} - not used + */ + function _doFetchSender(payload: string[], isAsync: boolean) { const endPointUrl = _self._senderConfig.endpointUrl(); const batch = _self._buffer.batchPayloads(payload); - const plainTextBatch = new Blob([batch], { type: 'text/plain;charset=UTF-8' }); + const plainTextBatch = new Blob([batch], { type: 'application/json' }); let requestHeaders = new Headers(); + let batchLength = batch.length; + // append Sdk-Context request header only in case of breeze endpoint if (isInternalApplicationInsightsEndpoint(endPointUrl)) { requestHeaders.append(RequestHeaders.sdkContextHeader, RequestHeaders.sdkContextHeaderAppIdRequest); } + arrForEach(objKeys(_headers), (headerName) => { requestHeaders.append(headerName, _headers[headerName]); }); - const init = { + + const init: RequestInit = { method: "POST", headers: requestHeaders, - body: plainTextBatch + body: plainTextBatch, + [DisabledPropertyName]: true // Mark so we don't attempt to track this request + } + + if (!isAsync) { + init.keepalive = true; + _syncFetchPayload += batchLength; } + const request = new Request(endPointUrl, init); + try { + // Also try and tag the request (just in case the value in init is not copied over) + request[DisabledPropertyName] = true; + } catch(e) { + // If the environment has locked down the XMLHttpRequest (preventExtensions and/or freeze), this would + // cause the request to fail and we no telemetry would be sent + } fetch(request).then(response => { + if (!isAsync) { + _syncFetchPayload -= batchLength; + batchLength = 0; + } + /** * The Promise returned from fetch() won’t reject on HTTP error status even if the response is an HTTP 404 or 500. * Instead, it will resolve normally (with ok status set to false), and it will only reject on network failure * or if anything prevented the request from completing. */ if (!response.ok) { - throw Error(response.statusText); + _self._onError(payload, response.statusText) } else { response.text().then(text => { _checkResponsStatus(response.status, payload, response.url, payload.length, response.statusText, text); }); - _self._buffer.markAsSent(payload); } }).catch((error: Error) => { + if (!isAsync) { + _syncFetchPayload -= batchLength; + batchLength = 0; + } + _self._onError(payload, error.message) }); + + _self._buffer.markAsSent(payload); } /** @@ -739,11 +833,12 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { return; } - _self._buffer.clearSent(payload); + const buffer = _self._buffer; + buffer.clearSent(payload); _consecutiveErrors++; for (const item of payload) { - _self._buffer.enqueue(item); + buffer.enqueue(item); } // setup timer @@ -820,6 +915,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { * appId from the backend for the correct correlation. */ function _xdrSender(payload: string[], isAsync: boolean) { + const buffer = _self._buffer; let _window = getWindow(); const xdr = new XDomainRequest(); xdr.onload = () => _self._xdrOnLoad(xdr, payload); @@ -834,7 +930,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { _InternalMessageId.TransmissionFailed, ". " + "Cannot send XDomain request. The endpoint URL protocol doesn't match the hosting page protocol."); - _self._buffer.clear(); + buffer.clear(); return; } @@ -842,10 +938,10 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControlsAI { xdr.open('POST', endpointUrl); // compose an array of payloads - const batch = _self._buffer.batchPayloads(payload); + const batch = buffer.batchPayloads(payload); xdr.send(batch); - _self._buffer.markAsSent(payload); + buffer.markAsSent(payload); } function _formatErrorMessageXdr(xdr: IXDomainRequest, message?: string): string { diff --git a/channels/applicationinsights-channel-js/src/TelemetryProcessors/Sample.ts b/channels/applicationinsights-channel-js/src/TelemetryProcessors/Sample.ts index 9c178fb29..2e49ab4a8 100644 --- a/channels/applicationinsights-channel-js/src/TelemetryProcessors/Sample.ts +++ b/channels/applicationinsights-channel-js/src/TelemetryProcessors/Sample.ts @@ -11,13 +11,12 @@ export class Sample implements ISample { // We're using 32 bit math, hence max value is (2^31 - 1) public INT_MAX_VALUE: number = 2147483647; private samplingScoreGenerator: SamplingScoreGenerator; - private _logger: IDiagnosticLogger; constructor(sampleRate: number, logger?: IDiagnosticLogger) { - this._logger = logger || safeGetLogger(null); + let _logger = logger || safeGetLogger(null); if (sampleRate > 100 || sampleRate < 0) { - this._logger.throwInternal(LoggingSeverity.WARNING, + _logger.throwInternal(LoggingSeverity.WARNING, _InternalMessageId.SampleRateOutOfRange, "Sampling rate is out of range (0..100). Sampling will be disabled, you may be sending too much data which may affect your AI service level.", { samplingRate: sampleRate }, true); diff --git a/channels/applicationinsights-channel-js/src/TelemetryProcessors/SamplingScoreGenerators/HashCodeScoreGenerator.ts b/channels/applicationinsights-channel-js/src/TelemetryProcessors/SamplingScoreGenerators/HashCodeScoreGenerator.ts index 177368732..7d4ef1c6a 100644 --- a/channels/applicationinsights-channel-js/src/TelemetryProcessors/SamplingScoreGenerators/HashCodeScoreGenerator.ts +++ b/channels/applicationinsights-channel-js/src/TelemetryProcessors/SamplingScoreGenerators/HashCodeScoreGenerator.ts @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// (Magic number) DJB algorithm can't work on shorter strings (results in poor distribution +const MIN_INPUT_LENGTH: number = 8; + export class HashCodeScoreGenerator { // We're using 32 bit math, hence max value is (2^31 - 1) public static INT_MAX_VALUE: number = 2147483647; - // (Magic number) DJB algorithm can't work on shorter strings (results in poor distribution - private static MIN_INPUT_LENGTH: number = 8; - public getHashCodeScore(key: string): number { const score = this.getHashCode(key) / HashCodeScoreGenerator.INT_MAX_VALUE; return score * 100; @@ -16,7 +16,7 @@ export class HashCodeScoreGenerator { public getHashCode(input: string): number { if (input === "") { return 0; } - while (input.length < HashCodeScoreGenerator.MIN_INPUT_LENGTH) { + while (input.length < MIN_INPUT_LENGTH) { input = input.concat(input); } diff --git a/channels/applicationinsights-channel-js/src/TelemetryProcessors/SamplingScoreGenerators/SamplingScoreGenerator.ts b/channels/applicationinsights-channel-js/src/TelemetryProcessors/SamplingScoreGenerators/SamplingScoreGenerator.ts index c0f417dd4..561acbeed 100644 --- a/channels/applicationinsights-channel-js/src/TelemetryProcessors/SamplingScoreGenerators/SamplingScoreGenerator.ts +++ b/channels/applicationinsights-channel-js/src/TelemetryProcessors/SamplingScoreGenerators/SamplingScoreGenerator.ts @@ -6,29 +6,30 @@ import { ITelemetryItem } from '@microsoft/applicationinsights-core-js'; import { ContextTagKeys } from '@microsoft/applicationinsights-common'; export class SamplingScoreGenerator { - private hashCodeGeneragor: HashCodeScoreGenerator; - private keys: ContextTagKeys; + + public getSamplingScore: (item: ITelemetryItem) => number; constructor() { - this.hashCodeGeneragor = new HashCodeScoreGenerator(); - this.keys = new ContextTagKeys(); - } + let _self = this; + let hashCodeGenerator: HashCodeScoreGenerator = new HashCodeScoreGenerator(); + let keys: ContextTagKeys = new ContextTagKeys(); - public getSamplingScore(item: ITelemetryItem): number { - let score: number = 0; - if (item.tags && item.tags[this.keys.userId]) { // search in tags first, then ext - score = this.hashCodeGeneragor.getHashCodeScore(item.tags[this.keys.userId]); - } else if (item.ext && item.ext.user && item.ext.user.id) { - score = this.hashCodeGeneragor.getHashCodeScore(item.ext.user.id); - } else if (item.tags && item.tags[this.keys.operationId]) { // search in tags first, then ext - score = this.hashCodeGeneragor.getHashCodeScore(item.tags[this.keys.operationId]); - } else if (item.ext && item.ext.telemetryTrace && item.ext.telemetryTrace.traceID) { - score = this.hashCodeGeneragor.getHashCodeScore(item.ext.telemetryTrace.traceID); - } else { - // tslint:disable-next-line:insecure-random - score = (Math.random() * 100); + _self.getSamplingScore = (item: ITelemetryItem): number => { + let score: number = 0; + if (item.tags && item.tags[keys.userId]) { // search in tags first, then ext + score = hashCodeGenerator.getHashCodeScore(item.tags[keys.userId]); + } else if (item.ext && item.ext.user && item.ext.user.id) { + score = hashCodeGenerator.getHashCodeScore(item.ext.user.id); + } else if (item.tags && item.tags[keys.operationId]) { // search in tags first, then ext + score = hashCodeGenerator.getHashCodeScore(item.tags[keys.operationId]); + } else if (item.ext && item.ext.telemetryTrace && item.ext.telemetryTrace.traceID) { + score = hashCodeGenerator.getHashCodeScore(item.ext.telemetryTrace.traceID); + } else { + // tslint:disable-next-line:insecure-random + score = (Math.random() * 100); + } + + return score; } - - return score; } } \ No newline at end of file diff --git a/extensions/applicationinsights-clickanalytics-js/src/handlers/AutoCaptureHandler.ts b/extensions/applicationinsights-clickanalytics-js/src/handlers/AutoCaptureHandler.ts index 8a7f4f58b..6e0d4525d 100644 --- a/extensions/applicationinsights-clickanalytics-js/src/handlers/AutoCaptureHandler.ts +++ b/extensions/applicationinsights-clickanalytics-js/src/handlers/AutoCaptureHandler.ts @@ -2,7 +2,7 @@ * @copyright Microsoft 2020 */ -import { IDiagnosticLogger, _InternalMessageId, getWindow, getDocument, EventHelper, isNullOrUndefined } from "@microsoft/applicationinsights-core-js"; +import { IDiagnosticLogger, _InternalMessageId, getWindow, getDocument, isNullOrUndefined, attachEvent } from "@microsoft/applicationinsights-core-js"; import { IAutoCaptureHandler, IPageActionOverrideValues, IClickAnalyticsConfiguration } from '../Interfaces/Datamodel' import { isRightClick, isLeftClick, isKeyboardEnter, isKeyboardSpace, isMiddleClick, isElementDnt } from '../common/Utils'; import { ActionType } from '../Enums'; @@ -28,13 +28,13 @@ export class AutoCaptureHandler implements IAutoCaptureHandler { if (win) { // IE9 onwards addEventListener is available, 'click' event captures mouse click. mousedown works on other browsers const event = (navigator.appVersion.indexOf('MSIE') !== -1) ? 'click' : 'mousedown'; - EventHelper.Attach(win, event , (evt:any) => { this._processClick(evt); }); - EventHelper.Attach(win, 'keyup' , (evt:any) => { this._processClick(evt); }); + attachEvent(win, event , (evt:any) => { this._processClick(evt); }); + attachEvent(win, 'keyup' , (evt:any) => { this._processClick(evt); }); } else if (doc) { // IE8 and below doesn't have addEventListener so it will use attachEvent // attaching to window does not work in IE8 - EventHelper.Attach(doc, 'onclick' , (evt:any) => { this._processClick(evt); }); - EventHelper.Attach(doc, 'keyup' , (evt:any) => { this._processClick(evt); }); + attachEvent(doc, 'click' , (evt:any) => { this._processClick(evt); }); + attachEvent(doc, 'keyup' , (evt:any) => { this._processClick(evt); }); } } diff --git a/extensions/applicationinsights-dependencies-js/src/ajax.ts b/extensions/applicationinsights-dependencies-js/src/ajax.ts index 3706b5687..b377d0e38 100644 --- a/extensions/applicationinsights-dependencies-js/src/ajax.ts +++ b/extensions/applicationinsights-dependencies-js/src/ajax.ts @@ -4,16 +4,15 @@ import { RequestHeaders, CorrelationIdHelper, TelemetryItemCreator, ICorrelationConfig, RemoteDependencyData, dateTimeUtilsNow, DisabledPropertyName, IDependencyTelemetry, - IConfig, ITelemetryContext, PropertiesPluginIdentifier, DistributedTracingModes, IRequestContext + IConfig, ITelemetryContext, PropertiesPluginIdentifier, DistributedTracingModes, IRequestContext, isInternalApplicationInsightsEndpoint } from '@microsoft/applicationinsights-common'; import { isNullOrUndefined, arrForEach, isString, strTrim, isFunction, LoggingSeverity, _InternalMessageId, IAppInsightsCore, BaseTelemetryPlugin, ITelemetryPluginChain, IConfiguration, IPlugin, ITelemetryItem, IProcessTelemetryContext, getLocation, getGlobal, strUndefined, strPrototype, IInstrumentCallDetails, InstrumentFunc, InstrumentProto, getPerformance, - IInstrumentHooksCallbacks, IInstrumentHook, objForEachKey, generateW3CId, getIEVersion, dumpObj,objKeys, ICustomProperties + IInstrumentHooksCallbacks, IInstrumentHook, objForEachKey, generateW3CId, getIEVersion, dumpObj,objKeys, ICustomProperties, isXhrSupported, attachEvent } from '@microsoft/applicationinsights-core-js'; import { ajaxRecord, IAjaxRecordResponse } from './ajaxRecord'; -import { EventHelper } from './ajaxUtils'; import { Traceparent } from './TraceParent'; import dynamicProto from "@microsoft/dynamicproto-js"; @@ -47,7 +46,7 @@ function _supportsFetch(): (input: RequestInfo, init?: RequestInit) => Promise { + xhr[strAjaxData].xhrMonitoringState.stateChangeAttached = attachEvent(xhr, "readystatechange", () => { try { if (xhr && xhr.readyState === 4 && _isMonitoredXhrInstance(xhr)) { _onAjaxComplete(xhr); diff --git a/extensions/applicationinsights-dependencies-js/src/ajaxUtils.ts b/extensions/applicationinsights-dependencies-js/src/ajaxUtils.ts index 5fb254487..7e5a1c204 100644 --- a/extensions/applicationinsights-dependencies-js/src/ajaxUtils.ts +++ b/extensions/applicationinsights-dependencies-js/src/ajaxUtils.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import { isNullOrUndefined } from '@microsoft/applicationinsights-core-js'; -export { EventHelper } from '@microsoft/applicationinsights-core-js'; export class stringUtils { public static GetLength(strObject: any) { diff --git a/shared/AppInsightsCommon/src/HelperFuncs.ts b/shared/AppInsightsCommon/src/HelperFuncs.ts index a12f821e3..afbf14325 100644 --- a/shared/AppInsightsCommon/src/HelperFuncs.ts +++ b/shared/AppInsightsCommon/src/HelperFuncs.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { arrForEach, getNavigator, IPlugin, isString } from "@microsoft/applicationinsights-core-js"; +import { arrForEach, IPlugin, isString } from "@microsoft/applicationinsights-core-js"; export function stringToBoolOrDefault(str: any, defaultValue = false): boolean { if (str === undefined || str === null) { @@ -35,20 +35,6 @@ export function msToTimeSpan(totalms: number): string { return (days > 0 ? days + "." : "") + hour + ":" + min + ":" + sec + "." + ms; } -export function isBeaconApiSupported(): boolean { - let result = false; - try { - let nav = getNavigator(); - if (nav) { - result = ('sendBeacon' in nav && (nav as any).sendBeacon); - } - } catch (e) { - // This can happen with some native browser objects, but should not happen for the type we are checking for - } - - return result; -} - export function getExtensionByName(extensions: IPlugin[], identifier: string): IPlugin | null { let extension: IPlugin = null; arrForEach(extensions, (value) => { diff --git a/shared/AppInsightsCommon/src/Interfaces/Context/ISample.ts b/shared/AppInsightsCommon/src/Interfaces/Context/ISample.ts index 0840379d4..cc46225ef 100644 --- a/shared/AppInsightsCommon/src/Interfaces/Context/ISample.ts +++ b/shared/AppInsightsCommon/src/Interfaces/Context/ISample.ts @@ -1,9 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { ITelemetryItem } from "@microsoft/applicationinsights-core-js"; + export interface ISample { /** * Sample rate */ sampleRate: number; + + isSampledIn(envelope: ITelemetryItem): boolean; } \ No newline at end of file diff --git a/shared/AppInsightsCommon/src/Interfaces/IConfig.ts b/shared/AppInsightsCommon/src/Interfaces/IConfig.ts index a18c25dab..bb039bbcc 100644 --- a/shared/AppInsightsCommon/src/Interfaces/IConfig.ts +++ b/shared/AppInsightsCommon/src/Interfaces/IConfig.ts @@ -5,127 +5,97 @@ import { DistributedTracingModes } from '../Enums'; import { IRequestContext } from './IRequestContext'; /** - * @description Configuration settings for how telemetry is sent + * Configuration settings for how telemetry is sent * @export * @interface IConfig */ export interface IConfig { /** - * @description - * @type {boolean} - * @memberof IConfig + * The JSON format (normal vs line delimited). True means line delimited JSON. */ emitLineDelimitedJson?: boolean; /** - * @description An optional account id, if your app groups users into accounts. No spaces, commas, semicolons, equals, or vertical bars. - * @type {string} - * @memberof IConfig + * An optional account id, if your app groups users into accounts. No spaces, commas, semicolons, equals, or vertical bars. */ accountId?: string; /** - * @description A session is logged if the user is inactive for this amount of time in milliseconds. Default 30 mins. - * @type {number} - * @memberof IConfig + * A session is logged if the user is inactive for this amount of time in milliseconds. Default 30 mins. * @default 30*60*1000 */ sessionRenewalMs?: number; /** - * @description A session is logged if it has continued for this amount of time in milliseconds. Default 24h. - * @type {number} - * @memberof IConfig + * A session is logged if it has continued for this amount of time in milliseconds. Default 24h. * @default 24*60*60*1000 */ sessionExpirationMs?: number; /** - * @description Max size of telemetry batch. If batch exceeds limit, it is sent and a new batch is started - * @type {number} - * @memberof IConfig + * Max size of telemetry batch. If batch exceeds limit, it is sent and a new batch is started * @default 100000 */ maxBatchSizeInBytes?: number; /** - * @description How long to batch telemetry for before sending (milliseconds) - * @type {number} - * @memberof IConfig + * How long to batch telemetry for before sending (milliseconds) * @default 15 seconds */ maxBatchInterval?: number; /** - * @description If true, debugging data is thrown as an exception by the logger. Default false - * @type {boolean} - * @memberof IConfig + * If true, debugging data is thrown as an exception by the logger. Default false * @defaultValue false */ enableDebug?: boolean; /** - * @description If true, exceptions are not autocollected. Default is false - * @type {boolean} - * @memberof IConfig + * If true, exceptions are not autocollected. Default is false * @defaultValue false */ disableExceptionTracking?: boolean; /** - * @description If true, telemetry is not collected or sent. Default is false - * @type {boolean} - * @memberof IConfig + * If true, telemetry is not collected or sent. Default is false * @defaultValue false */ disableTelemetry?: boolean; /** - * @description Percentage of events that will be sent. Default is 100, meaning all events are sent. - * @type {number} - * @memberof IConfig + * Percentage of events that will be sent. Default is 100, meaning all events are sent. * @defaultValue 100 */ samplingPercentage?: number; /** - * @description If true, on a pageview, the previous instrumented page's view time is tracked and sent as telemetry and a new timer is started for the current pageview. It is sent as a custom metric named PageVisitTime in milliseconds and is calculated via the Date now() function (if available) and falls back to (new Date()).getTime() if now() is unavailable (IE8 or less). Default is false. - * @type {boolean} - * @memberof IConfig + * If true, on a pageview, the previous instrumented page's view time is tracked and sent as telemetry and a new timer is started for the current pageview. It is sent as a custom metric named PageVisitTime in milliseconds and is calculated via the Date now() function (if available) and falls back to (new Date()).getTime() if now() is unavailable (IE8 or less). Default is false. */ autoTrackPageVisitTime?: boolean; /** - * @description Automatically track route changes in Single Page Applications (SPA). If true, each route change will send a new Pageview to Application Insights. - * @type {boolean} - * @memberof IConfig + * Automatically track route changes in Single Page Applications (SPA). If true, each route change will send a new Pageview to Application Insights. */ enableAutoRouteTracking?: boolean; /** - * @description If true, Ajax calls are not autocollected. Default is false - * @type {boolean} - * @memberof IConfig + * If true, Ajax calls are not autocollected. Default is false * @defaultValue false */ disableAjaxTracking?: boolean; /** - * @description If true, Fetch requests are not autocollected. Default is true. - * @type {boolean} - * @memberof IConfig + * If true, Fetch requests are not autocollected. Default is true. * @defaultValue true */ disableFetchTracking?: boolean; /** - * @description Provide a way to exclude specific route from automatic tracking for XMLHttpRequest or Fetch request. For an ajax / fetch request that the request url matches with the regex patterns, auto tracking is turned off. - * @type {string[] | RegExp[]} - * @memberof IConfig + * Provide a way to exclude specific route from automatic tracking for XMLHttpRequest or Fetch request. For an ajax / fetch request that the request url matches with the regex patterns, auto tracking is turned off. * @defaultValue undefined. */ - excludeRequestFromAutoTrackingPatterns?: string[] | RegExp[]; + excludeRequestFromAutoTrackingPatterns?: string[] | RegExp[]; /** * Provide a way to enrich dependencies logs with context at the beginning of api call. @@ -134,71 +104,53 @@ export interface IConfig { addRequestContext?: (requestContext?: IRequestContext) => ICustomProperties; /** - * @description If true, default behavior of trackPageView is changed to record end of page view duration interval when trackPageView is called. If false and no custom duration is provided to trackPageView, the page view performance is calculated using the navigation timing API. Default is false - * @type {boolean} - * @memberof IConfig + * If true, default behavior of trackPageView is changed to record end of page view duration interval when trackPageView is called. If false and no custom duration is provided to trackPageView, the page view performance is calculated using the navigation timing API. Default is false * @defaultValue false */ overridePageViewDuration?: boolean; /** - * @description Default 500 - controls how many ajax calls will be monitored per page view. Set to -1 to monitor all (unlimited) ajax calls on the page. - * @type {number} - * @memberof IConfig + * Default 500 - controls how many ajax calls will be monitored per page view. Set to -1 to monitor all (unlimited) ajax calls on the page. */ maxAjaxCallsPerView?: number; /** * @ignore - * @description If false, internal telemetry sender buffers will be checked at startup for items not yet sent. Default is true - * @type {boolean} - * @memberof IConfig + * If false, internal telemetry sender buffers will be checked at startup for items not yet sent. Default is true * @defaultValue true */ disableDataLossAnalysis?: boolean; /** - * @description If false, the SDK will add two headers ('Request-Id' and 'Request-Context') to all dependency requests to correlate them with corresponding requests on the server side. Default is false. - * @type {boolean} - * @memberof IConfig + * If false, the SDK will add two headers ('Request-Id' and 'Request-Context') to all dependency requests to correlate them with corresponding requests on the server side. Default is false. * @defaultValue false */ disableCorrelationHeaders?: boolean; /** - * @description Sets the distributed tracing mode. If AI_AND_W3C mode or W3C mode is set, W3C trace context headers (traceparent/tracestate) will be generated and included in all outgoing requests. + * Sets the distributed tracing mode. If AI_AND_W3C mode or W3C mode is set, W3C trace context headers (traceparent/tracestate) will be generated and included in all outgoing requests. * AI_AND_W3C is provided for back-compatibility with any legacy Application Insights instrumented services - * @type {enum} - * @memberof IConfig * @defaultValue AI_AND_W3C */ distributedTracingMode?: DistributedTracingModes; /** - * @description Disable correlation headers for specific domain - * @type {string[]} - * @memberof IConfig + * Disable correlation headers for specific domain */ correlationHeaderExcludedDomains?: string[]; /** - * @description Default false. If true, flush method will not be called when onBeforeUnload event triggers. - * @type {boolean} - * @memberof IConfig + * Default false. If true, flush method will not be called when onBeforeUnload event triggers. */ disableFlushOnBeforeUnload?: boolean; /** - * @description Default value of {@link #disableFlushOnBeforeUnload}. If true, flush method will not be called when onUnload event triggers. - * @type {boolean} - * @memberof IConfig + * Default value of {@link #disableFlushOnBeforeUnload}. If true, flush method will not be called when onUnload event triggers. */ disableFlushOnUnload?: boolean; /** - * @description If true, the buffer with all unsent telemetry is stored in session storage. The buffer is restored on page load. Default is true. - * @type {boolean} - * @memberof IConfig + * 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 */ enableSessionStorageBuffer?: boolean; @@ -207,8 +159,6 @@ export interface IConfig { * @deprecated Use either disableCookiesUsage or specify a cookieMgrCfg with the enabled value set. * If true, the SDK will not store or read any data from cookies. Default is false. As this field is being deprecated, when both * isCookieUseDisabled and disableCookiesUsage are used disableCookiesUsage will take precedent. - * @type {boolean} - * @memberof IConfig * @defaultValue false */ isCookieUseDisabled?: boolean; @@ -216,24 +166,18 @@ export interface IConfig { /** * If true, the SDK will not store or read any data from cookies. Default is false. * If you have also specified a cookieMgrCfg then enabled property (if specified) will take precedent over this value. - * @type {boolean} - * @memberof IConfig * @defaultValue false */ disableCookiesUsage?: boolean; /** - * @description Custom cookie domain. This is helpful if you want to share Application Insights cookies across subdomains. - * @type {string} - * @memberof IConfig + * Custom cookie domain. This is helpful if you want to share Application Insights cookies across subdomains. * @defaultValue "" */ cookieDomain?: string; /** - * @description Custom cookie path. This is helpful if you want to share Application Insights cookies behind an application gateway. - * @type {string} - * @memberof IConfig + * Custom cookie path. This is helpful if you want to share Application Insights cookies behind an application gateway. * @defaultValue "" */ cookiePath?: string; @@ -248,112 +192,95 @@ export interface IConfig { /** * Default false. If false, retry on 206 (partial success), 408 (timeout), 429 (too many requests), 500 (internal server error), 503 (service unavailable), and 0 (offline, only if detected) * @description - * @type {boolean} - * @memberof IConfig * @defaultValue false */ isRetryDisabled?: boolean; /** * @deprecated Used when initizialing from snippet only. - * @description The url from where the JS SDK will be downloaded. - * @type {string} - * @memberof IConfig + * The url from where the JS SDK will be downloaded. */ url?: string; /** - * @description If true, the SDK will not store or read any data from local and session storage. Default is false. - * @type {boolean} - * @memberof IConfig + * If true, the SDK will not store or read any data from local and session storage. Default is false. * @defaultValue false */ isStorageUseDisabled?: boolean; /** - * @description If false, the SDK will send all telemetry using the [Beacon API](https://www.w3.org/TR/beacon) - * @type {boolean} - * @memberof IConfig + * If false, the SDK will send all telemetry using the [Beacon API](https://www.w3.org/TR/beacon) * @defaultValue true */ isBeaconApiDisabled?: boolean; /** - * @description Sets the sdk extension name. Only alphabetic characters are allowed. The extension name is added as a prefix to the 'ai.internal.sdkVersion' tag (e.g. 'ext_javascript:2.0.0'). Default is null. - * @type {string} - * @memberof IConfig + * Don't use XMLHttpRequest or XDomainRequest (for IE < 9) by default instead attempt to use fetch() or sendBeacon. + * If no other transport is available it will still use XMLHttpRequest + */ + disableXhr?: boolean; + + /** + * If fetch keepalive is supported do not use it for sending events during unload, it may still fallback to fetch() without keepalive + */ + onunloadDisableFetch?: boolean; + + /** + * Sets the sdk extension name. Only alphabetic characters are allowed. The extension name is added as a prefix to the 'ai.internal.sdkVersion' tag (e.g. 'ext_javascript:2.0.0'). Default is null. * @defaultValue null */ sdkExtension?: string; /** - * @description Default is false. If true, the SDK will track all [Browser Link](https://docs.microsoft.com/en-us/aspnet/core/client-side/using-browserlink) requests. - * @type {boolean} - * @memberof IConfig + * Default is false. If true, the SDK will track all [Browser Link](https://docs.microsoft.com/en-us/aspnet/core/client-side/using-browserlink) requests. * @defaultValue false */ isBrowserLinkTrackingEnabled?: boolean; /** - * @description AppId is used for the correlation between AJAX dependencies happening on the client-side with the server-side requets. When Beacon API is enabled, it cannot be used automatically, but can be set manually in the configuration. Default is null - * @type {string} - * @memberof IConfig + * AppId is used for the correlation between AJAX dependencies happening on the client-side with the server-side requets. When Beacon API is enabled, it cannot be used automatically, but can be set manually in the configuration. Default is null * @defaultValue null */ appId?: string; /** - * @description If true, the SDK will add two headers ('Request-Id' and 'Request-Context') to all CORS requests to correlate outgoing AJAX dependencies with corresponding requests on the server side. Default is false - * @type {boolean} - * @memberof IConfig + * If true, the SDK will add two headers ('Request-Id' and 'Request-Context') to all CORS requests to correlate outgoing AJAX dependencies with corresponding requests on the server side. Default is false * @defaultValue false */ enableCorsCorrelation?: boolean; /** - * @description An optional value that will be used as name postfix for localStorage and session cookie name. - * @type {string} - * @memberof IConfig + * An optional value that will be used as name postfix for localStorage and session cookie name. * @defaultValue null */ namePrefix?: string; /** - * @description An optional value that will be used as name postfix for session cookie name. If undefined, namePrefix is used as name postfix for session cookie name. - * @type {string} - * @memberof IConfig + * An optional value that will be used as name postfix for session cookie name. If undefined, namePrefix is used as name postfix for session cookie name. * @defaultValue null */ sessionCookiePostfix?: string; /** - * @description An optional value that will be used as name postfix for user cookie name. If undefined, no postfix is added on user cookie name. - * @type {string} - * @memberof IConfig + * An optional value that will be used as name postfix for user cookie name. If undefined, no postfix is added on user cookie name. * @defaultValue null */ userCookiePostfix?: string; /** - * @description An optional value that will track Request Header through trackDependency function. - * @type {boolean} - * @memberof IConfig + * An optional value that will track Request Header through trackDependency function. * @defaultValue false */ enableRequestHeaderTracking?: boolean; /** - * @description An optional value that will track Response Header through trackDependency function. - * @type {boolean} - * @memberof IConfig + * An optional value that will track Response Header through trackDependency function. * @defaultValue false */ enableResponseHeaderTracking?: boolean; /** - * @description An optional value that will track Response Error data through trackDependency function. - * @type {boolean} - * @memberof IConfig + * An optional value that will track Response Error data through trackDependency function. * @defaultValue false */ enableAjaxErrorStatusText?: boolean; @@ -381,9 +308,7 @@ export interface IConfig { ajaxPerfLookupDelay?: number; /** - * @description Default false. when tab is closed, the SDK will send all remaining telemetry using the [Beacon API](https://www.w3.org/TR/beacon) - * @type {boolean} - * @memberof IConfig + * Default false. when tab is closed, the SDK will send all remaining telemetry using the [Beacon API](https://www.w3.org/TR/beacon) * @defaultValue false */ onunloadDisableBeacon?: boolean; @@ -392,45 +317,40 @@ export interface IConfig { /** * @ignore - * @description Internal only - * @type {boolean} - * @memberof IConfig + * Internal only */ autoExceptionInstrumented?: boolean; + + /** + * + */ correlationHeaderDomains?: string[] /** * @ignore - * @description Internal only - * @type {boolean} - * @memberof IConfig + * Internal only */ autoUnhandledPromiseInstrumented?: boolean; /** - * @description Default false. Define whether to track unhandled promise rejections and report as JS errors. + * Default false. Define whether to track unhandled promise rejections and report as JS errors. * When disableExceptionTracking is enabled (dont track exceptions) this value will be false. - * @type {boolean} - * @memberof IConfig * @defaultValue false */ enableUnhandledPromiseRejectionTracking?: boolean; /** - * @description Disable correlation headers using regular expressions - * @type {RegExp[]} + * Disable correlation headers using regular expressions */ correlationHeaderExcludePatterns?: RegExp[]; /** - * @description The ability for the user to provide extra headers - * @type [{header: string, value: string}] + * The ability for the user to provide extra headers */ customHeaders?: [{header: string, value: string}]; /** - * @description Provide user an option to convert undefined field to user defined value. - * @type any + * Provide user an option to convert undefined field to user defined value. */ convertUndefined?: any } diff --git a/shared/AppInsightsCommon/src/Util.ts b/shared/AppInsightsCommon/src/Util.ts index 05913c0b9..838687268 100644 --- a/shared/AppInsightsCommon/src/Util.ts +++ b/shared/AppInsightsCommon/src/Util.ts @@ -3,20 +3,20 @@ import { StorageType } from "./Enums"; import { - _InternalMessageId, LoggingSeverity, IDiagnosticLogger, IPlugin, - getGlobal, getGlobalInst, getDocument, getNavigator, getPerformance, - getExceptionName as coreGetExceptionName, dumpObj, objForEachKey, - isString, isNullOrUndefined, strTrim, random32, isArray, isError, isDate, + _InternalMessageId, IDiagnosticLogger, IPlugin, getPerformance, + getExceptionName as coreGetExceptionName, dumpObj, + isNullOrUndefined, strTrim, random32, isArray, isError, isDate, newId, generateW3CId, toISOString, arrForEach, getIEVersion, attachEvent, dateNow, uaDisallowsSameSiteNone, disableCookies as coreDisableCookies, canUseCookies as coreCanUseCookies, getCookie as coreGetCookie, - setCookie as coreSetCookie, deleteCookie as coreDeleteCookie + setCookie as coreSetCookie, deleteCookie as coreDeleteCookie, + isBeaconsSupported } from "@microsoft/applicationinsights-core-js"; import { RequestHeaders } from "./RequestResponseHeaders"; import { dataSanitizeString } from "./Telemetry/Common/DataSanitizer"; import { ICorrelationConfig } from "./Interfaces/ICorrelationConfig"; import { createDomEvent } from './DomHelperFuncs'; -import { stringToBoolOrDefault, msToTimeSpan, isBeaconApiSupported, isCrossOriginError, getExtensionByName } from "./HelperFuncs"; +import { stringToBoolOrDefault, msToTimeSpan, isCrossOriginError, getExtensionByName } from "./HelperFuncs"; import { strNotSpecified } from "./Constants"; import { utlCanUseLocalStorage, utlCanUseSessionStorage, utlDisableStorage, utlGetSessionStorage, utlGetSessionStorageKeys, utlGetLocalStorage, utlRemoveSessionStorage, utlRemoveStorage, utlSetSessionStorage, utlSetLocalStorage } from "./StorageHelperFuncs"; import { urlGetAbsoluteUrl, urlGetCompleteUrl, urlGetPathName, urlParseFullHost, urlParseHost, urlParseUrl } from "./UrlHelperFuncs"; @@ -273,7 +273,7 @@ export const Util: IUtil = { dump: dumpObj, getExceptionName: coreGetExceptionName, addEventHandler: attachEvent, - IsBeaconApiSupported: isBeaconApiSupported, + IsBeaconApiSupported: isBeaconsSupported, getExtension: getExtensionByName }; diff --git a/shared/AppInsightsCommon/src/applicationinsights-common.ts b/shared/AppInsightsCommon/src/applicationinsights-common.ts index 2530b30d8..67b6a1381 100644 --- a/shared/AppInsightsCommon/src/applicationinsights-common.ts +++ b/shared/AppInsightsCommon/src/applicationinsights-common.ts @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - +// Licensed under the export { IUtil, Util, ICorrelationIdHelper, CorrelationIdHelper, IDateTimeUtils, DateTimeUtils, dateTimeUtilsNow, dateTimeUtilsDuration, @@ -59,7 +58,8 @@ export { IUser, IUserContext } from './Interfaces/Context/IUser'; export { ITelemetryTrace, ITraceState } from './Interfaces/Context/ITelemetryTrace'; export { IRequestContext } from './Interfaces/IRequestContext'; export { DistributedTracingModes } from './Enums'; -export { stringToBoolOrDefault, msToTimeSpan, isBeaconApiSupported, getExtensionByName, isCrossOriginError } from './HelperFuncs'; +export { stringToBoolOrDefault, msToTimeSpan, getExtensionByName, isCrossOriginError } from './HelperFuncs'; +export { isBeaconsSupported as isBeaconApiSupported } from "@microsoft/applicationinsights-core-js" export { createDomEvent } from './DomHelperFuncs'; export { utlDisableStorage, utlCanUseLocalStorage, utlGetLocalStorage, utlSetLocalStorage, utlRemoveStorage, diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/CoreUtils.ts b/shared/AppInsightsCore/src/JavaScriptSDK/CoreUtils.ts index c7a682043..166db689a 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/CoreUtils.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/CoreUtils.ts @@ -10,7 +10,7 @@ import { getWindow, getDocument, getPerformance, isIE } from "./EnvUtils"; import { arrForEach, arrIndexOf, arrMap, arrReduce, attachEvent, dateNow, detachEvent, hasOwnProperty, isArray, isBoolean, isDate, isError, isFunction, isNullOrUndefined, isNumber, isObject, isString, isTypeof, - isUndefined, objDefineAccessors, objFreeze, objKeys, strTrim, toISOString + isUndefined, objDefineAccessors, objKeys, strTrim, toISOString } from "./HelperFuncs"; import { randomValue, random32, mwcRandomSeed, mwcRandom32 } from "./RandomHelper"; @@ -36,7 +36,7 @@ export function addEventHandler(eventName: string, callback: any): boolean { let doc = getDocument(); if (doc) { - result = EventHelper.Attach(doc, eventName, callback) || result; + result = attachEvent(doc, eventName, callback) || result; } return result; diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts index acf00ac33..a253a2dc9 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts @@ -3,14 +3,17 @@ "use strict"; import { - getGlobal, strShimUndefined, strShimObject, strShimPrototype, strShimFunction + getGlobal, strShimUndefined, strShimObject, strShimPrototype } from "@microsoft/applicationinsights-shims"; -import { isString, strContains } from "./HelperFuncs"; +import { isString, isUndefined, strContains } from "./HelperFuncs"; + +// TypeScript removed this interface so we need to declare the global so we can check for it's existence. +declare var XDomainRequest: any; /** * This file exists to hold environment utilities that are required to check and * validate the current operating environment. Unless otherwise required, please - * only defined methods (functions) in this class so that users of these + * only use defined methods (functions) in this class so that users of these * functions/properties only need to include those that are used within their own modules. */ @@ -31,6 +34,37 @@ const strTrident = "trident/"; let _isTrident: boolean = null; let _navUserAgentCheck: string = null; let _enableMocks = false; +let _useXDomainRequest: boolean | null = null; +let _beaconsSupported: boolean | null = null; + + +function _hasProperty(theClass: any, property: string) { + let supported = false; + if (theClass) { + try { + supported = property in theClass; + if (!supported) { + let proto = theClass[strShimPrototype]; + if (proto) { + supported = property in proto; + } + } + } catch (e) { + // Do Nothing + } + + if (!supported) { + try { + let tmp = new theClass(); + supported = !isUndefined(tmp[property]); + } catch (e) { + // Do Nothing + } + } + } + + return supported; +} /** * Enable the lookup of test mock objects if requested @@ -313,3 +347,63 @@ export function isSafari(userAgentStr ?: string) { var ua = (userAgentStr || "").toLowerCase(); return (ua.indexOf('safari') >= 0); } + +/** + * Checks if HTML5 Beacons are supported in the current environment. + * @returns True if supported, false otherwise. + */ + export function isBeaconsSupported(): boolean { + if (_beaconsSupported === null) { + _beaconsSupported = hasNavigator() && Boolean(getNavigator().sendBeacon); + } + + return _beaconsSupported; +} + +/** + * Checks if the Fetch API is supported in the current environment. + * @param withKeepAlive - [Optional] If True, check if fetch is available and it supports the keepalive feature, otherwise only check if fetch is supported + * @returns True if supported, otherwise false + */ +export function isFetchSupported(withKeepAlive?: boolean): boolean { + let isSupported = false; + try { + const fetchApi = getGlobalInst("fetch"); + isSupported = !!fetchApi; + const request = getGlobalInst("Request"); + if (isSupported && withKeepAlive && request) { + isSupported = _hasProperty(request, "keepalive"); + } + } catch (e) { + // Just Swallow any failure during availability checks + } + + return isSupported; +} + +export function useXDomainRequest(): boolean | undefined { + if (_useXDomainRequest === null) { + _useXDomainRequest = (typeof XDomainRequest !== undefined); + if (_useXDomainRequest && isXhrSupported()) { + _useXDomainRequest = _useXDomainRequest && !_hasProperty(getGlobalInst("XMLHttpRequest"), "withCredentials"); + } + } + + return _useXDomainRequest; +} + +/** + * Checks if XMLHttpRequest is supported + * @returns True if supported, otherwise false + */ +export function isXhrSupported(): boolean { + let isSupported = false; + try { + const xmlHttpRequest = getGlobalInst("XMLHttpRequest"); + isSupported = !!xmlHttpRequest; + } catch (e) { + // Just Swallow any failure during availability checks + } + + return isSupported; +} diff --git a/shared/AppInsightsCore/src/applicationinsights-core-js.ts b/shared/AppInsightsCore/src/applicationinsights-core-js.ts index f13e7d961..689f9a20d 100644 --- a/shared/AppInsightsCore/src/applicationinsights-core-js.ts +++ b/shared/AppInsightsCore/src/applicationinsights-core-js.ts @@ -31,7 +31,7 @@ export { getGlobalInst, hasWindow, getWindow, hasDocument, getDocument, getCrypto, getMsCrypto, hasNavigator, getNavigator, hasHistory, getHistory, getLocation, getPerformance, hasJSON, getJSON, isReactNative, getConsole, dumpObj, isIE, getIEVersion, isSafari, - setEnableEnvMocks + setEnableEnvMocks, isBeaconsSupported, isFetchSupported, useXDomainRequest, isXhrSupported } from "./JavaScriptSDK/EnvUtils"; export { getGlobal,