From 55259461b3a24698168a0145514ca16867fb2f56 Mon Sep 17 00:00:00 2001 From: Karlie-777 <79606506+Karlie-777@users.noreply.github.com> Date: Mon, 24 Oct 2022 16:03:24 -0700 Subject: [PATCH] [Task]14569737: Add support to throttle the deprecation message (#1882) * add throttle feature * move from core to common * update shrinkwrap * update * rename interface * update * update shrinkwrap * update shrinkwrap * reduce size * remove unused variables * update local storage objects * update shrinkwrap * resolve comments * update test --- .aiAutoMinify.json | 1 + common/config/rush/npm-shrinkwrap.json | 6 +- .../Tests/Unit/src/ThrottleMgr.tests.ts | 628 ++++++++++++++++++ .../Unit/src/appinsights-common.tests.ts | 2 + shared/AppInsightsCommon/package.json | 3 +- shared/AppInsightsCommon/src/Enums.ts | 6 + .../src/Interfaces/IThrottleMgr.ts | 109 +++ shared/AppInsightsCommon/src/ThrottleMgr.ts | 285 ++++++++ .../src/applicationinsights-common.ts | 4 +- .../src/JavaScriptSDK/EnvUtils.ts | 1 + 10 files changed, 1041 insertions(+), 4 deletions(-) create mode 100644 shared/AppInsightsCommon/Tests/Unit/src/ThrottleMgr.tests.ts create mode 100644 shared/AppInsightsCommon/src/Interfaces/IThrottleMgr.ts create mode 100644 shared/AppInsightsCommon/src/ThrottleMgr.ts diff --git a/.aiAutoMinify.json b/.aiAutoMinify.json index 6281a1ec2..a7b78cf43 100644 --- a/.aiAutoMinify.json +++ b/.aiAutoMinify.json @@ -20,6 +20,7 @@ "eStorageType", "FieldType", "eDistributedTracingModes", + "IThrottleMsgKey", "eRequestHeaders", "DataPointType", "DependencyKind", diff --git a/common/config/rush/npm-shrinkwrap.json b/common/config/rush/npm-shrinkwrap.json index c1acefb59..687398fe9 100644 --- a/common/config/rush/npm-shrinkwrap.json +++ b/common/config/rush/npm-shrinkwrap.json @@ -583,7 +583,7 @@ "node_modules/@rush-temp/applicationinsights-common": { "version": "0.0.0", "resolved": "file:projects/applicationinsights-common.tgz", - "integrity": "sha512-UDe8iJSsFsXErPxwIsKJGtqExe3BQtCM2dbPms+Kh8uvQ71DwfpAH6IS04LtaYD8uVaVxsrL4EKuE5s9Ytt7HA==", + "integrity": "sha512-/lfjcQI5ZsfXv2267rSrFiu3d5FZJWGZtv7Vs4GPF3ZfhxTLlRO6TAgwk5k7sDgmmvZRf3tlY4oAk1inFjb/pA==", "dependencies": { "@microsoft/api-extractor": "^7.18.1", "@microsoft/applicationinsights-shims": "2.0.1", @@ -600,6 +600,7 @@ "magic-string": "^0.25.7", "rollup": "^2.32.0", "rollup-plugin-cleanup": "^3.2.1", + "sinon": "^7.3.1", "tslib": "^2.0.0", "typescript": "^4.3.4" } @@ -5866,7 +5867,7 @@ }, "@rush-temp/applicationinsights-common": { "version": "file:projects\\applicationinsights-common.tgz", - "integrity": "sha512-UDe8iJSsFsXErPxwIsKJGtqExe3BQtCM2dbPms+Kh8uvQ71DwfpAH6IS04LtaYD8uVaVxsrL4EKuE5s9Ytt7HA==", + "integrity": "sha512-/lfjcQI5ZsfXv2267rSrFiu3d5FZJWGZtv7Vs4GPF3ZfhxTLlRO6TAgwk5k7sDgmmvZRf3tlY4oAk1inFjb/pA==", "requires": { "@microsoft/api-extractor": "^7.18.1", "@microsoft/applicationinsights-shims": "2.0.1", @@ -5883,6 +5884,7 @@ "magic-string": "^0.25.7", "rollup": "^2.32.0", "rollup-plugin-cleanup": "^3.2.1", + "sinon": "^7.3.1", "tslib": "^2.0.0", "typescript": "^4.3.4" } diff --git a/shared/AppInsightsCommon/Tests/Unit/src/ThrottleMgr.tests.ts b/shared/AppInsightsCommon/Tests/Unit/src/ThrottleMgr.tests.ts new file mode 100644 index 000000000..c6aa8243c --- /dev/null +++ b/shared/AppInsightsCommon/Tests/Unit/src/ThrottleMgr.tests.ts @@ -0,0 +1,628 @@ +import { Assert, AITestClass } from "@microsoft/ai-test-framework"; +import { AppInsightsCore, IAppInsightsCore, _eInternalMessageId } from "@microsoft/applicationinsights-core-js"; +import { IThrottleMsgKey } from "../../../src/Enums"; +import { IThrottleInterval, IThrottleLimit, IThrottleMgrConfig, IThrottleResult } from "../../../src/Interfaces/IThrottleMgr"; +import { ThrottleMgr} from "../../../src/ThrottleMgr"; +import { SinonSpy } from "sinon"; +import { Util } from "../../../src/Util"; + +const compareDates = (date1: Date, date: string | Date, expectedSame: boolean = true) => { + let isSame = false; + try { + if (date1 && date) { + let date2 = typeof date == "string"? new Date(date) : date; + isSame = date1.getUTCFullYear() === date2.getUTCFullYear() && + date1.getUTCMonth() === date2.getUTCMonth() && + date1.getUTCDate() === date2.getUTCDate(); + } + } catch (e) { + Assert.ok(false,"compare dates error" + e); + } + Assert.equal(isSame, expectedSame, "checking that the dates where as expected"); +} + +export class ThrottleMgrTest extends AITestClass { + private _core: IAppInsightsCore; + private _msgKey: IThrottleMsgKey; + private _storageName: string; + private _msgId: _eInternalMessageId; + private loggingSpy: SinonSpy; + + public testInitialize() { + this._core = new AppInsightsCore(); + this._msgKey = IThrottleMsgKey.ikeyDeprecate; + this._storageName = "appInsightsThrottle-" + this._msgKey; + this.loggingSpy = this.sandbox.stub(this._core.logger, "throwInternal"); + this._msgId = _eInternalMessageId.InstrumentationKeyDeprecation; + + if (Util.canUseLocalStorage()) { + window.localStorage.clear(); + } + } + + public testCleanup() { + if (Util.canUseLocalStorage()) { + window.localStorage.clear(); + } + } + + public registerTests() { + + this.testCase({ + name: "ThrottleMgrTest: Throttle Manager can get expected config", + test: () => { + let config = { + msgKey: this._msgKey, + disabled: false, + limit: { + samplingRate: 50, + maxSendNumber: 100 + } as IThrottleLimit, + interval: { + monthInterval: 2, + dayInterval: 10, + maxTimesPerMonth: 1 + } as IThrottleInterval + } as IThrottleMgrConfig; + + let throttleMgr = new ThrottleMgr(config, this._core); + let actualConfig = throttleMgr.getConfig(); + Assert.deepEqual(config, actualConfig); + + let isTriggered = throttleMgr.isTriggered(); + Assert.equal(isTriggered, false); + } + }); + + this.testCase({ + name: "ThrottleMgrTest: Throttle Manager can get default config", + test: () => { + let config = { + msgKey: this._msgKey + } as IThrottleMgrConfig; + + let expectedConfig = { + msgKey: this._msgKey, + disabled: false, + limit: { + samplingRate: 100, + maxSendNumber:1 + } as IThrottleLimit, + interval: { + monthInterval: 3, + dayInterval: 28, + maxTimesPerMonth: 1 + } as IThrottleInterval + } as IThrottleMgrConfig; + + let throttleMgr = new ThrottleMgr(config, this._core); + let actualConfig = throttleMgr.getConfig(); + Assert.deepEqual(expectedConfig, actualConfig); + + let isTriggered = throttleMgr.isTriggered(); + Assert.equal(isTriggered, false); + } + }); + + this.testCase({ + name: "ThrottleMgrTest: should not trigger throttle when disabled is true", + test: () => { + let config = { + msgKey: this._msgKey, + disabled: true + } as IThrottleMgrConfig; + let throttleMgr = new ThrottleMgr(config, this._core); + let canThrottle = throttleMgr.canThrottle(); + Assert.equal(canThrottle, false); + + let isTriggered = throttleMgr.isTriggered(); + Assert.equal(isTriggered, false); + } + }); + + this.testCase({ + name: "ThrottleMgrTest: should not trigger throttle when month interval requirements are not meet", + test: () => { + let date = new Date(); + let month = date.getUTCMonth(); + let year = date.getUTCFullYear(); + if (month == 0) { + date.setUTCFullYear(year-1); + date.setUTCMonth(11); + } else { + date.setUTCMonth(month-1); + } + let storageObj = { + date: date, + count: 0 + } + window.localStorage[this._storageName] = JSON.stringify(storageObj); + + let config = { + msgKey: this._msgKey, + disabled: false, + limit: { + samplingRate: 1000000, + maxSendNumber: 100 + } as IThrottleLimit, + interval: { + monthInterval: 3, + dayInterval: 1, + maxTimesPerMonth: 100 + } as IThrottleInterval + } as IThrottleMgrConfig; + + let throttleMgr = new ThrottleMgr(config, this._core); + let canThrottle = throttleMgr.canThrottle(); + Assert.equal(canThrottle, false); + + let isTriggered = throttleMgr.isTriggered(); + Assert.equal(isTriggered, false); + } + + }); + + this.testCase({ + name: "ThrottleMgrTest: should not trigger throttle when year and month interval requirements are not meet", + test: () => { + let date = new Date(); + let month = date.getUTCMonth(); + let year = date.getUTCFullYear(); + + if (month == 0) { + date.setUTCFullYear(year-2); + date.setUTCMonth(11); + } else { + date.setUTCFullYear(year-1); + date.setUTCMonth(month-1); + } + let storageObj = { + date: date, + count: 0 + } + window.localStorage[this._storageName] = JSON.stringify(storageObj); + + let config = { + msgKey: this._msgKey, + disabled: false, + limit: { + samplingRate: 1000000, + maxSendNumber: 100 + } as IThrottleLimit, + interval: { + monthInterval: 3, + dayInterval: 1, + maxTimesPerMonth: 100 + } as IThrottleInterval + } as IThrottleMgrConfig; + + let throttleMgr = new ThrottleMgr(config, this._core); + let canThrottle = throttleMgr.canThrottle(); + Assert.equal(canThrottle, false); + + let isTriggered = throttleMgr.isTriggered(); + Assert.equal(isTriggered, false); + } + + }); + + this.testCase({ + name: "ThrottleMgrTest: should not trigger throttle when day interval requirements are not meet", + test: () => { + let date = new Date(); + let storageObj = { + date: date, + count: 0 + } + window.localStorage[this._storageName] = JSON.stringify(storageObj); + + let config = { + msgKey: this._msgKey, + disabled: false, + limit: { + samplingRate: 1000000, + maxSendNumber: 100 + } as IThrottleLimit, + interval: { + monthInterval: 1, + dayInterval: 31, + maxTimesPerMonth: 100 + } as IThrottleInterval + } as IThrottleMgrConfig; + + let throttleMgr = new ThrottleMgr(config, this._core); + let canThrottle = throttleMgr.canThrottle(); + Assert.equal(canThrottle, false); + + let isTriggered = throttleMgr.isTriggered(); + Assert.equal(isTriggered, false); + } + + }); + + this.testCase({ + name: "ThrottleMgrTest: should trigger throttle at starting month", + test: () => { + let date = new Date(); + let storageObj = { + date: date, + count: 0 + } + window.localStorage[this._storageName] = JSON.stringify(storageObj); + + let config = { + msgKey: this._msgKey, + disabled: false, + limit: { + samplingRate: 1000000, + maxSendNumber: 100 + } as IThrottleLimit, + interval: { + monthInterval: 1, + dayInterval: 1, + maxTimesPerMonth: 100 + } as IThrottleInterval + } as IThrottleMgrConfig; + + let throttleMgr = new ThrottleMgr(config, this._core); + let canThrottle = throttleMgr.canThrottle(); + Assert.equal(canThrottle, true); + + let isTriggered = throttleMgr.isTriggered(); + Assert.equal(isTriggered, false); + } + + }); + + this.testCase({ + name: "ThrottleMgrTest: should trigger throttle when month and day intervals are meet", + test: () => { + let date = new Date(); + let year = date.getUTCFullYear() - 2; + date.setUTCFullYear(year); + let storageObj = { + date: date, + count: 0 + } + window.localStorage[this._storageName] = JSON.stringify(storageObj); + + let config = { + msgKey: this._msgKey, + disabled: false, + limit: { + samplingRate: 1000000, + maxSendNumber: 100 + } as IThrottleLimit, + interval: { + monthInterval: 4, + dayInterval: 1, + maxTimesPerMonth: 100 + } as IThrottleInterval + } as IThrottleMgrConfig; + + let throttleMgr = new ThrottleMgr(config, this._core); + let canThrottle = throttleMgr.canThrottle(); + Assert.equal(canThrottle, true); + + let isTriggered = throttleMgr.isTriggered(); + Assert.equal(isTriggered, false); + } + + }); + + this.testCase({ + name: "ThrottleMgrTest: should not trigger throttle when maxSentTimes is not meet", + test: () => { + let date = new Date(); + let day = date.getUTCDate(); + let storageObj = { + date: date, + count: 0 + } + window.localStorage[this._storageName] = JSON.stringify(storageObj); + let maxTimes = day-1; + let config = { + msgKey: this._msgKey, + disabled: false, + limit: { + samplingRate: 1000000, + maxSendNumber: 100 + } as IThrottleLimit, + interval: { + monthInterval: 1, + dayInterval: 1, + maxTimesPerMonth: maxTimes + } as IThrottleInterval + } as IThrottleMgrConfig; + + let throttleMgr = new ThrottleMgr(config, this._core); + let canThrottle = throttleMgr.canThrottle(); + Assert.equal(canThrottle, false); + + let isTriggered = throttleMgr.isTriggered(); + Assert.equal(isTriggered, false); + } + + }); + + this.testCase({ + name: "ThrottleMgrTest: should not trigger throttle when _isTrigger state is true (in valid send message date)", + test: () => { + let storageObj = { + date: new Date(), + count: 0, + preTriggerDate: new Date() + } + window.localStorage[this._storageName] = JSON.stringify(storageObj); + let config = { + msgKey: this._msgKey, + disabled: false, + limit: { + samplingRate: 1000000, + maxSendNumber: 100 + } as IThrottleLimit, + interval: { + monthInterval: 1, + dayInterval: 1, + maxTimesPerMonth: 33 + } as IThrottleInterval + } as IThrottleMgrConfig; + + let throttleMgr = new ThrottleMgr(config, this._core); + let canThrottle = throttleMgr.canThrottle(); + Assert.equal(canThrottle, true); + let isTriggered = throttleMgr.isTriggered(); + Assert.ok(isTriggered); + let result = throttleMgr.sendMessage(this._msgId, "test"); + let count = this.loggingSpy.callCount; + Assert.equal(count,0); + Assert.deepEqual(result, null); + let postIsTriggered = throttleMgr.isTriggered(); + Assert.ok(postIsTriggered); + + } + + }); + + this.testCase({ + name: "ThrottleMgrTest: should update local storage (in valid send message date)", + test: () => { + let date = new Date(); + let msg = "Instrumentation key support will end soon, see aka.ms/IkeyMigrate"; + + let preTriggerDate = date; + preTriggerDate.setUTCFullYear(date.getUTCFullYear() - 1); + let preStorageObj = { + date: date, + count: 0, + preTriggerDate: preTriggerDate + } + + let preStorageVal = JSON.stringify(preStorageObj); + window.localStorage[this._storageName] = preStorageVal; + + let config = { + msgKey: this._msgKey, + disabled: false, + limit: { + samplingRate: 1000000, + maxSendNumber: 1 + } as IThrottleLimit, + interval: { + monthInterval: 1, + dayInterval: date.getUTCDate(), + maxTimesPerMonth: 1 + } as IThrottleInterval + } as IThrottleMgrConfig; + + let throttleMgr = new ThrottleMgr(config, this._core); + let canThrottle = throttleMgr.canThrottle(); + Assert.equal(canThrottle, true); + let isPretriggered = throttleMgr.isTriggered(); + Assert.equal(isPretriggered, false); + + throttleMgr.onReadyState(true); + let sendDate = new Date(); + let result = throttleMgr.sendMessage(this._msgId, msg); + let expectedRetryRlt = { + isThrottled: true, + throttleNum: 1 + } as IThrottleResult + Assert.deepEqual(result, expectedRetryRlt); + let isPostTriggered = throttleMgr.isTriggered(); + Assert.equal(isPostTriggered, true); + + let afterTriggered = window.localStorage[this._storageName]; + let afterTriggeredObj = JSON.parse(afterTriggered); + compareDates(date, afterTriggeredObj.date) + Assert.equal(0, afterTriggeredObj.count); + compareDates(sendDate, afterTriggeredObj.preTriggerDate); + } + + }); + + this.testCase({ + name: "ThrottleMgrTest: should not trigger sendmessage when isready state is not set and should flush message after isReady state is set", + test: () => { + let msg = "Instrumentation key support will end soon, see aka.ms/IkeyMigrate"; + let date = new Date(); + let config = { + msgKey: IThrottleMsgKey.ikeyDeprecate, + disabled: false, + limit: { + samplingRate: 1000000, + maxSendNumber:1 + } as IThrottleLimit, + interval: { + monthInterval: 1, + dayInterval: 1, + maxTimesPerMonth: 100 + } as IThrottleInterval + } as IThrottleMgrConfig; + + let throttleMgr = new ThrottleMgr(config, this._core); + + + let canThrottle = throttleMgr.canThrottle(); + Assert.ok(canThrottle); + let isTriggeredPre = throttleMgr.isTriggered(); + Assert.equal(isTriggeredPre, false); + let initialVal = window.localStorage[this._storageName]; + let initObj = JSON.parse(initialVal); + compareDates(date, initObj.date); + Assert.equal(0, initObj.count); + Assert.equal(undefined, initObj.preTriggerDate); + + let result = throttleMgr.sendMessage(this._msgId, msg); + let count = this.loggingSpy.callCount; + Assert.equal(count,0); + Assert.deepEqual(result, null); + let isTriggeredPost = throttleMgr.isTriggered(); + Assert.equal(isTriggeredPost, false); + let postVal = window.localStorage[this._storageName]; + let postObj = JSON.parse(postVal); + compareDates(date, postObj.date); + Assert.equal(0, postObj.count); + Assert.equal(undefined, postObj.preTriggerDate); + + let isFlushed = throttleMgr.onReadyState(true); + Assert.ok(isFlushed); + let isTriggeredAfterReadySate = throttleMgr.isTriggered(); + Assert.ok(isTriggeredAfterReadySate); + let postOnReadyVal = window.localStorage[this._storageName]; + let postOnReadyObj = JSON.parse(postOnReadyVal); + compareDates(date, postOnReadyObj.date); + Assert.equal(0, postOnReadyObj.count); + compareDates(date, postOnReadyObj.preTriggerDate); + + let onReadyResult = throttleMgr.sendMessage(this._msgId, msg); + let onReadyCount = this.loggingSpy.callCount; + let expectedRlt = { + isThrottled: false, + throttleNum: 0 + } as IThrottleResult + Assert.equal(onReadyCount,1); + Assert.deepEqual(onReadyResult, expectedRlt); + let onReadyIsTriggered = throttleMgr.isTriggered(); + Assert.equal(onReadyIsTriggered, true); + let afterResendVal = window.localStorage[this._storageName]; + let afterResendObj = JSON.parse(afterResendVal); + compareDates(date, afterResendObj.date); + Assert.equal(1, afterResendObj.count); + compareDates(date, afterResendObj.preTriggerDate); + } + }); + + + this.testCase({ + name: "ThrottleMgrTest: throw messages with correct number", + test: () => { + let msg = "Instrumentation key support will end soon, see aka.ms/IkeyMigrate"; + let date = new Date(); + let testStorageObj = { + date: date, + count: 5 + } + let testStorageVal = JSON.stringify(testStorageObj); + window.localStorage[this._storageName] = testStorageVal; + + let config = { + msgKey: IThrottleMsgKey.ikeyDeprecate, + disabled: false, + limit: { + samplingRate: 1000000, + maxSendNumber:1 + } as IThrottleLimit, + interval: { + monthInterval: 1, + dayInterval: 1, + maxTimesPerMonth: 100 + } as IThrottleInterval + } as IThrottleMgrConfig; + + let throttleMgr = new ThrottleMgr(config, this._core); + + let canThrottle = throttleMgr.canThrottle(); + Assert.ok(canThrottle); + + let isTriggeredPre = throttleMgr.isTriggered(); + Assert.equal(isTriggeredPre, false); + + throttleMgr.onReadyState(true); + + let result = throttleMgr.sendMessage(this._msgId, msg); + let count = this.loggingSpy.callCount + Assert.equal(count,1); + let expectedRlt = { + isThrottled: true, + throttleNum: 1 + } as IThrottleResult + Assert.deepEqual(result, expectedRlt); + + let val = window.localStorage[this._storageName]; + let obj = JSON.parse(val); + compareDates(date, obj.date); + Assert.equal(0, obj.count); + compareDates(date, obj.preTriggerDate); + + let isTriggeredPost = throttleMgr.isTriggered(); + Assert.equal(isTriggeredPost, true); + } + }); + + this.testCase({ + name: "ThrottleMgrTest: should throw messages 1 time within a day", + test: () => { + let msg = "Instrumentation key support will end soon, see aka.ms/IkeyMigrate"; + + let config = { + msgKey: IThrottleMsgKey.ikeyDeprecate, + disabled: false, + limit: { + samplingRate: 1000000, + maxSendNumber:1 + } as IThrottleLimit, + interval: { + monthInterval: 1, + dayInterval: 1, + maxTimesPerMonth: 100 + } as IThrottleInterval + } as IThrottleMgrConfig; + + let throttleMgr = new ThrottleMgr(config, this._core); + + let canThrottle = throttleMgr.canThrottle(); + Assert.ok(canThrottle); + + let isTriggeredPre = throttleMgr.isTriggered(); + Assert.equal(isTriggeredPre, false); + + throttleMgr.onReadyState(true); + + let result = throttleMgr.sendMessage(this._msgId, msg); + let count = this.loggingSpy.callCount + Assert.equal(count,1); + + let expectedRlt = { + isThrottled: true, + throttleNum: 1 + } as IThrottleResult + Assert.deepEqual(result, expectedRlt); + + let isTriggeredPost = throttleMgr.isTriggered(); + Assert.equal(isTriggeredPost, true); + let canThrottlePost = throttleMgr.canThrottle(); + Assert.equal(canThrottlePost, true); + + let retryRlt = throttleMgr.sendMessage(this._msgId, msg); + let retryCount = this.loggingSpy.callCount + Assert.equal(retryCount,1); + let expectedRetryRlt = { + isThrottled: false, + throttleNum: 0 + } as IThrottleResult + Assert.deepEqual(retryRlt, expectedRetryRlt); + } + }); + } +} \ No newline at end of file diff --git a/shared/AppInsightsCommon/Tests/Unit/src/appinsights-common.tests.ts b/shared/AppInsightsCommon/Tests/Unit/src/appinsights-common.tests.ts index c4b33a47f..a05ddf193 100644 --- a/shared/AppInsightsCommon/Tests/Unit/src/appinsights-common.tests.ts +++ b/shared/AppInsightsCommon/Tests/Unit/src/appinsights-common.tests.ts @@ -4,8 +4,10 @@ import { UtilTests } from "./Util.tests"; import { ConnectionStringParserTests } from "./ConnectionStringParser.tests"; import { SeverityLevelTests } from "./SeverityLevel.tests"; import { RequestHeadersTests } from "./RequestHeaders.tests"; +import { ThrottleMgrTest } from "./ThrottleMgr.tests"; export function runTests() { + new ThrottleMgrTest().registerTests(); new ApplicationInsightsTests().registerTests(); new ExceptionTests().registerTests(); new UtilTests().registerTests(); diff --git a/shared/AppInsightsCommon/package.json b/shared/AppInsightsCommon/package.json index 22057e98a..4711553ba 100644 --- a/shared/AppInsightsCommon/package.json +++ b/shared/AppInsightsCommon/package.json @@ -43,7 +43,8 @@ "typescript": "^4.3.4", "tslib": "^2.0.0", "globby": "^11.0.0", - "magic-string": "^0.25.7" + "magic-string": "^0.25.7", + "sinon": "^7.3.1" }, "peerDependencies": { "tslib": "*" diff --git a/shared/AppInsightsCommon/src/Enums.ts b/shared/AppInsightsCommon/src/Enums.ts index d84fda2e1..5731a439f 100644 --- a/shared/AppInsightsCommon/src/Enums.ts +++ b/shared/AppInsightsCommon/src/Enums.ts @@ -47,3 +47,9 @@ export const DistributedTracingModes = createEnumStyle boolean; + public sendMessage: (msgID: _eInternalMessageId, message: string, severity?: eLoggingSeverity) => IThrottleResult | null; + public getConfig: () => IThrottleMgrConfig; + public isTriggered: () => boolean; // this function is to get previous triggered status + public isReady: () => boolean + public onReadyState: (isReady?: boolean) => boolean; + public flush: () => boolean; + + constructor(throttleMgr?: IThrottleMgrConfig, core?: IAppInsightsCore, namePrefix?: string) { + let _self = this; + let _canUseLocalStorage: boolean; + let _logger: IDiagnosticLogger | null | undefined; + let _config: IThrottleMgrConfig; + let _localStorageName: string | null; + let _localStorageObj: IThrottleLocalStorageObj | null | undefined; + let _isTriggered: boolean; //_isTriggered is to make sure that we only trigger throttle once a day + let _namePrefix: string; + let _queue: Array; + let _isReady: boolean = false; + + _initConfig(); + + _self.getConfig = (): IThrottleMgrConfig => { + return _config; + } + + /** + * Check if it is the correct day to send message. + * If _isTriggered is true, even if canThrottle returns true, message will not be sent, + * because we only allow triggering sendMessage() once a day. + * @returns if the current date is the valid date to send message + */ + _self.canThrottle = (): boolean => { + return _canThrottle(_config, _canUseLocalStorage, _localStorageObj); + } + + /** + * Check if throttle is triggered on current day(UTC) + * if canThrottle returns false, isTriggered will return false + * @returns if throttle is triggered on current day(UTC) + */ + _self.isTriggered = (): boolean => { + return _isTriggered; + } + + /** + * Before isReady set to true, all message will be stored in queue. + * Message will only be sent out after isReady set to true. + * Initial and default value: false + * @returns isReady state + */ + _self.isReady = (): boolean => { + return _isReady; + } + + /** + * Flush all message in queue with isReady state set to true. + * @returns if message queue is flushed + */ + _self.flush = (): boolean => { + try { + if (_isReady && _queue.length > 0) { + arrForEach(_queue, (item: SendMsgParameter) => { + _self.sendMessage(item.msgID, item.message, item.severity); + }); + return true; + } + } catch(err) { + // eslint-disable-next-line no-empty + } + return false; + } + + /** + * Set isReady State + * if isReady set to true, message queue will be flushed automatically. + * @param isReady isReady State + * @returns if message queue is flushed + */ + _self.onReadyState = (isReady?: boolean): boolean => { + _isReady = isNullOrUndefined(isReady)? true : isReady; + return _self.flush(); + } + + _self.sendMessage = (msgID: _eInternalMessageId, message: string, severity?: eLoggingSeverity): IThrottleResult | null => { + if (_isReady) { + let isSampledIn = _canSampledIn(); + if (!isSampledIn) { + return; + } + let canThrottle = _canThrottle(_config, _canUseLocalStorage, _localStorageObj); + let throttled = false; + let number = 0; + try { + if (canThrottle && !_isTriggered) { + number = Math.min(_config.limit.maxSendNumber, _localStorageObj.count + 1); + _localStorageObj.count = 0; + throttled = true; + _isTriggered = true; + _localStorageObj.preTriggerDate = new Date(); + } else { + _isTriggered = canThrottle; + _localStorageObj.count += 1; + } + _resetLocalStorage(_logger, _localStorageName, _localStorageObj); + for (let i = 0; i < number; i++) { + _sendMessage(msgID, _logger, message, severity); + } + } catch(e) { + // eslint-disable-next-line no-empty + } + return { + isThrottled: throttled, + throttleNum: number + } as IThrottleResult; + } else { + _queue.push({ + msgID: msgID, + message: message, + severity: severity + } as SendMsgParameter); + } + return null; + } + + function _initConfig() { + _canUseLocalStorage = utlCanUseLocalStorage(); + _logger = safeGetLogger(core); + _isTriggered = false; + _namePrefix = isNotNullOrUndefined(namePrefix)? namePrefix : ""; + _queue = []; + let configMgr = throttleMgr; + _config = {} as any; + _config.disabled = !!configMgr.disabled; + _config.msgKey = configMgr.msgKey; + // default: send data on 28th every 3 month each year + let interval = { + // dafault: sent every three months + monthInterval: configMgr.interval?.monthInterval || 3, + dayInterval : configMgr.interval?.dayInterval || 28, + maxTimesPerMonth: configMgr.interval?.maxTimesPerMonth || 1 + }; + _config.interval = interval; + let limit = { + samplingRate: configMgr.limit?.samplingRate || 100, + // dafault: every time sent only 1 event + maxSendNumber: configMgr.limit?.maxSendNumber || 1 + }; + _config.limit = limit; + _localStorageName = _getLocalStorageName(_config.msgKey, _namePrefix); + + if (_canUseLocalStorage && _localStorageName) { + _localStorageObj = _getLocalStorageObj(utlGetLocalStorage(_logger, _localStorageName), _logger, _localStorageName); + } + if (_localStorageObj) { + _isTriggered = _isTriggeredOnCurDate(_localStorageObj.preTriggerDate); + } + } + + function _canThrottle(config: IThrottleMgrConfig, canUseLocalStorage: boolean, localStorageObj: IThrottleLocalStorageObj) { + if (!config.disabled && canUseLocalStorage && isNotNullOrUndefined(localStorageObj)) { + let curDate = _getThrottleDate(); + let date = localStorageObj.date; + let interval = config.interval; + let monthExpand = (curDate.getUTCFullYear() - date.getUTCFullYear()) * 12 + curDate.getUTCMonth() - date.getUTCMonth(); + let monthCheck = _checkInterval(interval.monthInterval, 0, monthExpand); + let dayCheck = _checkInterval(interval.dayInterval, 0, curDate.getUTCDate()) -1; + return monthCheck >= 0 && dayCheck >= 0 && dayCheck <= config.interval.maxTimesPerMonth; + } + return false; + } + + function _getLocalStorageName(msgKey: IThrottleMsgKey, prefix?: string) { + let fix = isNotNullOrUndefined(prefix)? prefix : ""; + if (msgKey) { + return THROTTLE_STORAGE_PREFIX + fix + "-" + msgKey; + } + return null; + } + + // returns if throttle is triggered on current Date + function _isTriggeredOnCurDate(preTriggerDate?: Date) { + try { + if(preTriggerDate) { + let curDate = new Date(); + return preTriggerDate.getUTCFullYear() === curDate.getUTCFullYear() && + preTriggerDate.getUTCMonth() === curDate.getUTCMonth() && + preTriggerDate.getUTCDate() === curDate.getUTCDate(); + } + } catch (e) { + // eslint-disable-next-line no-empty + } + return false; + } + + // transfer local storage string value to object that identifies start date, current count and preTriggerDate + function _getLocalStorageObj(value: string, logger: IDiagnosticLogger, storageName: string) { + try { + let storageObj = { + date: _getThrottleDate(), + count: 0 + } as IThrottleLocalStorageObj; + if (value) { + let obj = JSON.parse(value); + return { + date: _getThrottleDate(obj.date) || storageObj.date, + count: obj.count || storageObj.count, + preTriggerDate: obj.preTriggerDate? _getThrottleDate(obj.preTriggerDate) : undefined + } as IThrottleLocalStorageObj; + } else { + _resetLocalStorage(logger, storageName, storageObj); + return storageObj; + + } + } catch(e) { + // eslint-disable-next-line no-empty + } + return null; + } + + // if datestr is not defined, current date will be returned + function _getThrottleDate(dateStr?: string) { + // if new Date() can't be created through the provided dateStr, null will be returned. + try { + if (dateStr) { + let date = new Date(dateStr); + //make sure it is a valid Date Object + if (!isNaN(date.getDate())) { + return date; + } + } else { + return new Date(); + } + + } catch (e) { + // eslint-disable-next-line no-empty + } + return null; + } + + function _resetLocalStorage(logger: IDiagnosticLogger, storageName: string, obj: IThrottleLocalStorageObj) { + try { + return utlSetLocalStorage(logger, storageName, strTrim(JSON.stringify(obj))); + } catch (e) { + // // eslint-disable-next-line no-empty + } + return false; + } + + function _checkInterval(interval: number, start: number, current: number) { + // count from start year + return (current >= start) && (current - start) % interval == 0 ? Math.floor((current - start) / interval) + 1 : -1; + } + + function _sendMessage(msgID: _eInternalMessageId, logger: IDiagnosticLogger, message: string, severity?: eLoggingSeverity) { + _throwInternal(logger, + severity || eLoggingSeverity.CRITICAL, + msgID, + message); + } + + // NOTE: config.limit.samplingRate is set to 4 decimal places, + // so config.limit.samplingRate = 1 means 0.0001% + function _canSampledIn() { + return randomValue(1000000) <= _config.limit.samplingRate; + } + } +} diff --git a/shared/AppInsightsCommon/src/applicationinsights-common.ts b/shared/AppInsightsCommon/src/applicationinsights-common.ts index 684bbebd0..d06a6a032 100644 --- a/shared/AppInsightsCommon/src/applicationinsights-common.ts +++ b/shared/AppInsightsCommon/src/applicationinsights-common.ts @@ -6,6 +6,7 @@ export { IUrlHelper, UrlHelper, isInternalApplicationInsightsEndpoint, createDistributedTraceContextFromTrace } from "./Util"; +export { ThrottleMgr } from "./ThrottleMgr"; export { parseConnectionString, ConnectionStringParser } from "./ConnectionStringParser"; export { FieldType } from "./Enums"; export { IRequestHeaders, RequestHeaders, eRequestHeaders } from "./RequestResponseHeaders"; @@ -58,7 +59,8 @@ export { IPropertiesPlugin } from "./Interfaces/IPropertiesPlugin"; export { IUser, IUserContext } from "./Interfaces/Context/IUser"; export { ITelemetryTrace, ITraceState } from "./Interfaces/Context/ITelemetryTrace"; export { IRequestContext } from "./Interfaces/IRequestContext"; -export { eDistributedTracingModes, DistributedTracingModes } from "./Enums"; +export { IThrottleLocalStorageObj, IThrottleMgrConfig, IThrottleResult, IThrottleLimit, IThrottleInterval } from "./Interfaces/IThrottleMgr"; +export { eDistributedTracingModes, DistributedTracingModes, IThrottleMsgKey } from "./Enums"; export { stringToBoolOrDefault, msToTimeSpan, getExtensionByName, isCrossOriginError } from "./HelperFuncs"; export { isBeaconsSupported as isBeaconApiSupported, diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts index 854840e16..d0ea33094 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts @@ -410,6 +410,7 @@ export function isXhrSupported(): boolean { return isSupported; } + function _getNamedValue(values: any, name: string) { if (values) { for (var i = 0; i < values.length; i++) {