diff --git a/AISKU/Tests/Unit/src/CdnThrottle.tests.ts b/AISKU/Tests/Unit/src/CdnThrottle.tests.ts new file mode 100644 index 000000000..600a08b36 --- /dev/null +++ b/AISKU/Tests/Unit/src/CdnThrottle.tests.ts @@ -0,0 +1,393 @@ +import { ApplicationInsights, ApplicationInsightsContainer, IApplicationInsights, IConfig, IConfiguration, LoggingSeverity, Snippet, _eInternalMessageId } from '../../../src/applicationinsights-web' +import { AITestClass, Assert, IFetchArgs, PollingAssert} from '@microsoft/ai-test-framework'; +import { IThrottleInterval, IThrottleLimit, IThrottleMgrConfig } from '@microsoft/applicationinsights-common'; +import { SinonSpy } from 'sinon'; +import { AppInsightsSku } from '../../../src/AISku'; +import { createSnippetV5 } from './testSnippetV5'; +import { CdnFeatureMode, FeatureOptInMode, getGlobal, getGlobalInst, isFunction, newId } from '@microsoft/applicationinsights-core-js'; +import { createSnippetV6 } from './testSnippetV6'; +import { CfgSyncPlugin, ICfgSyncConfig, ICfgSyncMode } from '@microsoft/applicationinsights-cfgsync-js'; +import { createSyncPromise } from '@nevware21/ts-async'; +import { ICfgSyncCdnConfig } from '@microsoft/applicationinsights-cfgsync-js/src/Interfaces/ICfgSyncCdnConfig'; + +const TestInstrumentationKey = 'b7170927-2d1c-44f1-acec-59f4e1751c11'; + +const default_throttle_config = { + disabled: true, + limit: { + samplingRate: 100, + maxSendNumber: 1 + }, + interval: { + monthInterval: 3, + daysOfMonth: [28] + } +} as IThrottleMgrConfig; + +const throttleCfg = { + 109: { + disabled: false, + limit: { + samplingRate: 1000000, + maxSendNumber: 2 + }, + interval: { + monthInterval: 2, + daysOfMonth:[1] + } + }, + 106: { + disabled: false, + limit: { + samplingRate: 1000000, + maxSendNumber: 2 + }, + interval: { + monthInterval: 2, + daysOfMonth:[1] + } + } +} + +const throttleCfgDisable = { + 109: { + disabled: true, + limit: { + samplingRate: 1000000, + maxSendNumber: 4 + }, + interval: { + monthInterval: 4, + daysOfMonth:[1] + } + }, + 106: { + disabled: true, + limit: { + samplingRate: 1000000, + maxSendNumber: 4 + }, + interval: { + monthInterval: 4, + daysOfMonth:[1] + } + } +} + +const sampleConfig = { + instrumentationKey:"testIkey", + enableAjaxPerfTracking: true, + throttleMgrCfg: throttleCfg +} as IConfiguration & IConfig; + + +export class CdnThrottle extends AITestClass { + private _ai: AppInsightsSku; + private getAi: ApplicationInsights; + private _config: IConfiguration | IConfig; + private identifier: string; + private fetchStub: any; + init: ApplicationInsights; + private res: any; + private _fetch: any; + loggingSpy: any; + + constructor() { + super("CdnThrottle"); + } + + public _getTestConfig() { + let config: IConfiguration | IConfig = { + instrumentationKey: TestInstrumentationKey, + featureOptIn : {["iKeyUsage"]: {mode: FeatureOptInMode.enable}}, + extensionConfig : {["AppInsightsCfgSyncPlugin"] : { + syncMode: ICfgSyncMode.Receive, + cfgUrl: "testurl" + }} + }; + return config; + } + + public testInitialize() { + try { + if (window.localStorage){ + window.localStorage.clear(); + } + this.identifier = "AppInsightsCfgSyncPlugin"; + this._config = this._getTestConfig(); + this._fetch = getGlobalInst("fetch"); + let doc = getGlobal(); + let cdnCfg = { + enabled: true, + config: sampleConfig + } as ICfgSyncConfig; + let cdnFeatureOptInCfg = { + enabled: true, + featureOptIn:{ + ["enableWParamFeature"]: {mode: CdnFeatureMode.enable, onCfg: {["maxMessageLimit"]: 11}, offCfg: {["maxMessageLimit"]: 12}}, + ["iKeyUsage"]: { + mode: CdnFeatureMode.enable, + onCfg: { + "throttleMgrCfg.106.disabled":false, + "throttleMgrCfg.109.disabled":false, + }, + offCfg: { + "throttleMgrCfg.106.disabled":true, + "throttleMgrCfg.109.disabled":true, + } + }}, + config: { + maxMessageLimit: 10, + throttleMgrCfg: throttleCfgDisable + } + } as ICfgSyncConfig; + doc["res"] = new (doc as any).Response(JSON.stringify(cdnCfg), { + status: 200, + headers: { "Content-type": "application/json" } + }); + doc["res2"] = new (doc as any).Response(JSON.stringify(cdnFeatureOptInCfg), { + status: 200, + headers: { "Content-type": "application/json" } + }); + } catch (e) { + console.error('Failed to initialize', e.message); + } + } + + public testFinishedCleanup(): void { + if (this._ai) { + this._ai.unload(false); + } + this.fetchStub = null; + getGlobal().fetch = this._fetch; + if (window.localStorage){ + window.localStorage.clear(); + } + } + + public registerTests() { + this.testCaseAsync({ + name: "CfgSyncPlugin: customer enable ikey messsage change, new config fetch from config url overwrite throttle setting and send message", + stepDelay: 10, + useFakeTimers: true, + steps: [ () => { + let doc = getGlobal(); + hookFetch((resolve) => { // global instance cannot access test private instance + AITestClass.orgSetTimeout(function() { + resolve( doc["res"]); + }, 0); + }); + this.fetchStub = this.sandbox.spy((doc as any), "fetch"); + this.init = new ApplicationInsights({ + config: { + instrumentationKey: TestInstrumentationKey, + extensionConfig : {["AppInsightsCfgSyncPlugin"] : { + syncMode: ICfgSyncMode.Receive, + cfgUrl: "testurl" + }} + } + }); + this.init.loadAppInsights(); + this._ai = this.init; + }].concat(PollingAssert.createPollingAssert(() => { + + if (this.fetchStub.called){ + let core = this._ai['core']; + let _logger = core.logger; + let loggingSpy = this.sandbox.stub(_logger, 'throwInternal'); + Assert.ok(!loggingSpy.called); + + // now enable feature + this.init.config.featureOptIn = {["iKeyUsage"]: {mode: FeatureOptInMode.enable}}; + this.clock.tick(1); + Assert.ok(loggingSpy.called); + Assert.equal(_eInternalMessageId.InstrumentationKeyDeprecation, loggingSpy.args[0][1]); + let message= loggingSpy.args[0][2]; + Assert.ok(message.includes("Instrumentation key")); + return true; + } + return false; + + }, "response received", 60, 1000) as any) + }); + this.testCaseAsync({ + name: "CfgSyncPlugin: customer didn't set throttle config, successfully fetch from config url", + stepDelay: 10, + useFakeTimers: true, + steps: [ () => { + let doc = getGlobal(); + hookFetch((resolve) => { // global instance cannot access test private instance + AITestClass.orgSetTimeout(function() { + resolve( doc["res"]); + }, 0); + }); + + this.fetchStub = this.sandbox.spy((doc as any), "fetch"); + this.init = new ApplicationInsights({ + config: this._config, + }); + this.init.loadAppInsights(); + this._ai = this.init; + }].concat(PollingAssert.createPollingAssert(() => { + + if (this.fetchStub.called){ + let plugin = this._ai.appInsights['core'].getPlugin(this.identifier).plugin; + let newCfg = plugin.getCfg(); + Assert.equal(JSON.stringify(newCfg.throttleMgrCfg), JSON.stringify(sampleConfig.throttleMgrCfg)); + // cdn should not be changed + let cdnCfg = this.init.config.throttleMgrCfg[_eInternalMessageId.CdnDeprecation]; + Assert.equal(JSON.stringify(cdnCfg), JSON.stringify(default_throttle_config)); + return true; + } + return false; + + }, "response received", 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "CfgSyncPlugin: customer set throttle config, new config fetch from config url could overwrite original one", + stepDelay: 10, + useFakeTimers: true, + steps: [ () => { + let doc = getGlobal(); + hookFetch((resolve) => { // global instance cannot access test private instance + AITestClass.orgSetTimeout(function() { + resolve( doc["res"]); + }, 0); + }); + + this.fetchStub = this.sandbox.spy((doc as any), "fetch"); + let config = { + instrumentationKey: TestInstrumentationKey, + featureOptIn : {["iKeyUsage"]: {mode: FeatureOptInMode.enable}}, + throttleMgrCfg: { + 109: { + disabled: false, + limit: { + samplingRate: 50, + maxSendNumber: 1 + }, + interval: { + daysOfMonth:[1], + monthInterval: 1 + } + } + }, + extensionConfig : {["AppInsightsCfgSyncPlugin"] : { + syncMode: ICfgSyncMode.Receive, + cfgUrl: "testurl" + }} + }; + + this.init = new ApplicationInsights({ + config: config, + }); + this.init.loadAppInsights(); + this._ai = this.init; + }].concat(PollingAssert.createPollingAssert(() => { + + if (this.fetchStub.called){ + let plugin = this._ai.appInsights['core'].getPlugin(this.identifier).plugin; + let newCfg = plugin.getCfg(); + Assert.equal(JSON.stringify(newCfg.throttleMgrCfg), JSON.stringify(sampleConfig.throttleMgrCfg)); + // cdn should not be overwritten + let cdnCfg = this.init.config.throttleMgrCfg[_eInternalMessageId.CdnDeprecation]; + Assert.equal(JSON.stringify(cdnCfg), JSON.stringify(default_throttle_config)); + let ikeyCfg = this.init.config.throttleMgrCfg[_eInternalMessageId.InstrumentationKeyDeprecation]; + Assert.equal(JSON.stringify(ikeyCfg), JSON.stringify(sampleConfig.throttleMgrCfg[_eInternalMessageId.InstrumentationKeyDeprecation])); + return true; + } + return false; + }, "response received", 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "CfgSyncPlugin: customer enable feature opt in, then the config in cdn feature opt in is applied", + stepDelay: 10, + useFakeTimers: true, + steps: [ () => { + let doc = getGlobal(); + hookFetch((resolve) => { // global instance cannot access test private instance + AITestClass.orgSetTimeout(function() { + resolve( doc["res2"]); + }, 0); + }); + this.fetchStub = this.sandbox.spy((doc as any), "fetch"); + this.init = new ApplicationInsights({ + config: { + instrumentationKey: TestInstrumentationKey, + extensionConfig : {["AppInsightsCfgSyncPlugin"] : { + syncMode: ICfgSyncMode.Receive, + cfgUrl: "testurl" + }}, + featureOptIn : {["iKeyUsage"]: {mode: FeatureOptInMode.enable}, + ["enableWParamFeature"]: {mode: FeatureOptInMode.enable}} + } + }); + this.init.loadAppInsights(); + this._ai = this.init; + }].concat(PollingAssert.createPollingAssert(() => { + + if (this.fetchStub.called){ + Assert.equal(this.init.config.throttleMgrCfg[_eInternalMessageId.InstrumentationKeyDeprecation].disabled, false); + Assert.equal(this.init.config.throttleMgrCfg[_eInternalMessageId.CdnDeprecation].disabled, true); + Assert.equal(this.init.config.throttleMgrCfg[_eInternalMessageId.InstrumentationKeyDeprecation].limit?.maxSendNumber, throttleCfgDisable[_eInternalMessageId.InstrumentationKeyDeprecation].limit?.maxSendNumber); + return true; + } + return false; + }, "response received", 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "CfgSyncPlugin: customer disable feature opt in, the origin config on cdn will apply", + stepDelay: 10, + useFakeTimers: true, + steps: [ () => { + let doc = getGlobal(); + hookFetch((resolve) => { // global instance cannot access test private instance + AITestClass.orgSetTimeout(function() { + resolve( doc["res2"]); + }, 0); + }); + this.fetchStub = this.sandbox.spy((doc as any), "fetch"); + this.init = new ApplicationInsights({ + config: { + instrumentationKey: TestInstrumentationKey, + extensionConfig : {["AppInsightsCfgSyncPlugin"] : { + syncMode: ICfgSyncMode.Receive, + cfgUrl: "testurl" + }}, + featureOptIn : { + ["enableWParamFeature"]: {mode: FeatureOptInMode.enable}} + } + }); + this.init.loadAppInsights(); + this._ai = this.init; + }].concat(PollingAssert.createPollingAssert(() => { + if (this.fetchStub.called){ + Assert.equal(this.init.config.throttleMgrCfg[_eInternalMessageId.InstrumentationKeyDeprecation].disabled, true); + Assert.equal(this.init.config.throttleMgrCfg[_eInternalMessageId.CdnDeprecation].disabled, true); + Assert.equal(this.init.config.throttleMgrCfg[_eInternalMessageId.InstrumentationKeyDeprecation].limit?.maxSendNumber, throttleCfgDisable[_eInternalMessageId.InstrumentationKeyDeprecation].limit?.maxSendNumber); + return true; + } + return false; + }, "response received", 60, 1000) as any) + }); + + + } +} + +function hookFetch(executor: (resolve: (value?: T | PromiseLike) => void, reject: (reason?: any) => void) => void): IFetchArgs[] { + let calls:IFetchArgs[] = []; + let global = getGlobal() as any; + global.fetch = function(input: RequestInfo, init?: RequestInit) { + calls.push({ + input, + init + }); + return createSyncPromise(executor); + } + + return calls; +} \ No newline at end of file diff --git a/AISKU/Tests/Unit/src/ThrottleSentMessage.tests.ts b/AISKU/Tests/Unit/src/ThrottleSentMessage.tests.ts index 1cede5c1b..317c6dda0 100644 --- a/AISKU/Tests/Unit/src/ThrottleSentMessage.tests.ts +++ b/AISKU/Tests/Unit/src/ThrottleSentMessage.tests.ts @@ -51,6 +51,9 @@ export class ThrottleSentMessage extends AITestClass { public testInitialize() { try { + if (window.localStorage){ + window.localStorage.clear(); + } this.useFakeServer = false; this._config = this._getTestConfig(); @@ -73,6 +76,9 @@ export class ThrottleSentMessage extends AITestClass { // force unload this._ai.unload(false); } + if (window.localStorage){ + window.localStorage.clear(); + } } public registerTests() { @@ -83,7 +89,7 @@ export class ThrottleSentMessage extends AITestClass { public cdnDeprecatedMessageTests(): void { this.testCase({ - name: "CdnDeprecatedMessageTests: Message is sent when az416426 is used", + name: "ThrottleSentMessage: Message is sent when az416426 is used", useFakeTimers: true, test: () => { Assert.ok(this._ai, 'ApplicationInsights SDK exists'); diff --git a/AISKU/Tests/Unit/src/aiskuunittests.ts b/AISKU/Tests/Unit/src/aiskuunittests.ts index 466241b20..d2056151e 100644 --- a/AISKU/Tests/Unit/src/aiskuunittests.ts +++ b/AISKU/Tests/Unit/src/aiskuunittests.ts @@ -6,6 +6,8 @@ import { SanitizerE2ETests } from './sanitizer.e2e.tests'; import { ValidateE2ETests } from './validate.e2e.tests'; import { SenderE2ETests } from './sender.e2e.tests'; import { SnippetInitializationTests } from './SnippetInitialization.Tests'; +import { CdnThrottle} from "./CdnThrottle.tests"; +import { ThrottleSentMessage } from "./ThrottleSentMessage.tests"; export function runTests() { new AISKUSizeCheck().registerTests(); @@ -17,4 +19,6 @@ export function runTests() { new SenderE2ETests().registerTests(); new SnippetInitializationTests(false).registerTests(); new SnippetInitializationTests(true).registerTests(); + new ThrottleSentMessage().registerTests(); + new CdnThrottle().registerTests(); } \ No newline at end of file diff --git a/AISKU/src/AISku.ts b/AISKU/src/AISku.ts index 02319ee98..0b3f6363b 100644 --- a/AISKU/src/AISku.ts +++ b/AISKU/src/AISku.ts @@ -9,8 +9,8 @@ import { Sender } from "@microsoft/applicationinsights-channel-js"; import { AnalyticsPluginIdentifier, DEFAULT_BREEZE_PATH, IAutoExceptionTelemetry, IConfig, IDependencyTelemetry, IEventTelemetry, IExceptionTelemetry, IMetricTelemetry, IPageViewPerformanceTelemetry, IPageViewTelemetry, IRequestHeaders, - ITelemetryContext as Common_ITelemetryContext, IThrottleMgrConfig, ITraceTelemetry, PropertiesPluginIdentifier, ThrottleMgr, - parseConnectionString + ITelemetryContext as Common_ITelemetryContext, IThrottleInterval, IThrottleLimit, IThrottleMgrConfig, ITraceTelemetry, + PropertiesPluginIdentifier, ThrottleMgr, parseConnectionString } from "@microsoft/applicationinsights-common"; import { AppInsightsCore, FeatureOptInMode, IAppInsightsCore, IChannelControls, IConfigDefaults, IConfiguration, ICookieMgr, ICustomProperties, @@ -53,16 +53,20 @@ const SDK_LOADER_VER = "SdkLoaderVer"; const UNDEFINED_VALUE: undefined = undefined; +const default_limit = { + samplingRate: 100, + maxSendNumber: 1 +} as IThrottleLimit; + +const default_interval = { + monthInterval: 3, + daysOfMonth: [28] +} as IThrottleInterval; + const default_throttle_config = { disabled: true, - limit: { - samplingRate: 100, - maxSendNumber: 1 - }, - interval: { - monthInterval: 3, - daysOfMonth: [28] - } + limit: cfgDfMerge(default_limit), + interval: cfgDfMerge(default_interval) } as IThrottleMgrConfig; // We need to include all properties that we only reference that we want to be dynamically updatable here