diff --git a/.aiAutoMinify.json b/.aiAutoMinify.json index aa6199cf1..cca95219e 100644 --- a/.aiAutoMinify.json +++ b/.aiAutoMinify.json @@ -8,6 +8,7 @@ "eBatchDiscardedReason", "FeatureOptInMode", "CdnFeatureMode", + "eActiveStatus", "eLoggingSeverity", "_eInternalMessageId", "SendRequestReason", diff --git a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts index 9a9a52744..1748db697 100644 --- a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts +++ b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts @@ -5,10 +5,10 @@ import { Snippet } from "../../../src/Snippet"; import { utlRemoveSessionStorage } from "@microsoft/applicationinsights-common"; export class AISKUSizeCheck extends AITestClass { - private readonly MAX_RAW_SIZE = 141; - private readonly MAX_BUNDLE_SIZE = 141; - private readonly MAX_RAW_DEFLATE_SIZE = 57; - private readonly MAX_BUNDLE_DEFLATE_SIZE = 57; + private readonly MAX_RAW_SIZE = 143; + private readonly MAX_BUNDLE_SIZE = 143; + private readonly MAX_RAW_DEFLATE_SIZE = 58; + private readonly MAX_BUNDLE_DEFLATE_SIZE = 58; private readonly rawFilePath = "../dist/es5/applicationinsights-web.min.js"; // Automatically updated by version scripts private readonly currentVer = "3.2.2"; diff --git a/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts b/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts index e904aba9e..6055b0c41 100644 --- a/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts +++ b/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts @@ -3,8 +3,9 @@ import { SinonSpy } from 'sinon'; import { ApplicationInsights } from '../../../src/applicationinsights-web' import { Sender } from '@microsoft/applicationinsights-channel-js'; import { IDependencyTelemetry, ContextTagKeys, Event, Trace, Exception, Metric, PageView, PageViewPerformance, RemoteDependencyData, DistributedTracingModes, RequestHeaders, IAutoExceptionTelemetry, BreezeChannelIdentifier, IConfig } from '@microsoft/applicationinsights-common'; -import { ITelemetryItem, getGlobal, newId, dumpObj, BaseTelemetryPlugin, IProcessTelemetryContext, __getRegisteredEvents, arrForEach, IConfiguration, FeatureOptInMode } from "@microsoft/applicationinsights-core-js"; +import { ITelemetryItem, getGlobal, newId, dumpObj, BaseTelemetryPlugin, IProcessTelemetryContext, __getRegisteredEvents, arrForEach, IConfiguration, ActiveStatus, FeatureOptInMode } from "@microsoft/applicationinsights-core-js"; import { TelemetryContext } from '@microsoft/applicationinsights-properties-js'; +import { createAsyncResolvedPromise } from '@nevware21/ts-async'; import { CONFIG_ENDPOINT_URL } from '../../../src/InternalConstants'; import { OfflineChannel } from '@microsoft/applicationinsights-offlinechannel-js'; @@ -47,6 +48,7 @@ export class ApplicationInsightsTests extends AITestClass { private _appId: string; private _ctx: any; + constructor(testName?: string) { super(testName || "ApplicationInsightsTests"); } @@ -250,6 +252,217 @@ export class ApplicationInsightsTests extends AITestClass { } }); + this.testCaseAsync({ + name: "Init: init with cs promise, change with cs string", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + + // unload previous one first + let oriInst = this._ai; + if (oriInst && oriInst.unload) { + // force unload + oriInst.unload(false); + } + + if (oriInst && oriInst["dependencies"]) { + oriInst["dependencies"].teardown(); + } + + this._config = this._getTestConfig(this._sessionPrefix); + let csPromise = createAsyncResolvedPromise("InstrumentationKey=testIkey;ingestionendpoint=testUrl"); + this._config.connectionString = csPromise; + this._config.initTimeOut= 80000; + + + let init = new ApplicationInsights({ + config: this._config + }); + init.loadAppInsights(); + this._ai = init; + let config = this._ai.config; + let core = this._ai.core; + let status = core.activeStatus && core.activeStatus(); + Assert.equal(status, ActiveStatus.PENDING, "status should be set to pending"); + + + config.connectionString = "InstrumentationKey=testIkey1;ingestionendpoint=testUrl1"; + this.clock.tick(1); + status = core.activeStatus && core.activeStatus(); + // promise is not resolved, no new changes applied + Assert.equal(status, ActiveStatus.PENDING, "status should be set to pending test1"); + + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this._ai.core + let activeStatus = core.activeStatus && core.activeStatus(); + + if (activeStatus === ActiveStatus.ACTIVE) { + Assert.equal("testIkey", core.config.instrumentationKey, "ikey should be set"); + Assert.equal("testUrl/v2/track", core.config.endpointUrl ,"endpoint shoule be set"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "Init: init with cs promise and offline channel", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + + // unload previous one first + let oriInst = this._ai; + if (oriInst && oriInst.unload) { + // force unload + oriInst.unload(false); + } + + if (oriInst && oriInst["dependencies"]) { + oriInst["dependencies"].teardown(); + } + + this._config = this._getTestConfig(this._sessionPrefix); + let csPromise = createAsyncResolvedPromise("InstrumentationKey=testIkey;ingestionendpoint=testUrl"); + this._config.connectionString = csPromise; + let offlineChannel = new OfflineChannel(); + this._config.channels = [[offlineChannel]]; + this._config.initTimeOut= 80000; + + + let init = new ApplicationInsights({ + config: this._config + }); + init.loadAppInsights(); + this._ai = init; + let config = this._ai.config; + let core = this._ai.core; + let status = core.activeStatus && core.activeStatus(); + Assert.equal(status, ActiveStatus.PENDING, "status should be set to pending"); + + + config.connectionString = "InstrumentationKey=testIkey1;ingestionendpoint=testUrl1" + this.clock.tick(1); + status = core.activeStatus && core.activeStatus(); + Assert.equal(status, ActiveStatus.PENDING, "status should be set to pending test1"); + + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this._ai.core + let activeStatus = core.activeStatus && core.activeStatus(); + + if (activeStatus === ActiveStatus.ACTIVE) { + Assert.equal("testIkey", core.config.instrumentationKey, "ikey should be set"); + Assert.equal("testUrl/v2/track", core.config.endpointUrl ,"endpoint shoule be set"); + let sendChannel = this._ai.getPlugin(BreezeChannelIdentifier); + let offlineChannelPlugin = this._ai.getPlugin("OfflineChannel").plugin; + Assert.equal(sendChannel.plugin.isInitialized(), true, "sender is initialized"); + Assert.equal(offlineChannelPlugin.isInitialized(), true, "offline channel is initialized"); + let urlConfig = offlineChannelPlugin["_getDbgPlgTargets"]()[0]; + Assert.ok(urlConfig, "offline url config is initialized"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + + + this.testCaseAsync({ + name: "Init: init with cs string, change with cs promise", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + let config = this._ai.config; + let expectedIkey = ApplicationInsightsTests._instrumentationKey; + let expectedConnectionString = ApplicationInsightsTests._connectionString; + let expectedEndpointUrl = "https://dc.services.visualstudio.com/v2/track"; + Assert.ok(config, "ApplicationInsights config exists"); + Assert.equal(expectedConnectionString, config.connectionString, "connection string is set"); + Assert.equal(expectedIkey, config.instrumentationKey, "ikey is set"); + Assert.equal(expectedEndpointUrl, config.endpointUrl, "endpoint url is set from connection string"); + let core = this._ai.core; + let status = core.activeStatus && core.activeStatus(); + Assert.equal(status, ActiveStatus.ACTIVE, "status should be set to active"); + + let csPromise = createAsyncResolvedPromise("InstrumentationKey=testIkey;ingestionendpoint=testUrl"); + + config.connectionString = csPromise; + config.initTimeOut = 80000; + this.clock.tick(1); + status = core.activeStatus && core.activeStatus(); + Assert.equal(status, ActiveStatus.PENDING, "status should be set to pending"); + + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this._ai.core + let activeStatus = core.activeStatus && core.activeStatus(); + + if (activeStatus === ActiveStatus.ACTIVE) { + Assert.equal("testIkey", core.config.instrumentationKey, "ikey should be set"); + Assert.equal("testUrl/v2/track", core.config.endpointUrl ,"endpoint shoule be set"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "Init: init with cs null, ikey promise, endpoint promise", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + + // unload previous one first + let oriInst = this._ai; + if (oriInst && oriInst.unload) { + // force unload + oriInst.unload(false); + } + + if (oriInst && oriInst["dependencies"]) { + oriInst["dependencies"].teardown(); + } + + this._config = this._getTestConfig(this._sessionPrefix); + let ikeyPromise = createAsyncResolvedPromise("testIkey"); + let endpointPromise = createAsyncResolvedPromise("testUrl"); + //let csPromise = createAsyncResolvedPromise("InstrumentationKey=testIkey;ingestionendpoint=testUrl"); + //this._config.connectionString = csPromise; + this._config.connectionString = null; + this._config.instrumentationKey = ikeyPromise; + this._config.endpointUrl = endpointPromise; + this._config.initTimeOut= 80000; + + + + let init = new ApplicationInsights({ + config: this._config + }); + init.loadAppInsights(); + this._ai = init; + let config = this._ai.config; + let core = this._ai.core; + let status = core.activeStatus && core.activeStatus(); + Assert.equal(status, ActiveStatus.PENDING, "status should be set to pending"); + Assert.equal(config.connectionString,null, "connection string shoule be null"); + + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this._ai.core + let activeStatus = core.activeStatus && core.activeStatus(); + + if (activeStatus === ActiveStatus.ACTIVE) { + Assert.equal("testIkey", core.config.instrumentationKey, "ikey should be set"); + Assert.equal("testUrl", core.config.endpointUrl ,"endpoint shoule be set"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCase({ name: "CfgSync DynamicConfigTests: Prod CDN is Fetched and feature is turned on/off as expected", useFakeTimers: true, @@ -295,7 +508,7 @@ export class ApplicationInsightsTests extends AITestClass { }); this.testCase({ - name: "CfgSync DynamicConfigTests: Offline Support can be added and initialized with endpoint url", + name: "Init Promise: Offline Support can be added and initialized with endpoint url", useFakeTimers: true, test: () => { this.clock.tick(1); @@ -327,13 +540,13 @@ export class ApplicationInsightsTests extends AITestClass { if (ai && ai["dependencies"]) { ai["dependencies"].teardown(); } - offlineChannel.teardown(); + //offlineChannel.teardown(); } }); this.testCase({ - name: "CfgSync DynamicConfigTests: Offline Support can be added and initialized with channels", + name: "Init Promise: Offline Support can be added and initialized with channels", useFakeTimers: true, test: () => { this.clock.tick(1); @@ -365,7 +578,6 @@ export class ApplicationInsightsTests extends AITestClass { if (ai && ai["dependencies"]) { ai["dependencies"].teardown(); } - offlineChannel.teardown(); } }); @@ -377,7 +589,7 @@ export class ApplicationInsightsTests extends AITestClass { this.clock.tick(1); let offlineChannel = new OfflineChannel(); let config = { - instrumentationKey: "testIKey", + connectionString: "InstrumentationKey=testIKey", extensionConfig:{ ["AppInsightsCfgSyncPlugin"]: { cfgUrl: "" @@ -395,13 +607,14 @@ export class ApplicationInsightsTests extends AITestClass { Assert.equal(sendChannel.plugin.isInitialized(), true, "sender is initialized"); Assert.equal(offlineChannelPlugin.isInitialized(), true, "offline channel is initialized"); let urlConfig = offlineChannelPlugin["_getDbgPlgTargets"]()[0]; + + this.clock.tick(1); Assert.ok(urlConfig, "offline url config is initialized"); ai.unload(false); if (ai && ai["dependencies"]) { ai["dependencies"].teardown(); } - offlineChannel.teardown(); } }); diff --git a/AISKU/src/AISku.ts b/AISKU/src/AISku.ts index 6b91f2700..ad01ee30d 100644 --- a/AISKU/src/AISku.ts +++ b/AISKU/src/AISku.ts @@ -7,8 +7,8 @@ import { AnalyticsPlugin, ApplicationInsights } from "@microsoft/applicationinsi import { CfgSyncPlugin, ICfgSyncConfig, ICfgSyncMode } from "@microsoft/applicationinsights-cfgsync-js"; import { Sender } from "@microsoft/applicationinsights-channel-js"; import { - AnalyticsPluginIdentifier, DEFAULT_BREEZE_PATH, IAutoExceptionTelemetry, IConfig, IDependencyTelemetry, IEventTelemetry, - IExceptionTelemetry, IMetricTelemetry, IPageViewPerformanceTelemetry, IPageViewTelemetry, IRequestHeaders, + AnalyticsPluginIdentifier, ConnectionString, DEFAULT_BREEZE_PATH, IAutoExceptionTelemetry, IConfig, IDependencyTelemetry, + IEventTelemetry, IExceptionTelemetry, IMetricTelemetry, IPageViewPerformanceTelemetry, IPageViewTelemetry, IRequestHeaders, ITelemetryContext as Common_ITelemetryContext, IThrottleInterval, IThrottleLimit, IThrottleMgrConfig, ITraceTelemetry, PropertiesPluginIdentifier, ThrottleMgr, parseConnectionString } from "@microsoft/applicationinsights-common"; @@ -26,8 +26,8 @@ import { IDependencyListenerHandler } from "@microsoft/applicationinsights-dependencies-js"; import { PropertiesPlugin } from "@microsoft/applicationinsights-properties-js"; -import { IPromise, createPromise } from "@nevware21/ts-async"; -import { arrForEach, arrIndexOf, objDefine, objForEachKey, strIndexOf, throwUnsupported } from "@nevware21/ts-utils"; +import { IPromise, createAsyncPromise, createPromise, doAwaitResponse } from "@nevware21/ts-async"; +import { arrForEach, arrIndexOf, isPromiseLike, objDefine, objForEachKey, strIndexOf, throwUnsupported } from "@nevware21/ts-utils"; import { IApplicationInsights } from "./IApplicationInsights"; import { CONFIG_ENDPOINT_URL, STR_ADD_TELEMETRY_INITIALIZER, STR_CLEAR_AUTHENTICATED_USER_CONTEXT, STR_EVT_NAMESPACE, STR_GET_COOKIE_MGR, @@ -200,8 +200,65 @@ export class AppInsightsSku implements IApplicationInsights { // Will get recalled if any referenced values are changed _addUnloadHook(onConfigChange(cfgHandler, () => { - if (_config.connectionString) { - const cs = parseConnectionString(_config.connectionString); + let configCs = _config.connectionString; + + function _parseCs() { + return createAsyncPromise((resolve, reject) => { + doAwaitResponse(configCs, (res) => { + let curCs = res && res.value; + let parsedCs = null; + if (!res.rejected && curCs) { + // replace cs with resolved values in case of circular promises + _config.connectionString = curCs; + parsedCs = parseConnectionString(curCs); + } + // if can't resolve cs promise, null will be returned + resolve(parsedCs); + }); + }); + + } + + if (isPromiseLike(configCs)) { + let ikeyPromise = createAsyncPromise((resolve, reject) => { + _parseCs().then((cs) => { + let ikey = _config.instrumentationKey; + ikey = cs && cs.instrumentationkey || ikey; + resolve(ikey); + }).catch((e) => { + // parseCs will always resolve(unless timeout) + // return null in case any error happens + resolve(null); + }); + + }); + + let url: IPromise | string = _config.userOverrideEndpointUrl; + if (isNullOrUndefined(url)) { + url = createAsyncPromise((resolve, reject) => { + _parseCs().then((cs) => { + let url = _config.endpointUrl; + let ingest = cs && cs.ingestionendpoint; + url = ingest? ingest + DEFAULT_BREEZE_PATH : url; + resolve(url); + }).catch((e) => { + // parseCs will always resolve(unless timeout) + // return null in case any error happens + resolve(null); + }); + + }); + } + + _config.instrumentationKey = ikeyPromise; + _config.endpointUrl = url; + + } + if (isString(configCs)) { + // confirm if promiselike function present + // handle cs promise here + // add cases to oneNote + const cs = parseConnectionString(configCs); const ingest = cs.ingestionendpoint; _config.endpointUrl = _config.userOverrideEndpointUrl ? _config.userOverrideEndpointUrl : ingest + DEFAULT_BREEZE_PATH; // add /v2/track _config.instrumentationKey = cs.instrumentationkey || _config.instrumentationKey; diff --git a/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts b/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts index 0a29ec081..58cf32060 100644 --- a/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts +++ b/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts @@ -2,10 +2,10 @@ import { AITestClass, Assert } from "@microsoft/ai-test-framework"; import * as pako from "pako"; export class AISKULightSizeCheck extends AITestClass { - private readonly MAX_RAW_SIZE = 88; - private readonly MAX_BUNDLE_SIZE = 88; - private readonly MAX_RAW_DEFLATE_SIZE = 36; - private readonly MAX_BUNDLE_DEFLATE_SIZE = 36; + private readonly MAX_RAW_SIZE = 89; + private readonly MAX_BUNDLE_SIZE = 89; + private readonly MAX_RAW_DEFLATE_SIZE = 37; + private readonly MAX_BUNDLE_DEFLATE_SIZE = 37; private readonly rawFilePath = "../dist/es5/applicationinsights-web-basic.min.js"; private readonly currentVer = "3.2.2"; private readonly prodFilePath = `../browser/es5/aib.${this.currentVer[0]}.min.js`; diff --git a/AISKULight/src/index.ts b/AISKULight/src/index.ts index ecb298200..7e237dccb 100644 --- a/AISKULight/src/index.ts +++ b/AISKULight/src/index.ts @@ -8,8 +8,8 @@ import { AppInsightsCore, IConfigDefaults, IConfiguration, IDynamicConfigHandler, ILoadedPlugin, IPlugin, ITelemetryItem, ITelemetryPlugin, ITelemetryUnloadState, IUnloadHook, UnloadHandler, WatcherFunction, cfgDfValidate, createDynamicConfig, onConfigChange, proxyFunctions } from "@microsoft/applicationinsights-core-js"; -import { IPromise } from "@nevware21/ts-async"; -import { isNullOrUndefined, objDefine, throwError } from "@nevware21/ts-utils"; +import { IPromise, createAsyncPromise, doAwaitResponse } from "@nevware21/ts-async"; +import { isNullOrUndefined, isPromiseLike, isString, objDefine, throwError } from "@nevware21/ts-utils"; const defaultConfigValues: IConfigDefaults = { diagnosticLogInterval: cfgDfValidate(_chkDiagLevel, 10000) @@ -73,8 +73,45 @@ export class ApplicationInsights { _config = cfgHandler.cfg; core.addUnloadHook(onConfigChange(cfgHandler, () => { - if (_config.connectionString) { - const cs = parseConnectionString(_config.connectionString); + let configCs = _config.connectionString; + + if (isPromiseLike(configCs)) { + let ikeyPromise = createAsyncPromise((resolve, reject) => { + doAwaitResponse(configCs, (res) => { + let curCs = res.value; + let ikey = _config.instrumentationKey; + if (!res.rejected && curCs) { + // replace cs with resolved values in case of circular promises + _config.connectionString = curCs; + let resolvedCs = parseConnectionString(curCs); + ikey = resolvedCs.instrumentationkey || ikey; + } + resolve(ikey); + }); + + }); + + let urlPromise = createAsyncPromise((resolve, reject) => { + doAwaitResponse(configCs, (res) => { + let curCs = res.value; + let url = _config.endpointUrl; + if (!res.rejected && curCs) { + let resolvedCs = parseConnectionString(curCs); + let ingest = resolvedCs.ingestionendpoint; + url = ingest? ingest + DEFAULT_BREEZE_PATH : url; + } + resolve(url); + }); + + }); + + _config.instrumentationKey = ikeyPromise; + _config.endpointUrl = _config.userOverrideEndpointUrl || urlPromise; + + } + + if (isString(configCs)) { + const cs = parseConnectionString(configCs); const ingest = cs.ingestionendpoint; _config.endpointUrl = _config.userOverrideEndpointUrl ? _config.userOverrideEndpointUrl : (ingest + DEFAULT_BREEZE_PATH); // only add /v2/track when from connectionstring _config.instrumentationKey = cs.instrumentationkey || _config.instrumentationKey; 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 9cf6d2c13..4cf71a8b6 100644 --- a/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts +++ b/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts @@ -1,11 +1,12 @@ -import { AITestClass } from "@microsoft/ai-test-framework"; +import { AITestClass, PollingAssert } from "@microsoft/ai-test-framework"; import { Sender } from "../../../src/Sender"; import { IOfflineListener, createOfflineListener, utlGetSessionStorageKeys, utlRemoveSessionStorage } from "@microsoft/applicationinsights-common"; import { EnvelopeCreator } from '../../../src/EnvelopeCreator'; import { Exception, CtxTagKeys, isBeaconApiSupported, DEFAULT_BREEZE_ENDPOINT, DEFAULT_BREEZE_PATH, utlCanUseSessionStorage, utlGetSessionStorage, utlSetSessionStorage } from "@microsoft/applicationinsights-common"; -import { ITelemetryItem, AppInsightsCore, ITelemetryPlugin, DiagnosticLogger, NotificationManager, SendRequestReason, _eInternalMessageId, safeGetLogger, isString, isArray, arrForEach, isBeaconsSupported, IXHROverride, IPayloadData,TransportType, getWindow } from "@microsoft/applicationinsights-core-js"; +import { ITelemetryItem, AppInsightsCore, ITelemetryPlugin, DiagnosticLogger, NotificationManager, SendRequestReason, _eInternalMessageId, safeGetLogger, isString, isArray, arrForEach, isBeaconsSupported, IXHROverride, IPayloadData,TransportType, getWindow, ActiveStatus } from "@microsoft/applicationinsights-core-js"; import { ArraySendBuffer, SessionStorageSendBuffer } from "../../../src/SendBuffer"; import { IInternalStorageItem, ISenderConfig } from "../../../src/Interfaces"; +import { createAsyncResolvedPromise } from "@nevware21/ts-async"; @@ -163,6 +164,40 @@ export class SenderTests extends AITestClass { } }); + this.testCaseAsync({ + name: "Channel Init: init with promise", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + + let core = new AppInsightsCore(); + let ikeyPromise = createAsyncResolvedPromise("testIkey"); + let urlPromise = createAsyncResolvedPromise("testUrl"); + let coreConfig = { + instrumentationKey: ikeyPromise, + endpointUrl: urlPromise, + initTimeOut: 80000, + extensionConfig: {} + } + core.initialize(coreConfig, [this._sender]); + + let status = core.activeStatus && core.activeStatus(); + QUnit.assert.equal(status, ActiveStatus.PENDING, "status should be set to pending"); + + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this._sender.core; + let activeStatus = core.activeStatus && core.activeStatus(); + + if (activeStatus === ActiveStatus.ACTIVE) { + QUnit.assert.equal("testIkey", core.config.instrumentationKey, "ikey should be set"); + QUnit.assert.equal("testUrl", core.config.endpointUrl ,"endpoint shoule be set"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + this.testCase({ name: "Channel Config: Sender override can be handled correctly", useFakeTimers: true, diff --git a/channels/applicationinsights-channel-js/src/Sender.ts b/channels/applicationinsights-channel-js/src/Sender.ts index a08fee86d..4e29a92af 100644 --- a/channels/applicationinsights-channel-js/src/Sender.ts +++ b/channels/applicationinsights-channel-js/src/Sender.ts @@ -5,13 +5,14 @@ import { createOfflineListener, eRequestHeaders, isInternalApplicationInsightsEndpoint, utlCanUseSessionStorage, utlSetStoragePrefix } from "@microsoft/applicationinsights-common"; import { - BaseTelemetryPlugin, IAppInsightsCore, IBackendResponse, IChannelControls, IConfigDefaults, IConfiguration, IDiagnosticLogger, - IInternalOfflineSupport, INotificationManager, IPayloadData, IPlugin, IProcessTelemetryContext, IProcessTelemetryUnloadContext, - ITelemetryItem, ITelemetryPluginChain, ITelemetryUnloadState, IXDomainRequest, IXHROverride, OnCompleteCallback, SendPOSTFunction, - SendRequestReason, SenderPostManager, TransportType, _ISendPostMgrConfig, _ISenderOnComplete, _eInternalMessageId, _throwInternal, - _warnToConsole, arrForEach, cfgDfBoolean, cfgDfValidate, createProcessTelemetryContext, createUniqueNamespace, dateNow, dumpObj, - eLoggingSeverity, formatErrorMessageXdr, formatErrorMessageXhr, getExceptionName, getIEVersion, isArray, isBeaconsSupported, - isFetchSupported, isNullOrUndefined, mergeEvtNamespace, objExtend, onConfigChange, parseResponse, prependTransports, runTargetUnload + ActiveStatus, BaseTelemetryPlugin, IAppInsightsCore, IBackendResponse, IChannelControls, IConfigDefaults, IConfiguration, + IDiagnosticLogger, IInternalOfflineSupport, INotificationManager, IPayloadData, IPlugin, IProcessTelemetryContext, + IProcessTelemetryUnloadContext, ITelemetryItem, ITelemetryPluginChain, ITelemetryUnloadState, IXDomainRequest, IXHROverride, + OnCompleteCallback, SendPOSTFunction, SendRequestReason, SenderPostManager, TransportType, _ISendPostMgrConfig, _ISenderOnComplete, + _eInternalMessageId, _throwInternal, _warnToConsole, arrForEach, cfgDfBoolean, cfgDfValidate, createProcessTelemetryContext, + createUniqueNamespace, dateNow, dumpObj, eLoggingSeverity, formatErrorMessageXdr, formatErrorMessageXhr, getExceptionName, getIEVersion, + isArray, isBeaconsSupported, isFetchSupported, isNullOrUndefined, mergeEvtNamespace, objExtend, onConfigChange, parseResponse, + prependTransports, runTargetUnload } from "@microsoft/applicationinsights-core-js"; import { IPromise } from "@nevware21/ts-async"; import { ITimerHandler, isNumber, isString, isTruthy, objDeepFreeze, objDefine, scheduleTimeout } from "@nevware21/ts-utils"; @@ -242,7 +243,8 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { if (_self.isInitialized()) { _throwInternal(_self.diagLog(), eLoggingSeverity.CRITICAL, _eInternalMessageId.SenderNotInitialized, "Sender is already initialized"); } - + + _base.initialize(config, core, extensions, pluginChain); let identifier = _self.identifier; _serializer = new Serializer(core.logger); @@ -270,6 +272,19 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { } }); + // or is not string + if (core.activeStatus() === ActiveStatus.PENDING) { + // waiting for core promises to be resolved + // NOTE: if active status is set to pending, stop sending, clear timer here + _self.pause(); + } else if (core.activeStatus() === ActiveStatus.ACTIVE) { + // core status changed from pending to other status + _self.resume(); + + } + + + // Only update the endpoint if the original config !== the current config // This is so any redirect endpointUrl is not overwritten if (_orgEndpointUrl !== senderConfig.endpointUrl) { @@ -348,7 +363,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { } _customHeaders = senderConfig.customHeaders; - if (!isInternalApplicationInsightsEndpoint(_endpointUrl) && _customHeaders && _customHeaders.length > 0) { + if (isString(_endpointUrl) && !isInternalApplicationInsightsEndpoint(_endpointUrl) && _customHeaders && _customHeaders.length > 0) { arrForEach(_customHeaders, customHeader => { this.addHeader(customHeader.header, customHeader.value); }); diff --git a/channels/offline-channel-js/src/OfflineChannel.ts b/channels/offline-channel-js/src/OfflineChannel.ts index 2a33a3277..23562d893 100644 --- a/channels/offline-channel-js/src/OfflineChannel.ts +++ b/channels/offline-channel-js/src/OfflineChannel.ts @@ -13,7 +13,7 @@ import { eLoggingSeverity, mergeEvtNamespace, onConfigChange, runTargetUnload } from "@microsoft/applicationinsights-core-js"; import { IPromise, ITaskScheduler, createAsyncPromise, createTaskScheduler } from "@nevware21/ts-async"; -import { ITimerHandler, isFunction, objDeepFreeze, scheduleTimeout } from "@nevware21/ts-utils"; +import { ITimerHandler, isFunction, isString, objDeepFreeze, scheduleTimeout } from "@nevware21/ts-utils"; import { EVT_DISCARD_STR, EVT_SENT_STR, EVT_STORE_STR, batchDropNotification, callNotification, isGreaterThanZero } from "./Helpers/Utils"; @@ -401,12 +401,12 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr _storeNotification(sentItems); } }; - if (payloadData) { + if (payloadData && _urlCfg && _urlCfg.batchHandler) { let promise = _urlCfg.batchHandler.storeBatch(payloadData, callback, unload); _queueStorageEvent("storeBatch", promise); } - if (!_inMemoBatch.count()) { + if (_inMemoBatch && !_inMemoBatch.count()) { _inMemoFlushTimer && _inMemoFlushTimer.cancel(); } @@ -448,8 +448,11 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr } } - let promise = _urlCfg.batchHandler.sendNextBatch(callback, false, _senderInst); - _queueStorageEvent("sendNextBatch", promise); + if (_urlCfg && _urlCfg.batchHandler) { + let promise = _urlCfg.batchHandler.sendNextBatch(callback, false, _senderInst); + _queueStorageEvent("sendNextBatch", promise); + } + } } else { @@ -570,6 +573,14 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr function _createUrlConfig(coreConfig: IConfiguration & IConfig, core: IAppInsightsCore, extensions: IPlugin[], pluginChain?: ITelemetryPluginChain) { _self._addHook(onConfigChange(coreConfig, (details) => { + if (!isString(coreConfig.instrumentationKey) || !isString(coreConfig.endpointUrl)) { + // if ikey or endpointUrl is promise, delay initialization + _self.pause(); + return; + } + if (_paused) { + _self.resume(); + } let storageConfig: IOfflineChannelConfiguration = null; let theConfig = details.cfg; @@ -614,7 +625,7 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr handler.initialize(providerContext); urlConfig = { - iKey: coreConfig.instrumentationKey, + iKey: coreConfig.instrumentationKey as string, url: curUrl, minPersistenceCacheLevel: storageConfig.minPersistenceLevel, coreRootCtx: coreRootCtx, diff --git a/channels/offline-channel-js/src/Providers/IndexDbProvider.ts b/channels/offline-channel-js/src/Providers/IndexDbProvider.ts index fb55ecf19..790058fa2 100644 --- a/channels/offline-channel-js/src/Providers/IndexDbProvider.ts +++ b/channels/offline-channel-js/src/Providers/IndexDbProvider.ts @@ -5,7 +5,7 @@ import dynamicProto from "@microsoft/dynamicproto-js"; import { EventPersistence } from "@microsoft/applicationinsights-common"; import { INotificationManager, IProcessTelemetryContext, IUnloadHookContainer, eBatchDiscardedReason, eLoggingSeverity, isNotNullOrUndefined, - isNumber, newGuid, onConfigChange + isNumber, isString, newGuid, onConfigChange } from "@microsoft/applicationinsights-core-js"; import { IPromise, createAsyncAllPromise, createAsyncPromise, doAwait, doAwaitResponse } from "@nevware21/ts-async"; import { batchDropNotification, getEndpointDomain, getTimeFromId, getTimeId } from "../Helpers/Utils"; @@ -280,7 +280,12 @@ export class IndexedDbProvider implements IOfflineProvider { } let coreConfig = providerContext.itemCtx.getCfg(); let itemCtx = providerContext.itemCtx; - _iKey = itemCtx.getCfg().instrumentationKey || coreConfig.instrumentationKey; + let ikey = itemCtx.getCfg().instrumentationKey || coreConfig.instrumentationKey; + if (!isString(ikey)) { + //_iKey = ikey + return; + } + _iKey = ikey; let storageConfig: IOfflineChannelConfiguration = providerContext.storageConfig; _storageId = _this.id || providerContext.id || newGuid(); diff --git a/common/config/rush/npm-shrinkwrap.json b/common/config/rush/npm-shrinkwrap.json index 4303090c1..4bfc03d9a 100644 --- a/common/config/rush/npm-shrinkwrap.json +++ b/common/config/rush/npm-shrinkwrap.json @@ -1573,16 +1573,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.1.tgz", - "integrity": "sha512-kZqi+WZQaZfPKnsflLJQCz6Ze9FFSMfXrrIOcyargekQxG37ES7DJNpJUE9Q/X5n3yTIP/WPutVNzgknQ7biLg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz", + "integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==", "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/type-utils": "7.13.1", - "@typescript-eslint/utils": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/type-utils": "7.14.1", + "@typescript-eslint/utils": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1606,15 +1606,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.13.1.tgz", - "integrity": "sha512-1ELDPlnLvDQ5ybTSrMhRTFDfOQEOXNM+eP+3HT/Yq7ruWpciQw+Avi73pdEbA4SooCawEWo3dtYbF68gN7Ed1A==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/typescript-estree": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "debug": "^4.3.4" }, "engines": { @@ -1634,13 +1634,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.1.tgz", - "integrity": "sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz", + "integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==", "peer": true, "dependencies": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1" + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1651,13 +1651,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.13.1.tgz", - "integrity": "sha512-aWDbLu1s9bmgPGXSzNCxELu+0+HQOapV/y+60gPXafR8e2g1Bifxzevaa+4L2ytCWm+CHqpELq4CSoN9ELiwCg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz", + "integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==", "peer": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.13.1", - "@typescript-eslint/utils": "7.13.1", + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/utils": "7.14.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1678,9 +1678,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.1.tgz", - "integrity": "sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz", + "integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==", "peer": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1691,13 +1691,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.1.tgz", - "integrity": "sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz", + "integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==", "peer": true, "dependencies": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1755,15 +1755,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.1.tgz", - "integrity": "sha512-h5MzFBD5a/Gh/fvNdp9pTfqJAbuQC4sCN2WzuXme71lqFJsZtLbjxfSk4r3p02WIArOF9N94pdsLiGutpDbrXQ==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz", + "integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/typescript-estree": "7.13.1" + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1777,12 +1777,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.1.tgz", - "integrity": "sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz", + "integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==", "peer": true, "dependencies": { - "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/types": "7.14.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2641,9 +2641,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.807", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.807.tgz", - "integrity": "sha512-kSmJl2ZwhNf/bcIuCH/imtNOKlpkLDn2jqT5FJ+/0CXjhnFaOa9cOe9gHKKy71eM49izwuQjZhKk+lWQ1JxB7A==" + "version": "1.4.811", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.811.tgz", + "integrity": "sha512-CDyzcJ5XW78SHzsIOdn27z8J4ist8eaFLhdto2hSMSJQgsiwvbv2fbizcKUICryw1Wii1TI/FEkvzvJsR3awrA==" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -4142,9 +4142,9 @@ "peer": true }, "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==" }, "node_modules/jsonfile": { "version": "4.0.0", @@ -7681,16 +7681,16 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.1.tgz", - "integrity": "sha512-kZqi+WZQaZfPKnsflLJQCz6Ze9FFSMfXrrIOcyargekQxG37ES7DJNpJUE9Q/X5n3yTIP/WPutVNzgknQ7biLg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz", + "integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==", "peer": true, "requires": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/type-utils": "7.13.1", - "@typescript-eslint/utils": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/type-utils": "7.14.1", + "@typescript-eslint/utils": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -7698,54 +7698,54 @@ } }, "@typescript-eslint/parser": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.13.1.tgz", - "integrity": "sha512-1ELDPlnLvDQ5ybTSrMhRTFDfOQEOXNM+eP+3HT/Yq7ruWpciQw+Avi73pdEbA4SooCawEWo3dtYbF68gN7Ed1A==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==", "peer": true, "requires": { - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/typescript-estree": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.1.tgz", - "integrity": "sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz", + "integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==", "peer": true, "requires": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1" + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1" } }, "@typescript-eslint/type-utils": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.13.1.tgz", - "integrity": "sha512-aWDbLu1s9bmgPGXSzNCxELu+0+HQOapV/y+60gPXafR8e2g1Bifxzevaa+4L2ytCWm+CHqpELq4CSoN9ELiwCg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz", + "integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==", "peer": true, "requires": { - "@typescript-eslint/typescript-estree": "7.13.1", - "@typescript-eslint/utils": "7.13.1", + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/utils": "7.14.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/types": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.1.tgz", - "integrity": "sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz", + "integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==", "peer": true }, "@typescript-eslint/typescript-estree": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.1.tgz", - "integrity": "sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz", + "integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==", "peer": true, "requires": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -7781,24 +7781,24 @@ } }, "@typescript-eslint/utils": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.1.tgz", - "integrity": "sha512-h5MzFBD5a/Gh/fvNdp9pTfqJAbuQC4sCN2WzuXme71lqFJsZtLbjxfSk4r3p02WIArOF9N94pdsLiGutpDbrXQ==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz", + "integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==", "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/typescript-estree": "7.13.1" + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1" } }, "@typescript-eslint/visitor-keys": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.1.tgz", - "integrity": "sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz", + "integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==", "peer": true, "requires": { - "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/types": "7.14.1", "eslint-visitor-keys": "^3.4.3" } }, @@ -8403,9 +8403,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.807", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.807.tgz", - "integrity": "sha512-kSmJl2ZwhNf/bcIuCH/imtNOKlpkLDn2jqT5FJ+/0CXjhnFaOa9cOe9gHKKy71eM49izwuQjZhKk+lWQ1JxB7A==" + "version": "1.4.811", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.811.tgz", + "integrity": "sha512-CDyzcJ5XW78SHzsIOdn27z8J4ist8eaFLhdto2hSMSJQgsiwvbv2fbizcKUICryw1Wii1TI/FEkvzvJsR3awrA==" }, "encodeurl": { "version": "1.0.2", @@ -9533,9 +9533,9 @@ "peer": true }, "jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==" }, "jsonfile": { "version": "4.0.0", diff --git a/extensions/applicationinsights-dependencies-js/Tests/Unit/src/ajax.tests.ts b/extensions/applicationinsights-dependencies-js/Tests/Unit/src/ajax.tests.ts index 31a8e5b9a..cb2386453 100644 --- a/extensions/applicationinsights-dependencies-js/Tests/Unit/src/ajax.tests.ts +++ b/extensions/applicationinsights-dependencies-js/Tests/Unit/src/ajax.tests.ts @@ -1,11 +1,12 @@ import { SinonStub } from "sinon"; import { Assert, AITestClass, PollingAssert } from "@microsoft/ai-test-framework"; -import { createSyncPromise } from "@nevware21/ts-async"; +import { createAsyncResolvedPromise, createSyncPromise } from "@nevware21/ts-async"; import { AjaxMonitor } from "../../../src/ajax"; import { DisabledPropertyName, IConfig, DistributedTracingModes, RequestHeaders, IDependencyTelemetry, IRequestContext, formatTraceParent, createTraceParent, PropertiesPluginIdentifier } from "@microsoft/applicationinsights-common"; import { AppInsightsCore, IConfiguration, ITelemetryItem, ITelemetryPlugin, IChannelControls, _eInternalMessageId, - getPerformance, getGlobalInst, getGlobal, generateW3CId, arrForEach + getPerformance, getGlobalInst, getGlobal, generateW3CId, arrForEach, + ActiveStatus } from "@microsoft/applicationinsights-core-js"; import { IDependencyListenerDetails } from "../../../src/DependencyListener"; import { FakeXMLHttpRequest } from "@microsoft/ai-test-framework"; @@ -208,6 +209,59 @@ export class AjaxTests extends AITestClass { } }); + this.testCaseAsync({ + name: "Dependencies Configuration: init with cs promise ikey promise and default enableAjaxPerfTracking", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + this._ajax = new AjaxMonitor(); + let csPromise = createAsyncResolvedPromise("testIkey"); + let appInsightsCore = new AppInsightsCore(); + let coreConfig = { + instrumentationKey: csPromise, + initTimeOut: 80000 + + }; + appInsightsCore.initialize(coreConfig, [this._ajax, new TestChannelPlugin()]); + + let trackStub = this.sandbox.stub(appInsightsCore, "track"); + let throwSpy = this.sandbox.spy(appInsightsCore.logger, "throwInternal"); + Assert.equal(false, trackStub.called, "Track should not be called"); + Assert.equal(false, throwSpy.called, "We should not have thrown an internal error"); + + this._context.core = appInsightsCore; + this._context.trackStub = trackStub; + this._context.throwSpy = throwSpy; + + let xhr = new XMLHttpRequest(); + xhr.open("GET", "http://microsoft.com"); + xhr.setRequestHeader("Content-type", "application/json"); + xhr.send(); + // Emulate response + (xhr).respond(200, {"Content-Type": "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*"}, ""); + Assert.ok((xhr)[AJAX_DATA_CONTAINER], "should have xhr hooks"); + + + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this._context.core + let activeStatus = core.activeStatus && core.activeStatus(); + let trackStub = this._context.trackStub; + let throwSpy = this._context.throwSpy; + + if (activeStatus === ActiveStatus.ACTIVE) { + Assert.equal("testIkey", core.config.instrumentationKey, "ikey should be set"); + Assert.equal(1, trackStub.callCount, "Track should be called once"); + Assert.equal(false, throwSpy.called, "We should not have thrown an internal error test1"); + let data = trackStub.args[0][0].baseData; + Assert.equal(data.type, "Ajax", "request type should be ajax"); + Assert.ok(data.properties, "properties should be added"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + this.testCase({ name: "Dependencies Configuration: Make sure we don't fail for invalid arguments", test: () => { diff --git a/extensions/applicationinsights-dependencies-js/src/ajax.ts b/extensions/applicationinsights-dependencies-js/src/ajax.ts index 51ab8b33e..576e4709c 100644 --- a/extensions/applicationinsights-dependencies-js/src/ajax.ts +++ b/extensions/applicationinsights-dependencies-js/src/ajax.ts @@ -597,12 +597,14 @@ export class AjaxMonitor extends BaseTelemetryPlugin implements IDependenciesPlu _isUsingW3CHeaders = _distributedTracingMode === eDistributedTracingModes.AI_AND_W3C || _distributedTracingMode === eDistributedTracingModes.W3C; if (_enableAjaxPerfTracking) { - let iKey = config.instrumentationKey || "unkwn"; + let iKey = (config.instrumentationKey as string) || "unkwn"; + // TODO: handle ikey promise if (iKey.length > 5) { _markPrefix = AJAX_MONITOR_PREFIX + strSubstring(iKey, iKey.length - 5) + "."; } else { _markPrefix = AJAX_MONITOR_PREFIX + iKey + "."; } + } _disableAjaxTracking = !!_extensionConfig.disableAjaxTracking; diff --git a/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts b/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts index 2e1f35752..a5a37f460 100644 --- a/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts +++ b/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts @@ -2,8 +2,8 @@ import { AITestClass } from "@microsoft/ai-test-framework"; import * as pako from 'pako'; export class FileSizeCheckTest extends AITestClass { - private readonly MAX_BUNDLE_SIZE = 66; - private readonly MAX_DEFLATE_SIZE = 28; + private readonly MAX_BUNDLE_SIZE = 68; + private readonly MAX_DEFLATE_SIZE = 29; private readonly bundleFilePath = "../bundle/es5/ms.core.min.js"; public testInitialize() { diff --git a/shared/AppInsightsCommon/src/applicationinsights-common.ts b/shared/AppInsightsCommon/src/applicationinsights-common.ts index 293037127..32af7d137 100644 --- a/shared/AppInsightsCommon/src/applicationinsights-common.ts +++ b/shared/AppInsightsCommon/src/applicationinsights-common.ts @@ -7,6 +7,7 @@ export { } from "./Util"; export { ThrottleMgr } from "./ThrottleMgr"; export { parseConnectionString, ConnectionStringParser } from "./ConnectionStringParser"; +export { ConnectionString } from "./Interfaces/ConnectionString"; export { FieldType } from "./Enums"; export { IRequestHeaders, RequestHeaders, eRequestHeaders } from "./RequestResponseHeaders"; export { DisabledPropertyName, ProcessLegacy, SampleRate, HttpMethod, DEFAULT_BREEZE_ENDPOINT, DEFAULT_BREEZE_PATH, strNotSpecified } from "./Constants"; diff --git a/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts index ef459f698..1cdcf9337 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts @@ -2,8 +2,8 @@ import { Assert, AITestClass } from "@microsoft/ai-test-framework"; import * as pako from "pako"; export class AppInsightsCoreSizeCheck extends AITestClass { - private readonly MAX_RAW_SIZE = 64; - private readonly MAX_BUNDLE_SIZE = 64; + private readonly MAX_RAW_SIZE = 65; + private readonly MAX_BUNDLE_SIZE = 65; private readonly MAX_RAW_DEFLATE_SIZE = 27; private readonly MAX_BUNDLE_DEFLATE_SIZE = 27; private readonly rawFilePath = "../dist/es5/applicationinsights-core-js.min.js"; diff --git a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts index 14baee9d1..75b018548 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts @@ -1,21 +1,27 @@ -import { Assert, AITestClass } from "@microsoft/ai-test-framework"; -import { IConfiguration, ITelemetryPlugin, ITelemetryItem, IPlugin, IAppInsightsCore, normalizeJsName, random32, mwcRandomSeed, newId, randomValue, mwcRandom32, isNullOrUndefined, SenderPostManager, OnCompleteCallback, IPayloadData, _ISenderOnComplete, TransportType, _ISendPostMgrConfig, dumpObj } from "../../../src/applicationinsights-core-js" +import { Assert, AITestClass, PollingAssert } from "@microsoft/ai-test-framework"; +import { IConfiguration, ITelemetryPlugin, ITelemetryItem, IPlugin, IAppInsightsCore, normalizeJsName, random32, mwcRandomSeed, newId, randomValue, mwcRandom32, isNullOrUndefined, SenderPostManager, OnCompleteCallback, IPayloadData, _ISenderOnComplete, TransportType, _ISendPostMgrConfig, dumpObj, onConfigChange, createProcessTelemetryContext } from "../../../src/applicationinsights-core-js" import { AppInsightsCore } from "../../../src/JavaScriptSDK/AppInsightsCore"; import { IChannelControls } from "../../../src/JavaScriptSDK.Interfaces/IChannelControls"; import { _eInternalMessageId, LoggingSeverity } from "../../../src/JavaScriptSDK.Enums/LoggingEnums"; import { _InternalLogMessage, DiagnosticLogger } from "../../../src/JavaScriptSDK/DiagnosticLogger"; +import { ActiveStatus } from "../../../src/JavaScriptSDK.Enums/InitActiveStatusEnum"; +import { createAsyncPromise, createAsyncRejectedPromise, createAsyncResolvedPromise, createTimeoutPromise, doAwaitResponse } from "@nevware21/ts-async"; const AIInternalMessagePrefix = "AITR_"; const MaxInt32 = 0xFFFFFFFF; export class ApplicationInsightsCoreTests extends AITestClass { + private ctx: any; public testInitialize() { super.testInitialize(); + this.ctx = {}; } public testCleanup() { super.testCleanup(); + this.ctx = {}; + } public registerTests() { @@ -848,6 +854,814 @@ export class ApplicationInsightsCoreTests extends AITestClass { } }); + // init with ikey: null + this.testCase({ + name: "ApplicationInsightsCore Init: init with ikey null, will throw error message", + useFakeTimers: true, + test: () => { + let trackPlugin = new TrackPlugin(); + let channelPlugin = new ChannelPlugin(); + channelPlugin.priority = 1001; + let core = new AppInsightsCore(); + let channelSpy = this.sandbox.stub(channelPlugin, "processTelemetry"); + let activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.NONE, "default should be inactive status"); + + let config = { + instrumentationKey: undefined, + endpointUrl: "testUrl" + } as IConfiguration; + + let errorisCalled = false; + + try { + core.initialize( + config, + [trackPlugin, channelPlugin]); + + } catch (e) { + errorisCalled = true; + Assert.ok(JSON.stringify(e.message).indexOf("Please provide instrumentation key") > -1, "should send provide ikey error message"); + } + + Assert.ok(errorisCalled, "ikey error should be called"); + + + Assert.ok(!channelSpy.calledOnce, "channel should not be called once"); + Assert.equal(core.config.instrumentationKey, null, "channel testIkey should be null"); + Assert.equal(core.config.endpointUrl, "testUrl", "channel endpoint should not be changed"); + activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.INACTIVE, "default should be inactive status test1"); + + core.track({name: "test1"}); + Assert.ok(core.eventCnt() == 0, "Event should not be queued"); + + let isInit = core.isInitialized(); + Assert.ok(!isInit, "core is not initialized"); + + // Test re-init with valid ikey + config.instrumentationKey = "testIkey"; + + core.initialize( + config, + [trackPlugin, channelPlugin]); + Assert.ok(channelSpy.calledOnce, "channel should be called once"); + Assert.equal(core.config.instrumentationKey, "testIkey", "channel testIkey should be set"); + Assert.equal(core.config.endpointUrl, "testUrl", "channel endpoint should not be changed"); + activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.ACTIVE, "default should be active status again"); + isInit = core.isInitialized(); + Assert.ok(isInit, "core is initialized"); + + } + }); + + // init with ikey: string, endpoint null + this.testCase({ + name: "ApplicationInsightsCore Init: init with ikey string, endpoint null", + useFakeTimers: true, + test: () => { + let trackPlugin = new TrackPlugin(); + let channelPlugin = new ChannelPlugin(); + channelPlugin.priority = 1001; + let core = new AppInsightsCore(); + let channelSpy = this.sandbox.stub(channelPlugin, "processTelemetry"); + let activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.NONE, "default should be inactive status"); + + let config = { + instrumentationKey: "testIkey", + endpointUrl: undefined + } as IConfiguration; + + let errorisCalled = false; + + try { + core.initialize( + config, + [trackPlugin, channelPlugin]); + + } catch (e) { + errorisCalled = true; + } + + Assert.ok(!errorisCalled, "ikey error should not be called"); + + Assert.ok(channelSpy.calledOnce, "channel should be called once"); + Assert.ok(core.eventCnt() == 0, "Event should not be queued"); + let evt = channelSpy.args[0][0]; + Assert.equal(evt.iKey, "testIkey", "event ikey should be null"); + Assert.equal(core.config.instrumentationKey, "testIkey", "channel testIkey should not be changed"); + Assert.equal(core.config.endpointUrl, null, "channel endpoint should not be changed"); + activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.ACTIVE, "default should be active status test"); + + } + }); + + // init with ikey: string, endpointUrl: string + this.testCase({ + name: "ApplicationInsightsCore Init: init with ikey string, endpoint url string, dynamic changes with string", + useFakeTimers: true, + test: () => { + let trackPlugin = new TrackPlugin(); + let channelPlugin = new ChannelPlugin(); + channelPlugin.priority = 1001; + let core = new AppInsightsCore(); + let channelSpy = this.sandbox.stub(channelPlugin, "processTelemetry"); + let activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.NONE, "default should be pending status"); + + let config = { + instrumentationKey: "testIkey", + endpointUrl: "testUrl" + } as IConfiguration; + core.initialize( + config, + [trackPlugin, channelPlugin]); + + Assert.ok(channelSpy.calledOnce, "channel should be called once"); + Assert.ok(core.eventCnt() == 0, "Event should not be queued"); + let evt = channelSpy.args[0][0]; + Assert.equal(evt.iKey, "testIkey", "event ikey should be set"); + Assert.equal(core.config.instrumentationKey, "testIkey", "channel testIkey should be set"); + Assert.equal(core.config.endpointUrl, "testUrl", "channel endpoint should be set"); + activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.ACTIVE, "default should be active status"); + + + + core.config.instrumentationKey = "testIkey1"; + core.config.endpointUrl = "testUrl1"; + this.clock.tick(1); + core.track({name: "test1"}); + Assert.equal(channelSpy.callCount, 2, "channel should be called twice"); + Assert.ok(core.eventCnt() == 0, "Event should not be queued test1"); + evt = channelSpy.args[1][0]; + Assert.equal(evt.name, "test1", "event name should be set"); + Assert.equal(evt.iKey, "testIkey1", "event ikey should be set test1"); + activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.ACTIVE, "default should be active status test1"); + + + // change the ikey to null again, inactive + core.config.instrumentationKey = undefined; + this.clock.tick(1); + activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.INACTIVE, "default should be inactive status test1"); + + } + }); + + this.testCaseAsync({ + name: "ApplicationInsightsCore Init: init with ikey resolved promise, endpoint url resolved promise", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + let trackPlugin = new TrackPlugin(); + let channelPlugin = new ChannelPlugin(); + channelPlugin.priority = 1001; + let core = new AppInsightsCore(); + let channelSpy = this.sandbox.stub(channelPlugin, "processTelemetry"); + this.ctx.core = core; + this.ctx.channelSpy = channelSpy; + + let ikeyPromise = createAsyncResolvedPromise("testIkey"); + let urlPromise = createAsyncResolvedPromise("testUrl"); + + let config = { + instrumentationKey: ikeyPromise, + endpointUrl: urlPromise, + initTimeOut: 80000 + } as IConfiguration; + core.initialize( + config, + [trackPlugin, channelPlugin]); + + + Assert.ok(!channelSpy.calledOnce, "channel should not be called once"); + Assert.ok(core.eventCnt() == 1, "Event should be queued"); + let activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.PENDING, "active status should be set to pending"); + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this.ctx.core; + let activeStatus = core.activeStatus(); + let channelSpy = this.ctx.channelSpy + + if (activeStatus === ActiveStatus.ACTIVE) { + Assert.equal(core.config.instrumentationKey, "testIkey", "channel testIkey should not be changed"); + Assert.equal(core.config.endpointUrl, "testUrl", "channel endpoint should be changed"); + Assert.ok(channelSpy.calledOnce, "channel should be called once"); + Assert.ok(core.eventCnt() == 0, "Event should be released"); + let evt = channelSpy.args[0][0]; + Assert.equal(evt.iKey, "testIkey", "event ikey should be set"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "ApplicationInsightsCore Init: init with ikey resolved promise, endpoint url rejected promise", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + let trackPlugin = new TrackPlugin(); + let channelPlugin = new ChannelPlugin(); + channelPlugin.priority = 1001; + let core = new AppInsightsCore(); + let channelSpy = this.sandbox.stub(channelPlugin, "processTelemetry"); + this.ctx.core = core; + this.ctx.channelSpy = channelSpy; + + let ikeyPromise = createAsyncResolvedPromise("testIkey"); + let urlPromise = createAsyncRejectedPromise(new Error("endpoint error")); + + let config = { + instrumentationKey: ikeyPromise, + endpointUrl: urlPromise, + initTimeOut: 80000 + } as IConfiguration; + core.initialize( + config, + [trackPlugin, channelPlugin]); + + + Assert.ok(!channelSpy.calledOnce, "channel should not be called once"); + Assert.ok(core.eventCnt() == 1, "Event should be queued"); + let activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.PENDING, "active status should be set to pending"); + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this.ctx.core; + let activeStatus = core.activeStatus(); + let channelSpy = this.ctx.channelSpy + + if (activeStatus === ActiveStatus.ACTIVE) { + Assert.equal(core.config.instrumentationKey, "testIkey", "channel testIkey should not be changed"); + Assert.equal(core.config.endpointUrl, null, "channel endpoint should not be changed"); + Assert.ok(channelSpy.calledOnce, "channel should be called once"); + Assert.ok(core.eventCnt() == 0, "Event should be released"); + let evt = channelSpy.args[0][0]; + Assert.equal(evt.iKey, "testIkey", "event ikey should be set"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "ApplicationInsightsCore Init: init with ikey rejected promise, endpoint url rejected promise", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + let trackPlugin = new TrackPlugin(); + let channelPlugin = new ChannelPlugin(); + channelPlugin.priority = 1001; + let core = new AppInsightsCore(); + let channelSpy = this.sandbox.stub(channelPlugin, "processTelemetry"); + this.ctx.core = core; + this.ctx.channelSpy = channelSpy; + + let ikeyPromise = createAsyncRejectedPromise(new Error("ikey error")); + let urlPromise = createAsyncRejectedPromise(new Error("endpoint error")); + + this.ctx.ikeyPromise = ikeyPromise; + this.ctx.urlPromise = urlPromise; + + let config = { + instrumentationKey: ikeyPromise, + endpointUrl: urlPromise, + initTimeOut: 80000 + } as IConfiguration; + + core.initialize( + config, + [trackPlugin, channelPlugin]); + + Assert.ok(!channelSpy.calledOnce, "channel should not be called once"); + Assert.ok(core.eventCnt() == 1, "Event should be queued"); + let activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.PENDING, "active status should be set to pending"); + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this.ctx.core; + let activeStatus = core.activeStatus(); + let channelSpy = this.ctx.channelSpy + let ikeyPromise = this.ctx.ikeyPromise; + let urlPromise = this.ctx.urlPromise; + + if (activeStatus === ActiveStatus.INACTIVE) { + Assert.deepEqual(core.config.instrumentationKey, ikeyPromise, "channel testIkey should not be changed"); + Assert.deepEqual(core.config.endpointUrl, urlPromise, "channel endpoint should not be changed"); + Assert.ok(!channelSpy.calledOnce, "channel should not be called once"); + Assert.ok(core.eventCnt() == 0, "Event should be released"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "ApplicationInsightsCore Init: init with ikey promise chain, endpoint url promise chain", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + let trackPlugin = new TrackPlugin(); + let channelPlugin = new ChannelPlugin(); + channelPlugin.priority = 1001; + let core = new AppInsightsCore(); + let channelSpy = this.sandbox.stub(channelPlugin, "processTelemetry"); + this.ctx.core = core; + this.ctx.channelSpy = channelSpy; + + let csPromise = createAsyncResolvedPromise("instrumentationKey=testIkey;endpoint=testUrl"); + let ikeyPromise = createAsyncPromise((resolve, reject) => { + doAwaitResponse(csPromise, (res) => { + if (!res.rejected) { + resolve("testIkey"); + return; + } + reject(new Error("ikey error")); + }) + }); + let urlPromise = createAsyncPromise((resolve, reject) => { + doAwaitResponse(csPromise, (res) => { + if (!res.rejected) { + resolve("testUrl"); + return; + } + reject(new Error("url error")); + }) + }); + + let config = { + instrumentationKey: ikeyPromise, + endpointUrl: urlPromise, + initTimeOut: 80000 + } as IConfiguration; + core.initialize( + config, + [trackPlugin, channelPlugin]); + + + Assert.ok(!channelSpy.calledOnce, "channel should not be called once"); + Assert.ok(core.eventCnt() == 1, "Event should be queued"); + let activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.PENDING, "active status should be set to pending"); + + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this.ctx.core; + let activeStatus = core.activeStatus(); + let channelSpy = this.ctx.channelSpy + + if (activeStatus === ActiveStatus.ACTIVE) { + Assert.equal(core.config.instrumentationKey, "testIkey", "channel testIkey should not be changed"); + Assert.equal(core.config.endpointUrl, "testUrl", "channel endpoint should be changed"); + Assert.ok(channelSpy.calledOnce, "channel should be called once"); + Assert.ok(core.eventCnt() == 0, "Event should be released"); + let evt = channelSpy.args[0][0]; + Assert.equal(evt.iKey, "testIkey", "event ikey should be set"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "ApplicationInsightsCore Init: init with ikey mutiple layer promise chain, endpoint url mutiple layer promise chain", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + let trackPlugin = new TrackPlugin(); + let channelPlugin = new ChannelPlugin(); + channelPlugin.priority = 1001; + let core = new AppInsightsCore(); + let channelSpy = this.sandbox.stub(channelPlugin, "processTelemetry"); + this.ctx.core = core; + this.ctx.channelSpy = channelSpy; + + let csPromise = createAsyncResolvedPromise("instrumentationKey=testIkey;endpoint=testUrl"); + let ikeyPromise = createAsyncPromise((resolve, reject) => { + doAwaitResponse(csPromise, (res) => { + if (!res.rejected) { + resolve(createAsyncResolvedPromise("testIkey")); + return; + } + reject(createAsyncRejectedPromise(new Error("ikey error"))); + return; + }) + }); + let urlPromise = createAsyncPromise((resolve, reject) => { + doAwaitResponse(csPromise, (res) => { + if (!res.rejected) { + resolve(createAsyncResolvedPromise("testUrl")); + return; + } + reject(createAsyncRejectedPromise(new Error("url error"))); + return; + }) + }); + + let config = { + instrumentationKey: ikeyPromise, + endpointUrl: urlPromise, + initTimeOut: 80000 + } as IConfiguration; + core.initialize( + config, + [trackPlugin, channelPlugin]); + + + Assert.ok(!channelSpy.calledOnce, "channel should not be called once"); + Assert.ok(core.eventCnt() == 1, "Event should be queued"); + let activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.PENDING, "active status should be set to pending"); + + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this.ctx.core; + let activeStatus = core.activeStatus(); + let channelSpy = this.ctx.channelSpy + + if (activeStatus === ActiveStatus.ACTIVE) { + Assert.equal(core.config.instrumentationKey, "testIkey", "channel testIkey should not be changed"); + Assert.equal(core.config.endpointUrl, "testUrl", "channel endpoint should be changed"); + Assert.ok(channelSpy.calledOnce, "channel should be called once"); + Assert.ok(core.eventCnt() == 0, "Event should be released"); + let evt = channelSpy.args[0][0]; + Assert.equal(evt.iKey, "testIkey", "event ikey should be set"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + + + this.testCaseAsync({ + name: "ApplicationInsightsCore Init: init with ikey string, endpoint url string, dynamic changed with resolved promises", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + let trackPlugin = new TrackPlugin(); + let channelPlugin = new ChannelPlugin(); + channelPlugin.priority = 1001; + let core = new AppInsightsCore(); + let channelSpy = this.sandbox.stub(channelPlugin, "processTelemetry"); + + let config = { + instrumentationKey: "testIkey", + endpointUrl: "testUrl", + initTimeOut: 80000 + } as IConfiguration; + core.initialize( + config, + [trackPlugin, channelPlugin]); + this.ctx.core = core; + this.ctx.channelSpy = channelSpy; + + Assert.ok(channelSpy.calledOnce, "channel should not be called once"); + Assert.ok(core.eventCnt() == 0, "Event should not be queued"); + let activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.ACTIVE, "default should be active status"); + Assert.ok(channelSpy.calledOnce, "channel should be called once"); + Assert.ok(core.eventCnt() == 0, "Event should be released"); + let evt = channelSpy.args[0][0]; + Assert.equal(evt.iKey, "testIkey", "event ikey should be set"); + activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.ACTIVE, "default should be active status"); + + let ikeyPromise = createAsyncResolvedPromise("testIkey1"); + let urlPromise = createAsyncResolvedPromise("testUrl1"); + core.config.instrumentationKey = ikeyPromise; + core.config.endpointUrl = urlPromise; + this.clock.tick(1); + activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.PENDING, "active status should be set to pending"); + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this.ctx.core; + let activeStatus = core.activeStatus(); + let channelSpy = this.ctx.channelSpy + + if (activeStatus === ActiveStatus.ACTIVE) { + Assert.equal(core.config.instrumentationKey, "testIkey1", "channel testIkey should not be changed"); + Assert.equal(core.config.endpointUrl, "testUrl1", "channel endpoint should be changed"); + core.track({name: "test2"}); + Assert.ok(core.eventCnt() == 0, "Event should not be queued test1"); + let evt = channelSpy.args[1][0]; + Assert.equal(evt.name, "test2", "event name should be set test2"); + Assert.equal(evt.iKey, "testIkey1", "event ikey should be set test1"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "ApplicationInsightsCore Init: init with ikey resolved promise, endpoint url resolved promise, dynamic change with promise", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + let trackPlugin = new TrackPlugin(); + let channelPlugin = new ChannelPlugin(); + channelPlugin.priority = 1001; + let core = new AppInsightsCore(); + let channelSpy = this.sandbox.stub(channelPlugin, "processTelemetry"); + this.ctx.core = core; + this.ctx.channelSpy = channelSpy; + + let ikeyPromise = createAsyncResolvedPromise("testIkey"); + let urlPromise = createAsyncResolvedPromise("testUrl"); + + let config = { + instrumentationKey: ikeyPromise, + endpointUrl: urlPromise, + initTimeOut: 80000 + } as IConfiguration; + core.initialize( + config, + [trackPlugin, channelPlugin]); + + + Assert.ok(!channelSpy.calledOnce, "channel should not be called once"); + Assert.ok(core.eventCnt() == 1, "Event should be queued"); + let activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.PENDING, "active status should be set to pending"); + + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this.ctx.core; + let activeStatus = core.activeStatus(); + let channelSpy = this.ctx.channelSpy; + if (activeStatus === ActiveStatus.ACTIVE) { + Assert.equal(activeStatus, ActiveStatus.ACTIVE, "active status should be set to active"); + Assert.equal(core.config.instrumentationKey, "testIkey", "channel testIkey should not be changed"); + Assert.equal(core.config.endpointUrl, "testUrl", "channel endpoint should be changed"); + Assert.ok(channelSpy.calledOnce, "channel should be called once"); + Assert.ok(core.eventCnt() == 0, "Event should be released"); + let evt = channelSpy.args[0][0]; + Assert.equal(evt.iKey, "testIkey", "event ikey should be set"); + + let ikeyPromise = createAsyncResolvedPromise("testIkey1"); + let urlPromise = createAsyncResolvedPromise("testUrl1"); + core.config.instrumentationKey = ikeyPromise; + core.config.endpointUrl = urlPromise; + this.ctx.secondCall = true; + //Assert.equal(activeStatus, ActiveStatus.PENDING, "active status should be set to pending test1"); + return true; + } + return false; + }, "Wait for promise first response" + new Date().toISOString(), 60, 1000) as any).concat(PollingAssert.createPollingAssert(() => { + let core = this.ctx.core; + let activeStatus = core.activeStatus(); + let channelSpy = this.ctx.channelSpy; + + if (this.ctx.secondCall && activeStatus === ActiveStatus.ACTIVE) { + Assert.equal(activeStatus, ActiveStatus.ACTIVE, "active status should be set to active test1"); + Assert.equal(core.config.instrumentationKey, "testIkey1", "channel testIkey should not be changed test1"); + Assert.equal(core.config.endpointUrl, "testUrl1", "channel endpoint should be changed test1"); + Assert.ok(core.eventCnt() == 0, "Event should be released"); + core.track({name: "test1"}); + let evt = channelSpy.args[1][0]; + Assert.equal(evt.name, "test1", "event name should be set test2"); + Assert.equal(evt.iKey, "testIkey1", "event ikey should be set test1"); + + return true; + } + return false; + }, "Wait for promise second response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "ApplicationInsightsCore Init: init with ikey promise, endpoint url promise, dynamic changed with strings", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + let trackPlugin = new TrackPlugin(); + let channelPlugin = new ChannelPlugin(); + channelPlugin.priority = 1001; + let core = new AppInsightsCore(); + let channelSpy = this.sandbox.stub(channelPlugin, "processTelemetry"); + + let ikeyPromise = createAsyncResolvedPromise("testIkey"); + let urlPromise = createAsyncResolvedPromise("testUrl"); + + let unresolveIkeyPromise = createAsyncPromise((resolve, reject) => { + //do nothing, + }) // init with it, it should be pending + + let newIkeyPromise = createAsyncPromise((resolve, reject) => { + resolve("ikey") + }) // init with it, pending, no changes or string + // resolve first one, active + + let config = { + instrumentationKey: ikeyPromise, + endpointUrl: urlPromise, + initTimeOut: 80000 + } as IConfiguration; + core.initialize( + config, + [trackPlugin, channelPlugin]); + this.ctx.core = core; + this.ctx.channelSpy = channelSpy; + + Assert.ok(!channelSpy.calledOnce, "channel should not be called once"); + Assert.ok(core.eventCnt() == 1, "Event should be queued"); + let activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.PENDING, "default should be pending status"); + + // status is pending, following changes should not be applied + core.config.instrumentationKey = "testIkey1"; + core.config.endpointUrl = "testUrl1"; + + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this.ctx.core; + let activeStatus = core.activeStatus(); + let channelSpy = this.ctx.channelSpy + + if (activeStatus === ActiveStatus.ACTIVE) { + Assert.equal(core.config.instrumentationKey, "testIkey", "channel testIkey should not be changed"); + Assert.equal(core.config.endpointUrl, "testUrl", "channel endpoint should be changed"); + Assert.ok(core.eventCnt() == 0, "Event should not be queued test1"); + let evt1 = channelSpy.args[0][0]; + Assert.equal(evt1.iKey, "testIkey", "event ikey should be set test1"); + this.clock.tick(1); + core.track({name: "test2"}); + let evt2 = channelSpy.args[1][0]; + Assert.equal(evt2.name, "test2", "event name should be set test2"); + Assert.equal(evt2.iKey, "testIkey", "event ikey should be set test1"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + + this.testCaseAsync({ + name: "ApplicationInsightsCore Init: init with ikey promise, endpoint url promise, dynamic changed with strings while waiting promises", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + let trackPlugin = new TrackPlugin(); + let channelPlugin = new ChannelPlugin(); + channelPlugin.priority = 1001; + let core = new AppInsightsCore(); + let channelSpy = this.sandbox.stub(channelPlugin, "processTelemetry"); + + let urlPromise = createAsyncResolvedPromise("testUrl"); + + let resolveFunc; + + let ikeyPromise = createAsyncPromise((resolve, reject) => { + resolveFunc = resolve; + //do nothing, mock unresolve + }); + + let config = { + instrumentationKey: ikeyPromise, + endpointUrl: urlPromise, + initTimeOut: 80000 + } as IConfiguration; + core.initialize( + config, + [trackPlugin, channelPlugin]); + this.ctx.core = core; + this.ctx.channelSpy = channelSpy; + + Assert.ok(!channelSpy.calledOnce, "channel should not be called once"); + Assert.ok(core.eventCnt() == 1, "Event should be queued"); + let activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.PENDING, "default should be pending status"); + + // status is pending, following changes should not be applied + core.config.instrumentationKey = "testIkey1"; + this.clock.tick(1); + Assert.ok(!channelSpy.calledOnce, "channel should not be called once"); + Assert.ok(core.eventCnt() == 1, "Event should be queued"); + activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.PENDING, "default should be pending status"); + + resolveFunc("testIkey"); + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this.ctx.core; + let activeStatus = core.activeStatus(); + let channelSpy = this.ctx.channelSpy + + if (activeStatus === ActiveStatus.ACTIVE) { + Assert.equal(core.config.instrumentationKey, "testIkey", "channel testIkey should not be changed"); + Assert.equal(core.config.endpointUrl, "testUrl", "channel endpoint should be changed"); + Assert.ok(core.eventCnt() == 0, "Event should not be queued test1"); + let evt1 = channelSpy.args[0][0]; + Assert.equal(evt1.iKey, "testIkey", "event ikey should be set test1"); + this.clock.tick(1); + core.track({name: "test2"}); + let evt2 = channelSpy.args[1][0]; + Assert.equal(evt2.name, "test2", "event name should be set test2"); + Assert.equal(evt2.iKey, "testIkey", "event ikey should be set test1"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "ApplicationInsightsCore Init: init with ikey and endpoint timeout promises", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + let trackPlugin = new TrackPlugin(); + let channelPlugin = new ChannelPlugin(); + channelPlugin.priority = 1001; + let core = new AppInsightsCore(); + let channelSpy = this.sandbox.stub(channelPlugin, "processTelemetry"); + this.ctx.core = core; + this.ctx.channelSpy = channelSpy; + + let ikeyPromise = createTimeoutPromise(60, true,"testIkey"); + let urlPromise = createTimeoutPromise(60, true, "testUrl"); + + let config = { + instrumentationKey: ikeyPromise, + endpointUrl: urlPromise, + initTimeOut: 1 + } as IConfiguration; + core.initialize( + config, + [trackPlugin, channelPlugin]); + + + Assert.ok(!channelSpy.calledOnce, "channel should not be called once"); + Assert.ok(core.eventCnt() == 1, "Event should be queued"); + let activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.PENDING, "active status should be set to pending"); + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this.ctx.core; + let activeStatus = core.activeStatus(); + let channelSpy = this.ctx.channelSpy; + + if (activeStatus === ActiveStatus.INACTIVE) { + Assert.ok(!channelSpy.calledOnce, "channel should not be called once"); + Assert.ok(core.eventCnt() == 0, "Event should be released"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "ApplicationInsightsCore Init: init with ikey timeout promises and endpoint promises", + stepDelay: 100, + useFakeTimers: true, + steps: [() => { + let channelPlugin = new ChannelPlugin(); + channelPlugin.priority = 1001; + let core = new AppInsightsCore(); + let channelSpy = this.sandbox.stub(channelPlugin, "processTelemetry"); + + let ikeyPromise = createTimeoutPromise(20, true, "testIkey1"); + let urlPromise = createTimeoutPromise(1, true, "testUrl1"); + + let config = { + instrumentationKey: ikeyPromise, + endpointUrl: urlPromise, + initTimeOut: 6 + } as IConfiguration; + core.initialize( + config, + [channelPlugin]); + this.ctx.core = core; + this.ctx.channelSpy = channelSpy; + + let activeStatus = core.activeStatus(); + Assert.equal(activeStatus, ActiveStatus.PENDING, "active status should be set to pending"); + Assert.ok(!channelSpy.calledOnce, "channel should not be called"); + core.track({name: "testEvent"}); + + + }].concat(PollingAssert.createPollingAssert(() => { + let core = this.ctx.core; + let activeStatus = core.activeStatus(); + let channelSpy = this.ctx.channelSpy + + if (activeStatus === ActiveStatus.INACTIVE) { + Assert.ok(core.eventCnt() == 0, "Event should be released"); + Assert.ok(!channelSpy.called, "channel should not be called"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + + this.testCase({ name: 'newId tests length', @@ -1186,9 +2000,11 @@ class ChannelPlugin implements IChannelControls { } public initialize = (config: IConfiguration) => { + } public _processTelemetry(env: ITelemetryItem) { + console.log(JSON.stringify(env)) } } @@ -1295,7 +2111,6 @@ class TestOfflineChannelPlugin implements IChannelControls { this.events.push(env); // Just calling processTelemetry as this is the original design of the Plugins (as opposed to the newer processNext()) - this._nextPlugin?.processTelemetry(env); } } diff --git a/shared/AppInsightsCore/src/JavaScriptSDK.Enums/InitActiveStatusEnum.ts b/shared/AppInsightsCore/src/JavaScriptSDK.Enums/InitActiveStatusEnum.ts new file mode 100644 index 000000000..856fdd83f --- /dev/null +++ b/shared/AppInsightsCore/src/JavaScriptSDK.Enums/InitActiveStatusEnum.ts @@ -0,0 +1,30 @@ +import { createEnumStyle } from "./EnumHelperFuncs"; + +export const enum eActiveStatus { + + // None + NONE = 0, + /** + * inactive status means there might be rejected ikey/endpoint promises or ikey/endpoint resolved is not valid + */ + INACTIVE = 1, + + /** + * active mean ikey/endpoint promises is resolved and initializing with ikey/endpoint is successful + */ + ACTIVE = 2, + + /** + * Waiting for promises to be resolved + * NOTE: if status is set to be pending, incoming changes will be dropped until pending status is removed + */ + PENDING = 3, +} + +export const ActiveStatus = createEnumStyle({ + NONE: eActiveStatus.NONE, + PENDING: eActiveStatus.PENDING, + INACTIVE: eActiveStatus.INACTIVE, + ACTIVE: eActiveStatus.ACTIVE +}); +export type ActiveStatus = number | eActiveStatus; \ No newline at end of file diff --git a/shared/AppInsightsCore/src/JavaScriptSDK.Enums/LoggingEnums.ts b/shared/AppInsightsCore/src/JavaScriptSDK.Enums/LoggingEnums.ts index abe98b4c0..257afa082 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK.Enums/LoggingEnums.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK.Enums/LoggingEnums.ts @@ -126,7 +126,8 @@ export const enum _eInternalMessageId { DynamicConfigException = 108, DefaultThrottleMsgKey = 109, CdnDeprecation = 110, - SdkLdrUpdate = 111 + SdkLdrUpdate = 111, + InitPromiseException = 112 } export type _InternalMessageId = number | _eInternalMessageId; diff --git a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IAppInsightsCore.ts b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IAppInsightsCore.ts index c90b3e9d4..1fd19e0ee 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IAppInsightsCore.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IAppInsightsCore.ts @@ -4,6 +4,7 @@ import { IPromise } from "@nevware21/ts-async"; import { ITimerHandler } from "@nevware21/ts-utils"; import { WatcherFunction } from "../Config/IDynamicWatcher"; +import { eActiveStatus } from "../JavaScriptSDK.Enums/InitActiveStatusEnum"; import { SendRequestReason } from "../JavaScriptSDK.Enums/SendRequestReason"; import { UnloadHandler } from "../JavaScriptSDK/UnloadHandlerContainer"; import { IChannelControls } from "./IChannelControls"; @@ -225,4 +226,22 @@ export interface IAppInsightsCore number; + + /** + * Watches and tracks status of initialization process + * @returns ActiveStatus + * @since 3.3.0 + * If returned status is active, it means initialization process is completed. + * If returned status is pending, it means the initialization process is waiting for promieses to be resolved. + * If returned status is inactive, it means ikey is invalid or can 't get ikey or enpoint url from promsises. + */ + activeStatus?: () => eActiveStatus | number; + + /** + * Set Active Status to pending, which will block the incoming changes until internal promises are resolved + * @internal Internal use + * @since 3.3.0 + */ + _setPendingStatus?: () => void; + } diff --git a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts index 3ff6ebc8e..e6eb408a3 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { IPromise } from "@nevware21/ts-async"; import { IAppInsightsCore } from "./IAppInsightsCore"; import { IChannelControls } from "./IChannelControls"; import { ICookieMgrConfig } from "./ICookieMgr"; @@ -17,12 +18,12 @@ export interface IConfiguration { /** * Instrumentation key of resource. Either this or connectionString must be specified. */ - instrumentationKey?: string; + instrumentationKey?: string| IPromise; /** * Connection string of resource. Either this or instrumentationKey must be specified. */ - connectionString?: string; + connectionString?: string | IPromise ; /** * Set the timer interval (in ms) for internal logging queue, this is the @@ -72,7 +73,7 @@ export interface IConfiguration { /** * Endpoint where telemetry data is sent */ - endpointUrl?: string; + endpointUrl?: string | IPromise; /** * Extension configs loaded in SDK @@ -190,4 +191,21 @@ export interface IConfiguration { * @defaultValue undefined */ featureOptIn?: IFeatureOptIn; + + /** + * If your connection string, instrumentation key and endpoint url are promises, + * this config is to manually set timeout for those promises. + * Default: 50000ms + * @since 3.3.0 + */ + initTimeOut?: number; + + /** + * If your connection string, instrumentation key and endpoint url are promises, + * this config is to manually set in memory proxy track calls count limit before promises finished. + * Default: 100 + * @since 3.3.0 + */ + initInMemoMaxSize?: number; + } diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/AppInsightsCore.ts b/shared/AppInsightsCore/src/JavaScriptSDK/AppInsightsCore.ts index 0991472e8..b8686f885 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/AppInsightsCore.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/AppInsightsCore.ts @@ -3,16 +3,17 @@ "use strict"; import dynamicProto from "@microsoft/dynamicproto-js"; -import { IPromise, createPromise } from "@nevware21/ts-async"; +import { IPromise, createAllSettledPromise, createPromise, doAwaitResponse } from "@nevware21/ts-async"; import { ITimerHandler, arrAppend, arrForEach, arrIndexOf, createTimeout, deepExtend, hasDocument, isFunction, isNullOrUndefined, isPlainObject, - objDeepFreeze, objDefine, objForEachKey, objFreeze, objHasOwn, scheduleTimeout, throwError + isPromiseLike, objDeepFreeze, objDefine, objForEachKey, objFreeze, objHasOwn, scheduleTimeout, throwError } from "@nevware21/ts-utils"; import { createDynamicConfig, onConfigChange } from "../Config/DynamicConfig"; import { IConfigDefaults } from "../Config/IConfigDefaults"; import { IDynamicConfigHandler, _IInternalDynamicConfigHandler } from "../Config/IDynamicConfigHandler"; import { IWatchDetails, WatcherFunction } from "../Config/IDynamicWatcher"; import { eEventsDiscardedReason } from "../JavaScriptSDK.Enums/EventsDiscardedReason"; +import { ActiveStatus, eActiveStatus } from "../JavaScriptSDK.Enums/InitActiveStatusEnum"; import { _eInternalMessageId, eLoggingSeverity } from "../JavaScriptSDK.Enums/LoggingEnums"; import { SendRequestReason } from "../JavaScriptSDK.Enums/SendRequestReason"; import { TelemetryUnloadReason } from "../JavaScriptSDK.Enums/TelemetryUnloadReason"; @@ -41,7 +42,7 @@ import { createCookieMgr } from "./CookieMgr"; import { createUniqueNamespace } from "./DataCacheHelper"; import { getDebugListener } from "./DbgExtensionUtils"; import { DiagnosticLogger, _InternalLogMessage, _throwInternal, _warnToConsole } from "./DiagnosticLogger"; -import { getSetValue, proxyFunctionAs, proxyFunctions, toISOString } from "./HelperFuncs"; +import { getSetValue, isNotNullOrUndefined, proxyFunctionAs, proxyFunctions, toISOString } from "./HelperFuncs"; import { STR_CHANNELS, STR_CREATE_PERF_MGR, STR_DISABLED, STR_EMPTY, STR_EXTENSIONS, STR_EXTENSION_CONFIG, UNDEFINED_VALUE } from "./InternalConstants"; @@ -59,6 +60,8 @@ const strValidationError = "Plugins must provide initialize method"; const strNotificationManager = "_notificationManager"; const strSdkUnloadingError = "SDK is still unloading..."; const strSdkNotInitialized = "SDK is not initialized"; +const maxInitQueueSize = 100; +const maxInitTimeout = 50000; // const strPluginUnloadFailed = "Failed to unload plugin"; /** @@ -292,6 +295,11 @@ export class AppInsightsCore im let _extensions: IPlugin[]; let _pluginVersionStringArr: string[]; let _pluginVersionString: string; + let _activeStatus: eActiveStatus; // to indicate if ikey or endpoint url promised is resolved or not + let _endpoint: string; + let _initInMemoMaxSize: number; // max event count limit during wait for init promises to be resolved + let _isStatusSet: boolean; // track if active status is set in case of init timeout and init promises setting the status twice + let _initTimer: ITimerHandler; /** * Internal log poller @@ -307,11 +315,20 @@ export class AppInsightsCore im // Special internal method to allow the unit tests and DebugPlugin to hook embedded objects _self["_getDbgPlgTargets"] = () => { - return [_extensions]; + return [_extensions, _eventQueue]; }; _self.isInitialized = () => _isInitialized; + // since version 3.3.0 + _self.activeStatus = () => _activeStatus; + + // since version 3.3.0 + // internal + _self._setPendingStatus = () => { + _activeStatus = eActiveStatus.PENDING; + }; + // Creating the self.initialize = () _self.initialize = (config: CfgType, extensions: IPlugin[], logger?: IDiagnosticLogger, notificationManager?: INotificationManager): void => { if (_isUnloading) { @@ -330,8 +347,122 @@ export class AppInsightsCore im // This will be "re-run" if the referenced config properties are changed _addUnloadHook(_configHandler.watch((details) => { - _instrumentationKey = details.cfg.instrumentationKey; + let rootCfg = details.cfg; + + let isPending = _activeStatus === eActiveStatus.PENDING; + + if (isPending){ + // means waiting for previous promises to be resolved, won't apply new changes + return; + } + + _initInMemoMaxSize = rootCfg.initInMemoMaxSize || maxInitQueueSize; + // app Insights core only handle ikey and endpointurl, aisku will handle cs + let ikey = rootCfg.instrumentationKey; + let endpointUrl = rootCfg.endpointUrl; // do not need to validate endpoint url, if it is null, default one will be set by sender + + if (isNullOrUndefined(ikey)) { + _instrumentationKey = null; + // if new ikey is null, set status to be inactive, all new events will be saved in memory or dropped + _activeStatus = ActiveStatus.INACTIVE; + let msg = "Please provide instrumentation key"; + + if (!_isInitialized) { + // only throw error during initialization + throwError(msg); + } else { + _throwInternal(_logger, eLoggingSeverity.CRITICAL, _eInternalMessageId.InvalidInstrumentationKey, msg); + _releaseQueues(); + } + return; + + } + + let promises: IPromise[] = []; + if (isPromiseLike(ikey)) { + promises.push(ikey); + _instrumentationKey = null; // reset current local ikey variable (otherwise it will always be the previous ikeys if timeout is called before promise cb) + } else { + // string + _instrumentationKey = ikey; + } + + if (isPromiseLike(endpointUrl)) { + promises.push(endpointUrl); + _endpoint = null; // reset current local endpoint variable (otherwise it will always be the previous urls if timeout is called before promise cb) + } else { + // string or null + _endpoint = endpointUrl; + } + + // at least have one promise + if (promises.length) { + // reset to false for new dynamic changes + _isStatusSet = false; + _activeStatus = eActiveStatus.PENDING; + let initTimeout = isNotNullOrUndefined(rootCfg.initTimeOut)? rootCfg.initTimeOut : maxInitTimeout; // rootCfg.initTimeOut could be 0 + let allPromises = createAllSettledPromise(promises); + _initTimer = scheduleTimeout(() => { + // set _isStatusSet to true + // set active status + // release queues + _initTimer = null; + if (!_isStatusSet) { + _setStatus(); + } + + }, initTimeout); + + doAwaitResponse(allPromises, (response) => { + try { + if (_isStatusSet) { + // promises take too long to resolve, ignore them + // active status should be set by timeout already + return; + } + + if (!response.rejected) { + let values = response.value; + if (values && values.length) { + // ikey + let ikeyRes = values[0]; + _instrumentationKey = ikeyRes && ikeyRes.value; + + // endpoint + if (values.length > 1) { + let endpointRes = values[1]; + _endpoint = endpointRes && endpointRes.value; + + } + + } + if (_instrumentationKey) { + // if ikey is null, no need to trigger extra dynamic changes for extensions + config.instrumentationKey = _instrumentationKey; // set config.instrumentationKey for extensions to consume + config.endpointUrl = _endpoint; // set config.endpointUrl for extensions to consume + } + + } + + // set _isStatusSet to true + // set active status + // release queues + _setStatus(); + + } catch (e) { + if (!_isStatusSet){ + _setStatus(); + } + } + + }); + } else { + // means no promises + _setStatus(); + + } + //_instrumentationKey = details.cfg.instrumentationKey; // Mark the extensionConfig and all first level keys as referenced // This is so that calls to getExtCfg() will always return the same object // Even when a user may "re-assign" the plugin properties (or it's unloaded/reloaded) @@ -339,10 +470,8 @@ export class AppInsightsCore im objForEachKey(extCfg, (key) => { details.ref(extCfg, key); }); - - if (isNullOrUndefined(_instrumentationKey)) { - throwError("Please provide instrumentation key"); - } + + })); _notificationManager = notificationManager; @@ -377,9 +506,10 @@ export class AppInsightsCore im _cfgListeners = null; _isInitialized = true; - _self.releaseQueue(); - - _self.pollInternalLogs(); + if (_activeStatus === ActiveStatus.ACTIVE) { + _releaseQueues(); + } + }; _self.getChannels = (): IChannelControls[] => { @@ -416,12 +546,16 @@ export class AppInsightsCore im // Common Schema 4.0 telemetryItem.ver = telemetryItem.ver || "4.0"; - if (!_isUnloading && _self.isInitialized()) { + if (!_isUnloading && _self.isInitialized() && _activeStatus === ActiveStatus.ACTIVE) { // Process the telemetry plugin chain _createTelCtx().processNext(telemetryItem); - } else { + } else if (_activeStatus !== ActiveStatus.INACTIVE){ // Queue events until all extensions are initialized - _eventQueue.push(telemetryItem); + if (_eventQueue.length <= _initInMemoMaxSize) { + // set limit, if full, stop adding new events + _eventQueue.push(telemetryItem); + } + } }, () => ({ item: telemetryItem }), !((telemetryItem as any).sync)); }; @@ -490,10 +624,18 @@ export class AppInsightsCore im if (_isInitialized && _eventQueue.length > 0) { let eventQueue = _eventQueue; _eventQueue = []; + if (_activeStatus === eActiveStatus.ACTIVE) { + arrForEach(eventQueue, (event: ITelemetryItem) => { + event.iKey = event.iKey || _instrumentationKey; + _createTelCtx().processNext(event); + }); - arrForEach(eventQueue, (event: ITelemetryItem) => { - _createTelCtx().processNext(event); - }); + } else { + // new one for msg ikey + _throwInternal(_logger, eLoggingSeverity.WARNING, _eInternalMessageId.FailedToSendQueuedTelemetry, "core init status is not active"); + } + + } }; @@ -505,6 +647,25 @@ export class AppInsightsCore im return _startLogPoller(true); }; + function _setStatus() { + _isStatusSet = true; + if (isNullOrUndefined(_instrumentationKey)) { + _activeStatus = ActiveStatus.INACTIVE; + _throwInternal(_logger, eLoggingSeverity.CRITICAL, _eInternalMessageId.InitPromiseException, "ikey can't be resolved from promises"); + } else { + _activeStatus = ActiveStatus.ACTIVE; + } + _releaseQueues(); + + } + + function _releaseQueues() { + if (_isInitialized) { + _self.releaseQueue(); + _self.pollInternalLogs(); + } + } + function _startLogPoller(alwaysStart?: boolean): ITimerHandler { if ((!_internalLogPoller || !_internalLogPoller.enabled) && !_forceStopInternalLogPoller) { let shouldStart = alwaysStart || (_logger && _logger.queue.length > 0); @@ -872,6 +1033,11 @@ export class AppInsightsCore im _forceStopInternalLogPoller = false; _internalLogPoller = null; _internalLogPollerListening = false; + _activeStatus = eActiveStatus.NONE; // default is None + _endpoint = null; + _initInMemoMaxSize = null; + _isStatusSet = false; + _initTimer = null; } function _createTelCtx(): IProcessTelemetryContext { @@ -1403,6 +1569,30 @@ export class AppInsightsCore im return null; } + /** + * Watches and tracks status of initialization process + * @returns ActiveStatus + * @since 3.3.0 + * If returned status is active, it means initialization process is completed. + * If returned status is pending, it means the initialization process is waiting for promieses to be resolved. + * If returned status is inactive, it means ikey is invalid or can 't get ikey or enpoint url from promsises. + */ + public activeStatus(): eActiveStatus | number { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + return null; + } + + /** + * Set Active Status to pending, which will block the incoming changes until internal promises are resolved + * @internal Internal use + * @since 3.3.0 + */ + public _setPendingStatus(): void { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + return null; + } + + protected releaseQueue() { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } diff --git a/shared/AppInsightsCore/src/applicationinsights-core-js.ts b/shared/AppInsightsCore/src/applicationinsights-core-js.ts index 2d02f8d1b..9eaf98bde 100644 --- a/shared/AppInsightsCore/src/applicationinsights-core-js.ts +++ b/shared/AppInsightsCore/src/applicationinsights-core-js.ts @@ -18,6 +18,7 @@ export { eEventsDiscardedReason, EventsDiscardedReason, eBatchDiscardedReason, B export { SendRequestReason, TransportType } from "./JavaScriptSDK.Enums/SendRequestReason"; export { TelemetryUpdateReason } from "./JavaScriptSDK.Enums/TelemetryUpdateReason"; export { TelemetryUnloadReason } from "./JavaScriptSDK.Enums/TelemetryUnloadReason"; +export { eActiveStatus, ActiveStatus } from "./JavaScriptSDK.Enums/InitActiveStatusEnum" export { throwAggregationError } from "./JavaScriptSDK/AggregationError"; export { AppInsightsCore } from "./JavaScriptSDK/AppInsightsCore"; export { BaseTelemetryPlugin } from "./JavaScriptSDK/BaseTelemetryPlugin"; diff --git a/tools/applicationinsights-web-snippet/src/snippet.ts b/tools/applicationinsights-web-snippet/src/snippet.ts index d22c7325c..9e3b83cdd 100644 --- a/tools/applicationinsights-web-snippet/src/snippet.ts +++ b/tools/applicationinsights-web-snippet/src/snippet.ts @@ -56,7 +56,7 @@ declare var cfg:ISnippetConfig; function _parseConnectionString() { let fields:Fields = {}; let connectionString = aiConfig.connectionString; - if (connectionString) { + if (typeof connectionString === "string" && connectionString) { let kvPairs = connectionString.split(";"); for (let lp = 0; lp < kvPairs.length; lp++) { let kvParts = kvPairs[lp].split("=");