diff --git a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts index 2cebc91b2..a399900fe 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 = 140; - private readonly MAX_BUNDLE_SIZE = 140; - private readonly MAX_RAW_DEFLATE_SIZE = 56; - private readonly MAX_BUNDLE_DEFLATE_SIZE = 56; + 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 rawFilePath = "../dist/es5/applicationinsights-web.min.js"; // Automatically updated by version scripts private readonly currentVer = "3.1.2"; diff --git a/AISKU/Tests/Unit/src/SnippetInitialization.Tests.ts b/AISKU/Tests/Unit/src/SnippetInitialization.Tests.ts index 8ca9e6947..c81500e07 100644 --- a/AISKU/Tests/Unit/src/SnippetInitialization.Tests.ts +++ b/AISKU/Tests/Unit/src/SnippetInitialization.Tests.ts @@ -313,7 +313,11 @@ export class SnippetInitializationTests extends AITestClass { if(this.successSpy.called) { let currentCount: number = 0; this.successSpy.args.forEach(call => { - call[0].forEach(message => { + call[0].forEach(item => { + let message = item; + if (typeof item !== "string") { + message = item.item; + } // Ignore the internal SendBrowserInfoOnUserInit message (Only occurs when running tests in a browser) if (!message || message.indexOf("AI (Internal): 72 ") == -1) { currentCount ++; @@ -1086,7 +1090,11 @@ export class SnippetInitializationTests extends AITestClass { if(this.successSpy.called) { let currentCount: number = 0; this.successSpy.args.forEach(call => { - call[0].forEach(message => { + call[0].forEach(item => { + let message = item.item; + if (typeof item === "string") { + message = item; + } // Ignore the internal SendBrowserInfoOnUserInit message (Only occurs when running tests in a browser) if (!message || message.indexOf("AI (Internal): 72 ") == -1) { currentCount ++; diff --git a/AISKU/Tests/Unit/src/sender.e2e.tests.ts b/AISKU/Tests/Unit/src/sender.e2e.tests.ts index 2f83b99ec..cc120c784 100644 --- a/AISKU/Tests/Unit/src/sender.e2e.tests.ts +++ b/AISKU/Tests/Unit/src/sender.e2e.tests.ts @@ -7,8 +7,8 @@ import { Assert, AITestClass, PollingAssert} from "@microsoft/ai-test-framework" export class SenderE2ETests extends AITestClass { private readonly _instrumentationKey = 'b7170927-2d1c-44f1-acec-59f4e1751c11'; - private readonly _bufferName = 'AI_buffer'; - private readonly _sentBufferName = 'AI_sentBuffer'; + private readonly _bufferName = 'AI_buffer_1'; + private readonly _sentBufferName = 'AI_sentBuffer_1'; private _ai: IApplicationInsights; private _sender: Sender; diff --git a/AISKU/Tests/Unit/src/validate.e2e.tests.ts b/AISKU/Tests/Unit/src/validate.e2e.tests.ts index 877e1e55d..99ae6a31e 100644 --- a/AISKU/Tests/Unit/src/validate.e2e.tests.ts +++ b/AISKU/Tests/Unit/src/validate.e2e.tests.ts @@ -138,7 +138,11 @@ export class ValidateE2ETests extends AITestClass { .concat(() => { let acceptedItems = 0; this.successSpy.args.forEach(call => { - call[0].forEach(message => { + call[0].forEach(item => { + let message = item; + if (typeof item !== "string") { + message = item.item; + } // Ignore the internal SendBrowserInfoOnUserInit message (Only occurs when running tests in a browser) if (message.indexOf("AI (Internal): 72 ") == -1) { acceptedItems ++; diff --git a/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts b/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts index ddee291c9..e2681e67c 100644 --- a/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts +++ b/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts @@ -2,8 +2,8 @@ import { AITestClass, Assert } from "@microsoft/ai-test-framework"; import * as pako from "pako"; export class AISKULightSizeCheck extends AITestClass { - private readonly MAX_RAW_SIZE = 87; - private readonly MAX_BUNDLE_SIZE = 87; + 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 rawFilePath = "../dist/es5/applicationinsights-web-basic.min.js"; 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 dafa3ac01..9cf6d2c13 100644 --- a/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts +++ b/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts @@ -1,28 +1,28 @@ import { AITestClass } from "@microsoft/ai-test-framework"; import { Sender } from "../../../src/Sender"; -import { IOfflineListener, createOfflineListener } from "@microsoft/applicationinsights-common"; +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, isFetchSupported, TransportType, getWindow, getGlobal} from "@microsoft/applicationinsights-core-js"; +import { ITelemetryItem, AppInsightsCore, ITelemetryPlugin, DiagnosticLogger, NotificationManager, SendRequestReason, _eInternalMessageId, safeGetLogger, isString, isArray, arrForEach, isBeaconsSupported, IXHROverride, IPayloadData,TransportType, getWindow } from "@microsoft/applicationinsights-core-js"; import { ArraySendBuffer, SessionStorageSendBuffer } from "../../../src/SendBuffer"; -import { ISenderConfig } from "../../../src/Interfaces"; +import { IInternalStorageItem, ISenderConfig } from "../../../src/Interfaces"; -const BUFFER_KEY = "AI_buffer"; -const SENT_BUFFER_KEY = "AI_sentBuffer"; +const BUFFER_KEY = "AI_buffer_1"; +const SENT_BUFFER_KEY = "AI_sentBuffer_1"; export class SenderTests extends AITestClass { private _sender: Sender; private _instrumentationKey = 'iKey'; private _offline: IOfflineListener; - protected _getBuffer(key: string, logger: DiagnosticLogger, namePrefix?: string): string[] { + protected _getBuffer(key: string, logger: DiagnosticLogger, namePrefix?: string): IInternalStorageItem[] { let prefixedKey = key; try { prefixedKey = namePrefix ? namePrefix + "_" + prefixedKey : prefixedKey; const bufferJson = utlGetSessionStorage(logger, prefixedKey); if (bufferJson) { - let buffer: string[] = JSON.parse(bufferJson); + let buffer: IInternalStorageItem[] = JSON.parse(bufferJson); if (isString(buffer)) { buffer = JSON.parse(buffer as any); } @@ -122,6 +122,7 @@ export class SenderTests extends AITestClass { QUnit.assert.equal(undefined, defaultSenderConfig.httpXHROverride, "Channel default httpXHROverride config is set"); QUnit.assert.equal(false, defaultSenderConfig.alwaysUseXhrOverride, "Channel default alwaysUseXhrOverride config is set"); QUnit.assert.equal(true, defaultSenderConfig.disableSendBeaconSplit, "Channel default disableSendBeaconSplit config is set"); + QUnit.assert.equal(10, defaultSenderConfig.maxRetryCnt, "Channel default maxRetryCnt config is set"); //check dynamic config core.config.extensionConfig = core.config.extensionConfig? core.config.extensionConfig : {}; @@ -182,7 +183,7 @@ export class SenderTests extends AITestClass { } } } - let testBatch: string[] = ["test", "test1"]; + let testBatch: IInternalStorageItem[] = [{item: "test", cnt: 0}, {item: "test1", cnt: 0}]; const telemetryItem: ITelemetryItem = { name: "fake item", iKey: "test", @@ -229,7 +230,7 @@ export class SenderTests extends AITestClass { this._sender.onunloadFlush(); QUnit.assert.deepEqual(2, sentPayloadData.length, "httpXHROverride should be called"); let data = sentPayloadData[1].payload.oriPayload; - payload = JSON.parse(data[0]); + payload = JSON.parse(data[0].item); QUnit.assert.deepEqual("test", payload.iKey, "httpXHROverride should send expected payload test1"); sync = sentPayloadData[1].sync; QUnit.assert.equal(true, sync, "Channel httpXHROverride sync is called with true during send test2 (sender interface should be opposite with the sender)"); @@ -237,6 +238,72 @@ export class SenderTests extends AITestClass { } }); + this.testCase({ + name: "Channel Config MaxRetry Count: payload exceeds max retry count should not be sent again", + useFakeTimers: true, + test: () => { + let core = new AppInsightsCore(); + + let coreConfig = { + instrumentationKey: "abc", + extensionConfig: { + [this._sender.identifier]: { + //httpXHROverride: xhrOverride, + //alwaysUseXhrOverride: true, + maxRetryCnt: 1 + } + } + } + const telemetryItem: ITelemetryItem = { + name: "fake item", + iKey: "test", + baseType: "some type", + baseData: {} + }; + core.initialize(coreConfig, [this._sender]); + + let logger = new DiagnosticLogger({instrumentationKey: "abc"}); + core.logger = logger; + QUnit.assert.deepEqual(this._getBuffer(BUFFER_KEY, logger), [], "session storage buffer is empty"); + QUnit.assert.deepEqual(this._getBuffer(SENT_BUFFER_KEY, logger), [], "session storage sent buffer is empty"); + try { + this._sender.processTelemetry(telemetryItem); + QUnit.assert.deepEqual(this._getBuffer(BUFFER_KEY, logger).length, 1, "session storage buffer should have 1 item"); + QUnit.assert.deepEqual(this._getBuffer(SENT_BUFFER_KEY, logger), [], "session storage sent buffer is empty test1"); + } catch(e) { + QUnit.assert.ok(false, "Exception - " + e); + } + + // inital send, cnt = 0 + this._sender.flush(false); + QUnit.assert.deepEqual(this._getBuffer(BUFFER_KEY, logger), [], "session storage buffer is empty"); + QUnit.assert.deepEqual(this._getBuffer(SENT_BUFFER_KEY, logger).length, 1, "session storage sent buffer should have one event"); + + let requests = this._getXhrRequests(); + QUnit.assert.deepEqual(requests.length, 1, "should have only 1 requests"); + let request = requests[0]; + this.sendJsonResponse(request, {}, 500); + QUnit.assert.deepEqual(this._getBuffer(BUFFER_KEY, logger).length, 1, "session storage buffer should have one item test2"); + QUnit.assert.deepEqual(this._getBuffer(SENT_BUFFER_KEY, logger), [], "session storage sent buffer should not have one event test2"); + + QUnit.assert.deepEqual(this._getBuffer(BUFFER_KEY, logger)[0].cnt, 1, "session storage buffer should have item with retry cnt 1"); + + // retry 1, cnt = 1 + this._sender.flush(false); + QUnit.assert.deepEqual(this._getBuffer(BUFFER_KEY, logger), [], "session storage buffer is empty test4"); + QUnit.assert.deepEqual(this._getBuffer(SENT_BUFFER_KEY, logger).length, 1, "session storage sent buffer should have one event test4"); + + requests = this._getXhrRequests(); + QUnit.assert.deepEqual(requests.length, 2, "should have only 1 requests"); + request = requests[1]; + this.sendJsonResponse(request, {}, 500); + // items should not be added back + QUnit.assert.deepEqual(this._getBuffer(BUFFER_KEY, logger), [], "session storage buffer should not have one item test5"); + QUnit.assert.deepEqual(this._getBuffer(SENT_BUFFER_KEY, logger), [], "session storage sent buffer should not have one event test5"); + + } + }); + this.testCase({ name: "Channel Config: Invalid paylod Sender should not be sent", useFakeTimers: true, @@ -292,6 +359,91 @@ export class SenderTests extends AITestClass { } }); + this.testCase({ + name: "Channel Config: sessionStorage can get items from previous buffers", + useFakeTimers: true, + test: () => { + let core = new AppInsightsCore(); + let coreConfig = { + instrumentationKey: "b7170927-2d1c-44f1-acec-59f4e1751c13", + extensionConfig: { + [this._sender.identifier]: { + namePrefix: "test" + } + } + } + + let item1: ITelemetryItem = { + name: "fake item1", + iKey: "abc", + baseType: "some type", + baseData: {} + }; + + let item2: ITelemetryItem = { + name: "fake item2", + iKey: "abc", + baseType: "some type", + baseData: {} + }; + + let item3: ITelemetryItem = { + name: "fake item3", + iKey: "abc", + baseType: "some type", + baseData: {} + }; + + let item4: ITelemetryItem = { + name: "fake item4", + iKey: "abc", + baseType: "some type", + baseData: {} + }; + + let item5: ITelemetryItem = { + name: "fake item5", + iKey: "abc", + baseType: "some type", + baseData: {} + }; + + let items = [item1, item2]; + let sentItems = [item3]; + let prefixItems = [item4, item5]; + //mock previous items stored in previous buffer key + sessionStorage.setItem("AI_buffer",JSON.stringify(items)); + sessionStorage.setItem("AI_sentBuffer",JSON.stringify(sentItems)); + sessionStorage.setItem("test_AI_buffer",JSON.stringify(prefixItems)); + + let keys = utlGetSessionStorageKeys(); + QUnit.assert.deepEqual(keys.length, 3, "session buffer should contain only three keys"); + + let logger = new DiagnosticLogger({instrumentationKey: "abc"}); + core.logger = logger; + + core.initialize(coreConfig, [this._sender]); + QUnit.assert.equal(true, this._sender._senderConfig.enableSessionStorageBuffer, "Channel default enableSessionStorageBuffer config is set"); + QUnit.assert.equal(true, utlCanUseSessionStorage(), "SessionStorage should be able to use"); + QUnit.assert.deepEqual(this._getBuffer("test_" + BUFFER_KEY, logger).length, 5, "session storage buffer should contain all previous events"); + QUnit.assert.deepEqual(this._getBuffer("test_" + SENT_BUFFER_KEY, logger), [], "session storage sent buffer is empty"); + + let previousItems = this._sender._buffer.getItems(); + + QUnit.assert.deepEqual(previousItems.length, 5, "buffer should contain 5 previous items"); + + keys = utlGetSessionStorageKeys(); + QUnit.assert.deepEqual(keys.length, 2, "session buffer should contain only two keys"); + QUnit.assert.ok(keys.indexOf("test_" + BUFFER_KEY) > -1, "session buffer key contain buffer key"); + QUnit.assert.ok(keys.indexOf("test_" + SENT_BUFFER_KEY) > -1, "session buffer key contain sent buffer key"); + + utlRemoveSessionStorage(logger, "test_" + BUFFER_KEY); + utlRemoveSessionStorage(logger, "test_" + SENT_BUFFER_KEY); + + + } + }); + this.testCase({ name: "Channel Config: sessionStorage change from true to false can be handled correctly", @@ -334,7 +486,7 @@ export class SenderTests extends AITestClass { QUnit.assert.equal(false, loggerSpy.calledOnce, "The send has not yet been triggered"); let payload = this._getBuffer(BUFFER_KEY, logger); QUnit.assert.equal(payload.length, 1, "payload length is equal to one"); - QUnit.assert.ok(payload[0].indexOf("some type") > 0, "payload is saved to session storage"); + QUnit.assert.ok(payload[0].item.indexOf("some type") > 0, "payload is saved to session storage"); let sentPayload = this._getBuffer(SENT_BUFFER_KEY, logger); QUnit.assert.deepEqual([], sentPayload, "sent payload is empty"); QUnit.assert.equal(this._sender._buffer.getItems().length, 1, "buffer length shoule be one"); @@ -346,7 +498,7 @@ export class SenderTests extends AITestClass { this.clock.tick(1); QUnit.assert.equal(false, this._sender._senderConfig.enableSessionStorageBuffer, "Channel enableSessionStorageBuffer config is disabled"); QUnit.assert.equal(this._sender._buffer.getItems().length, 1, "session storage buffer is transferred"); - QUnit.assert.ok(this._sender._buffer.getItems()[0].indexOf("some type") > 1, "in memory storage buffer is set"); + QUnit.assert.ok(this._sender._buffer.getItems()[0].item.indexOf("some type") > 1, "in memory storage buffer is set"); this.clock.tick(15000); QUnit.assert.equal(true, loggerSpy.calledOnce, "The send has been triggered"); @@ -432,7 +584,7 @@ export class SenderTests extends AITestClass { let payload = this._getBuffer(BUFFER_KEY, logger); QUnit.assert.equal(payload.length, 1, "payload length is equal to one"); - QUnit.assert.ok(payload[0].indexOf("some type") > 0, "payload is saved to session storage"); + QUnit.assert.ok(payload[0].item.indexOf("some type") > 0, "payload is saved to session storage"); QUnit.assert.equal(this._sender._buffer.getItems().length, 1, "buffer length shoule be one"); let sentPayload = this._getBuffer(SENT_BUFFER_KEY, logger); QUnit.assert.deepEqual([], sentPayload, "sent payload is empty"); @@ -481,7 +633,7 @@ export class SenderTests extends AITestClass { QUnit.assert.equal(this._sender._buffer.getItems().length, 1, "session storage buffer is set"); let payload = this._getBuffer(BUFFER_KEY, logger); QUnit.assert.equal(payload.length, 1, "payload length is equal to one"); - QUnit.assert.ok(payload[0].indexOf("some type") > 0, "payload is saved to session storage"); + QUnit.assert.ok(payload[0].item.indexOf("some type") > 0, "payload is saved to session storage"); QUnit.assert.deepEqual(this._getBuffer(BUFFER_KEY, logger, prefixName), [], "session storage buffer with prefix is empty"); @@ -495,7 +647,7 @@ export class SenderTests extends AITestClass { QUnit.assert.deepEqual(this._getBuffer(SENT_BUFFER_KEY, logger), [], "session storage sent buffer is empty"); payload = this._getBuffer(BUFFER_KEY, logger, prefixName); QUnit.assert.equal(payload.length, 1, "payload length is equal to one"); - QUnit.assert.ok(payload[0].indexOf("some type") > 0, "payload is saved to session storage with prefix"); + QUnit.assert.ok(payload[0].item.indexOf("some type") > 0, "payload is saved to session storage with prefix"); QUnit.assert.equal(this._sender._buffer.getItems().length, 1, "new session storage buffer is set"); utlSetSessionStorage(logger, BUFFER_KEY,JSON.stringify([])); @@ -544,7 +696,7 @@ export class SenderTests extends AITestClass { QUnit.assert.equal(false, loggerSpy.calledOnce, "The send has not yet been triggered"); let payload = this._getBuffer(BUFFER_KEY, logger); QUnit.assert.equal(payload.length, 1, "payload length is equal to one"); - QUnit.assert.ok(payload[0].indexOf("some type") > 0, "payload is saved to session storage"); + QUnit.assert.ok(payload[0].item.indexOf("some type") > 0, "payload is saved to session storage"); // change endpointUrl core.config.extensionConfig = core.config.extensionConfig? core.config.extensionConfig : {}; @@ -555,7 +707,7 @@ export class SenderTests extends AITestClass { payload = this._sender._buffer.getItems(); QUnit.assert.deepEqual(payload.length, 1, "buffer is not changed"); payload = this._getBuffer(BUFFER_KEY, logger); - QUnit.assert.ok(payload[0].indexOf("some type") > 0, "payload is not changed"); + QUnit.assert.ok(payload[0].item.indexOf("some type") > 0, "payload is not changed"); utlSetSessionStorage(logger, BUFFER_KEY,JSON.stringify([])); } @@ -589,18 +741,59 @@ export class SenderTests extends AITestClass { let arrBufferCopy= arrBuffer.createNew(logger, config, false); // set to false to make sure it is array buffer QUnit.assert.deepEqual(arrBufferCopy.getItems(), [], "payload should be empty"); - let payload = ["payload1", "payload2", "payload3", "payload4", "payload5", "payload6"]; + //let payload = [{"payload1"}, "payload2", "payload3", "payload4", "payload5", "payload6"]; + let payload = [{item: "payload1", cnt: 0}, {item: "payload2", cnt: 0}, {item: "payload3", cnt: 0}, {item: "payload4", cnt: 0}, {item: "payload5", cnt: 0}, {item: "payload6", cnt: 0} ]; arrForEach(payload, (val) =>{ arrBuffer.enqueue(val); }); arrBufferCopy = arrBuffer.createNew(logger, config, false); QUnit.assert.deepEqual(payload, arrBufferCopy.getItems(), "payload should be same"); - arrBuffer.enqueue("payload"); + arrBuffer.enqueue({item:"payload", cnt: 0}); QUnit.assert.deepEqual(arrBuffer.getItems().length, 7, "arrBuffer length"); QUnit.assert.deepEqual(arrBufferCopy.getItems().length, 6, "copy is deep copy"); } }); + this.testCase({ + name: "ArraySendBuffer Max Count: item exceeds max cnt should not be added again", + test: () => { + let config = { + endpointUrl: "https//: test", + emitLineDelimitedJson: false, + maxBatchInterval: 15000, + maxBatchSizeInBytes: 102400, + disableTelemetry: false, + enableSessionStorageBuffer: true, + isRetryDisabled: false, + isBeaconApiDisabled:true, + disableXhr: false, + onunloadDisableFetch: false, + onunloadDisableBeacon: false, + instrumentationKey:"key", + namePrefix: "", + samplingPercentage: 100, + customHeaders: [{header:"header",value:"val" }], + convertUndefined: "", + eventsLimitInMem: 10000, + maxRetryCnt: 1 + } as ISenderConfig; + let logger = new DiagnosticLogger({instrumentationKey: "abc"}); + + let arrBuffer = new ArraySendBuffer(logger, config); + QUnit.assert.deepEqual(arrBuffer.getItems(), [], "payload should be empty"); + let payload1 = {item: "payload1", cnt: 1}; + arrBuffer.enqueue(payload1); + QUnit.assert.deepEqual(arrBuffer.getItems().length, 1, "buffer should have one item"); + + let payload2 = {item: "payload2", cnt: 2}; + arrBuffer.enqueue(payload2); + QUnit.assert.deepEqual(arrBuffer.getItems().length, 1, "payload exceeds max cnt should not be added again"); + + utlSetSessionStorage(logger, BUFFER_KEY,JSON.stringify([])) + } + }); + + this.testCase({ name: "ArraySendBuffer createNew: function createNew() can return expected sessionStorage buffer", test: () => { @@ -629,14 +822,15 @@ export class SenderTests extends AITestClass { let sessionBuffer = arrBuffer.createNew(logger, config, true); // set to false to make sure it is session storage buffer QUnit.assert.deepEqual(sessionBuffer.getItems(), [], "payload should be empty"); - let payload = ["payload1", "payload2", "payload3", "payload4", "payload5", "payload6"]; + //let payload = ["payload1", "payload2", "payload3", "payload4", "payload5", "payload6"]; + let payload = [{item: "payload1", cnt: 0}, {item: "payload2", cnt: 0}, {item: "payload3", cnt: 0}, {item: "payload4", cnt: 0}, {item: "payload5", cnt: 0}, {item: "payload6", cnt: 0} ]; arrForEach(payload, (val) =>{ arrBuffer.enqueue(val); }); sessionBuffer = arrBuffer.createNew(logger, config, true); QUnit.assert.deepEqual(sessionBuffer.getItems(), payload, "payload should be same"); QUnit.assert.deepEqual(this._getBuffer(BUFFER_KEY, logger), payload, "session storage buffer is set"); - arrBuffer.enqueue("payload"); + arrBuffer.enqueue({item: "payload", cnt: 0}); QUnit.assert.deepEqual(arrBuffer.getItems().length, 7, "arrBuffer length"); QUnit.assert.deepEqual(sessionBuffer.getItems().length, 6, "copy is deep copy"); @@ -672,22 +866,62 @@ export class SenderTests extends AITestClass { let arrBuffer = sessionBuffer.createNew(logger, config, false); QUnit.assert.deepEqual(arrBuffer.getItems(), [], "payload should be empty"); - let payload = ["payload1", "payload2", "payload3", "payload4", "payload5", "payload6"]; + //let payload = ["payload1", "payload2", "payload3", "payload4", "payload5", "payload6"]; + let payload = [{item: "payload1", cnt: 0}, {item: "payload2", cnt: 0}, {item: "payload3", cnt: 0}, {item: "payload4", cnt: 0}, {item: "payload5", cnt: 0}, {item: "payload6", cnt: 0} ]; arrForEach(payload, (val) =>{ sessionBuffer.enqueue(val); }); QUnit.assert.deepEqual(this._getBuffer(BUFFER_KEY, logger), payload, "session storage buffer is set"); arrBuffer = sessionBuffer.createNew(logger, config, false); QUnit.assert.deepEqual(arrBuffer.getItems(), payload, "payload should be same"); - sessionBuffer.enqueue("payload"); + sessionBuffer.enqueue({item: "payload", cnt: 0}); QUnit.assert.deepEqual(sessionBuffer.getItems().length, 1, "sessionBuffer length"); - QUnit.assert.deepEqual(this._getBuffer(BUFFER_KEY, logger), ["payload"], "session storage buffer is set"); + QUnit.assert.deepEqual(this._getBuffer(BUFFER_KEY, logger), [{item: "payload", cnt: 0}], "session storage buffer is set"); QUnit.assert.deepEqual(arrBuffer.getItems().length, 6, "copy is deep copy"); utlSetSessionStorage(logger, BUFFER_KEY,JSON.stringify([])); } }); + this.testCase({ + name: "SessionStorageSendBuffer Max Count: payload exceeds max retry cnt should not be added again", + test: () => { + let config = { + endpointUrl: "https//: test", + emitLineDelimitedJson: false, + maxBatchInterval: 15000, + maxBatchSizeInBytes: 102400, + disableTelemetry: false, + enableSessionStorageBuffer: true, + isRetryDisabled: false, + isBeaconApiDisabled:true, + disableXhr: false, + onunloadDisableFetch: false, + onunloadDisableBeacon: false, + instrumentationKey:"key", + namePrefix: "", + samplingPercentage: 100, + customHeaders: [{header:"header",value:"val" }], + convertUndefined: "", + eventsLimitInMem: 10000, + maxRetryCnt: 1 + } as ISenderConfig; + let logger = new DiagnosticLogger({instrumentationKey: "abc"}); + + let sessionBuffer = new SessionStorageSendBuffer(logger, config); + QUnit.assert.deepEqual(sessionBuffer.getItems(), [], "payload should be empty"); + let payload1 = {item: "payload1", cnt: 1}; + sessionBuffer.enqueue(payload1); + QUnit.assert.deepEqual(sessionBuffer.getItems().length, 1, "should have only one payload"); + + let payload2 = {item: "payload2", cnt: 2}; + sessionBuffer.enqueue(payload2); + QUnit.assert.deepEqual(sessionBuffer.getItems().length, 1, "should have only one payload"); + + utlSetSessionStorage(logger, BUFFER_KEY,JSON.stringify([])); + } + }); + this.testCase({ name: "SessionStorageSendBuffer createNew: function createNew() can return expected sessionStorage buffer with same prefix name", test: () => { @@ -717,8 +951,10 @@ export class SenderTests extends AITestClass { QUnit.assert.deepEqual(sessionBufferCopy.getItems(), [], "payload should be empty"); QUnit.assert.deepEqual(this._getBuffer(BUFFER_KEY, logger), [], "session storage buffer should be empty"); - let payload = ["payload1", "payload2", "payload3", "payload4", "payload5", "payload6"]; - let sentPayload = ["sent1", "sent2","sent3","sent4"]; + //let payload = ["payload1", "payload2", "payload3", "payload4", "payload5", "payload6"]; + let payload = [{item: "payload1", cnt: 0}, {item: "payload2", cnt: 0}, {item: "payload3", cnt: 0}, {item: "payload4", cnt: 0}, {item: "payload5", cnt: 0}, {item: "payload6", cnt: 0} ]; + let sentPayload = [{item: "sent1", cnt: 0}, {item: "sent2", cnt: 0}, {item: "sent3", cnt: 0}, {item: "sent4", cnt: 0} ]; + //let sentPayload = ["sent1", "sent2","sent3","sent4"]; arrForEach(payload, (val) =>{ sessionBuffer.enqueue(val); }); @@ -772,8 +1008,10 @@ export class SenderTests extends AITestClass { QUnit.assert.deepEqual(this._getBuffer(SENT_BUFFER_KEY, logger), [], "session storage sent buffer should be empty"); QUnit.assert.deepEqual(this._getBuffer(SENT_BUFFER_KEY, logger, prefix), [], "session storage sent buffer with prefix should be empty"); - let payload = ["payload1", "payload2", "payload3", "payload4", "payload5", "payload6"]; - let sentPayload = ["sent1", "sent2","sent3","sent4"]; + // let payload = ["payload1", "payload2", "payload3", "payload4", "payload5", "payload6"]; + // let sentPayload = ["sent1", "sent2","sent3","sent4"]; + let payload = [{item: "payload1", cnt: 0}, {item: "payload2", cnt: 0}, {item: "payload3", cnt: 0}, {item: "payload4", cnt: 0}, {item: "payload5", cnt: 0}, {item: "payload6", cnt: 0} ]; + let sentPayload = [{item: "sent1", cnt: 0}, {item: "sent2", cnt: 0}, {item: "sent3", cnt: 0}, {item: "sent4", cnt: 0} ]; arrForEach(payload, (val) =>{ sessionBuffer.enqueue(val); }); @@ -1004,7 +1242,7 @@ export class SenderTests extends AITestClass { try { this._sender.processTelemetry(telemetryItem, null); let buffer = this._sender._buffer.getItems(); - let payload = JSON.parse(buffer[buffer.length-1]); + let payload = JSON.parse(buffer[buffer.length-1].item); var actualIkey = payload.iKey; } catch(e) { QUnit.assert.ok(false, "Exception - " + e); @@ -1692,9 +1930,9 @@ export class SenderTests extends AITestClass { try { sender.processTelemetry(telemetryItem, null); QUnit.assert.equal(1, buffer.getItems().length, "sender buffer should have one payload"); - let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + let bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 1, "sender buffer should have one payload"); - let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + let sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(0, sentItems.length, "sent buffer should have zero payload"); sender.onunloadFlush(); } catch(e) { @@ -1709,15 +1947,15 @@ export class SenderTests extends AITestClass { QUnit.assert.equal(false, fetchstub.called, "fetch sender is not called"); QUnit.assert.equal(0, buffer.getItems().length, "sender buffer should not have one payload"); QUnit.assert.equal(0, buffer.count(), "sender buffer should not have any payload"); - let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + let bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 0, "sender buffer should be clear payload"); - let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + let sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(1, sentItems.length, "sent buffer should have only one payload"); this.sendJsonResponse(xhrRequest, {}, 200); - bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 0, "sender buffer should be clear payload"); - sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(0, sentItems.length, "sent buffer should have no payload"); (window as any).XMLHttpRequest = fakeXMLHttpRequest; @@ -1779,9 +2017,9 @@ export class SenderTests extends AITestClass { try { sender.processTelemetry(telemetryItem, null); QUnit.assert.equal(1, buffer.getItems().length, "sender buffer should have one payload"); - let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + let bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 1, "sender buffer should have one payload"); - let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + let sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(0, sentItems.length, "sent buffer should have zero payload"); sender.onunloadFlush(); } catch(e) { @@ -1793,9 +2031,9 @@ export class SenderTests extends AITestClass { QUnit.assert.equal(false, fetchstub.called, "fetch sender is not called"); QUnit.assert.equal(0, buffer.getItems().length, "sender buffer should not have one payload"); QUnit.assert.equal(0, buffer.count(), "sender buffer should not have any payload"); - let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + let bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 0, "sender buffer should be clear payload"); - let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + let sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(0, sentItems.length, "sent buffer should not have one payload"); (window as any).XMLHttpRequest = fakeXMLHttpRequest; @@ -1870,9 +2108,9 @@ export class SenderTests extends AITestClass { sender.processTelemetry(telemetryItem, null); sender.processTelemetry(telemetryItem1, null); QUnit.assert.equal(2, buffer.getItems().length, "sender buffer should have one payload"); - let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + let bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 2, "sender buffer should have one payload"); - let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + let sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(0, sentItems.length, "sent buffer should have zero payload"); sender.onunloadFlush(); } catch(e) { @@ -1886,16 +2124,16 @@ export class SenderTests extends AITestClass { QUnit.assert.equal(false, fetchstub.called, "fetch sender is not called"); QUnit.assert.equal(0, buffer.getItems().length, "sender buffer should not have one payload"); QUnit.assert.equal(0, buffer.count(), "sender buffer should not have any payload"); - let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + let bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 0, "sender buffer should be clear payload"); - let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + let sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(1, sentItems.length, "sent buffer should have one payload"); - QUnit.assert.ok(sentItems[0].indexOf("iKey1") >= 0, "sent buffer should have ikey1 payload"); + QUnit.assert.ok(sentItems[0].item.indexOf("iKey1") >= 0, "sent buffer should have ikey1 payload"); this.sendJsonResponse(xhrRequest, {}, 200); - bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 0, "sender buffer should have no payload test1"); - sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(0, sentItems.length, "sent buffer should have zero payload test1"); (window as any).XMLHttpRequest = fakeXMLHttpRequest; @@ -1957,9 +2195,9 @@ export class SenderTests extends AITestClass { try { sender.processTelemetry(telemetryItem, null); QUnit.assert.equal(1, buffer.getItems().length, "sender buffer should have one payload"); - let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + let bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 1, "sender buffer should have one payload"); - let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + let sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(0, sentItems.length, "sent buffer should have zero payload"); sender.onunloadFlush(); } catch(e) { @@ -1972,16 +2210,16 @@ export class SenderTests extends AITestClass { QUnit.assert.equal(true, fetchstub.calledOnce, "fetch sender is called once"); QUnit.assert.equal(0, buffer.getItems().length, "sender buffer should not have one payload"); QUnit.assert.equal(0, buffer.count(), "sender buffer should not have any payload"); - let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + let bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 0, "sender buffer should be clear payload"); - let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + let sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(0, sentItems.length, "sent buffer should have one payload test1"); let setItemCalled = 0; let args = sessionSpy.args; let itemCount = 0; args.forEach((arg) => { - if (arg && arg[0] === "AI_sentBuffer") { + if (arg && arg[0] === SENT_BUFFER_KEY) { let data = JSON.parse(arg[1]); let cnt = data.length; if(data && cnt) { @@ -2056,9 +2294,9 @@ export class SenderTests extends AITestClass { try { sender.processTelemetry(telemetryItem, null); QUnit.assert.equal(1, buffer.getItems().length, "sender buffer should have one payload"); - let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + let bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 1, "sender buffer should have one payload"); - let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + let sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(0, sentItems.length, "sent buffer should have zero payload"); sender.onunloadFlush(); } catch(e) { @@ -2071,9 +2309,9 @@ export class SenderTests extends AITestClass { QUnit.assert.equal(0, this._getXhrRequests().length, "xhr sender should not be called"); QUnit.assert.equal(0, buffer.getItems().length, "sender buffer should not have one payload"); QUnit.assert.equal(0, buffer.count(), "sender buffer should not have any payload"); - let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + let bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 0, "sender buffer should be clear payload"); - let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + let sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(0, sentItems.length, "sent buffer should not have one payload"); @@ -2151,9 +2389,9 @@ export class SenderTests extends AITestClass { sender.processTelemetry(telemetryItem, null); sender.processTelemetry(telemetryItem1, null); QUnit.assert.equal(2, buffer.getItems().length, "sender buffer should have two payload"); - let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + let bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 2, "sender buffer should have two payload"); - let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + let sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(0, sentItems.length, "sent buffer should have zero payload"); sender.onunloadFlush(); } catch(e) { @@ -2165,16 +2403,16 @@ export class SenderTests extends AITestClass { QUnit.assert.equal(true, fetchstub.called, "fetch sender is called"); QUnit.assert.equal(0, buffer.getItems().length, "sender buffer should not have one payload"); QUnit.assert.equal(0, buffer.count(), "sender buffer should not have any payload"); - let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + let bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 0, "sender buffer should be clear payload"); - let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + let sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(0, sentItems.length, "sent buffer should have no payload left"); let setItemCalled = 0; let itemCount = 0; let args = sessionSpy.args; args.forEach((arg) => { - if (arg && arg[0] === "AI_sentBuffer") { + if (arg && arg[0] === SENT_BUFFER_KEY) { let data = JSON.parse(arg[1]); let cnt = data.length; if(data && cnt) { @@ -2257,9 +2495,9 @@ export class SenderTests extends AITestClass { sender.processTelemetry(telemetryItem, null); sender.processTelemetry(telemetryItem1, null); QUnit.assert.equal(2, buffer.getItems().length, "sender buffer should have two payload"); - let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + let bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 2, "sender buffer should have two payload"); - let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + let sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(0, sentItems.length, "sent buffer should have zero payload"); sender.onunloadFlush(); } catch(e) { @@ -2271,9 +2509,9 @@ export class SenderTests extends AITestClass { QUnit.assert.equal(false, fetchstub.called, "fetch sender is called"); QUnit.assert.equal(0, buffer.getItems().length, "sender buffer should not have one payload"); QUnit.assert.equal(0, buffer.count(), "sender buffer should not have any payload"); - let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any); + let bufferItems = JSON.parse(sessionStorage.getItem(BUFFER_KEY) as any); QUnit.assert.equal(bufferItems.length, 0, "sender buffer should be clear payload"); - let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any); + let sentItems = JSON.parse(sessionStorage.getItem(SENT_BUFFER_KEY) as any); QUnit.assert.equal(2, sentItems.length, "sent buffer should not have two payload"); (window as any).XMLHttpRequest = fakeXMLHttpRequest; diff --git a/channels/applicationinsights-channel-js/src/Interfaces.ts b/channels/applicationinsights-channel-js/src/Interfaces.ts index 771635c12..7d6a43142 100644 --- a/channels/applicationinsights-channel-js/src/Interfaces.ts +++ b/channels/applicationinsights-channel-js/src/Interfaces.ts @@ -1,6 +1,22 @@ import { IStorageBuffer } from "@microsoft/applicationinsights-common"; import { IXHROverride } from "@microsoft/applicationinsights-core-js"; +/** + * Internal interface for sendBuffer, do not export it + * @internal + * @since 3.1.3 + */ +export interface IInternalStorageItem { + /** + * serialized telemetry to be stored. + */ + item: string; + /** + * total retry count + */ + cnt?: number; +} + export interface ISenderConfig { /** * The url to which payloads will be sent @@ -141,6 +157,15 @@ export interface ISenderConfig { * @since 3.1.1 */ retryCodes?: number[]; + + /** + * (Optional) The specific max retry count for each telemetry item. + * Default: 10 + * if it is set to 0, means no retry allowed + * if it is set to undefined, means no limit for retry times + * @since 3.2.0 + */ + maxRetryCnt?: number; } export interface IBackendResponse { diff --git a/channels/applicationinsights-channel-js/src/SendBuffer.ts b/channels/applicationinsights-channel-js/src/SendBuffer.ts index 9080f604d..03e244e92 100644 --- a/channels/applicationinsights-channel-js/src/SendBuffer.ts +++ b/channels/applicationinsights-channel-js/src/SendBuffer.ts @@ -1,16 +1,21 @@ import dynamicProto from "@microsoft/dynamicproto-js"; -import { utlGetSessionStorage, utlSetSessionStorage } from "@microsoft/applicationinsights-common"; +import { utlGetSessionStorage, utlRemoveSessionStorage, utlSetSessionStorage } from "@microsoft/applicationinsights-common"; import { IDiagnosticLogger, _eInternalMessageId, _throwInternal, arrForEach, arrIndexOf, dumpObj, eLoggingSeverity, getExceptionName, getJSON, - isArray, isFunction, isString + isArray, isFunction, isNullOrUndefined, isString } from "@microsoft/applicationinsights-core-js"; -import { ISenderConfig } from "./Interfaces"; +import { IInternalStorageItem, ISenderConfig } from "./Interfaces"; +/** + * Before 3.1.2, payload only allow string + * After 3.2.0, IInternalStorageItem is accepted + */ export interface ISendBuffer { + /** * Enqueue the payload */ - enqueue: (payload: string) => void; + enqueue: (payload: IInternalStorageItem) => void; /** * Returns the number of elements in the buffer @@ -30,23 +35,23 @@ export interface ISendBuffer { /** * Returns items stored in the buffer */ - getItems: () => string[]; + getItems: () => IInternalStorageItem[]; /** * Build a batch of all elements in the payload array */ - batchPayloads: (payload: string[]) => string; + batchPayloads: (payload: IInternalStorageItem[]) => string; /** * Moves items to the SENT_BUFFER. * The buffer holds items which were sent, but we haven't received any response from the backend yet. */ - markAsSent: (payload: string[]) => void; + markAsSent: (payload: IInternalStorageItem[]) => void; /** * Removes items from the SENT_BUFFER. Should be called on successful response from the backend. */ - clearSent: (payload: string[]) => void; + clearSent: (payload: IInternalStorageItem[]) => void; /** * Copy current buffer items to a new buffer. @@ -59,25 +64,26 @@ export interface ISendBuffer { abstract class BaseSendBuffer { - protected _get: () => string[]; - protected _set: (buffer: string[]) => string[]; + protected _get: () => IInternalStorageItem[]; + protected _set: (buffer: IInternalStorageItem[]) => IInternalStorageItem[]; constructor(logger: IDiagnosticLogger, config: ISenderConfig) { - let _buffer: string[] = []; + let _buffer: IInternalStorageItem[] = []; let _bufferFullMessageSent = false; + let _maxRetryCnt = config.maxRetryCnt; this._get = () => { return _buffer; }; - this._set = (buffer: string[]) => { + this._set = (buffer: IInternalStorageItem[]) => { _buffer = buffer; return _buffer; }; dynamicProto(BaseSendBuffer, this, (_self) => { - _self.enqueue = (payload: string) => { + _self.enqueue = (payload: IInternalStorageItem) => { if (_self.count() >= config.eventsLimitInMem) { // sent internal log only once per page view if (!_bufferFullMessageSent) { @@ -91,8 +97,20 @@ abstract class BaseSendBuffer { return; } - + + payload.cnt = payload.cnt || 0; + // max retry is defined, and max retry is reached, do not add the payload to buffer + if (!isNullOrUndefined(_maxRetryCnt)) { + if (payload.cnt > _maxRetryCnt) { + // TODO: add log here on dropping payloads + return; + } + } _buffer.push(payload); + + return; + + }; _self.count = (): number => { @@ -102,7 +120,7 @@ abstract class BaseSendBuffer { _self.size = (): number => { let size = _buffer.length; for (let lp = 0; lp < _buffer.length; lp++) { - size += _buffer[lp].length; + size += (_buffer[lp].item).length; } if (!config.emitLineDelimitedJson) { @@ -117,15 +135,19 @@ abstract class BaseSendBuffer { _bufferFullMessageSent = false; }; - _self.getItems = (): string[] => { + _self.getItems = (): IInternalStorageItem[] => { return _buffer.slice(0) }; - _self.batchPayloads = (payload: string[]): string => { - if (payload && payload.length > 0) { + _self.batchPayloads = (payloads: IInternalStorageItem[]): string => { + if (payloads && payloads.length > 0) { + let payloadStr: string[] = []; + arrForEach(payloads, (payload) => { + payloadStr.push(payload.item); + }) const batch = config.emitLineDelimitedJson ? - payload.join("\n") : - "[" + payload.join(",") + "]"; + payloadStr.join("\n") : + "[" + payloadStr.join(",") + "]"; return batch; } @@ -146,7 +168,7 @@ abstract class BaseSendBuffer { }); } - public enqueue(payload: string) { + public enqueue(payload: IInternalStorageItem) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } @@ -164,12 +186,12 @@ abstract class BaseSendBuffer { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } - public getItems(): string[] { + public getItems(): IInternalStorageItem[] { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging return null; } - public batchPayloads(payload: string[]): string { + public batchPayloads(payload: IInternalStorageItem[]): string { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging return null; } @@ -190,31 +212,34 @@ export class ArraySendBuffer extends BaseSendBuffer implements ISendBuffer { dynamicProto(ArraySendBuffer, this, (_self, _base) => { - _self.markAsSent = (payload: string[]) => { + _self.markAsSent = (payload: IInternalStorageItem[]) => { _base.clear(); }; - _self.clearSent = (payload: string[]) => { + _self.clearSent = (payload: IInternalStorageItem[]) => { // not supported }; }); } - public markAsSent(payload: string[]) { + public markAsSent(payload: IInternalStorageItem[]) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } - public clearSent(payload: string[]) { + public clearSent(payload: IInternalStorageItem[]) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } } +const PREVIOUS_KEYS: string[] = ["AI_buffer", "AI_sentBuffer"]; + /* * Session storage buffer holds a copy of all unsent items in the browser session storage. */ export class SessionStorageSendBuffer extends BaseSendBuffer implements ISendBuffer { - static BUFFER_KEY = "AI_buffer"; - static SENT_BUFFER_KEY = "AI_sentBuffer"; + static VERSION = "_1"; + static BUFFER_KEY = "AI_buffer"+ this.VERSION; + static SENT_BUFFER_KEY = "AI_sentBuffer" + this.VERSION; // Maximum number of payloads stored in the buffer. If the buffer is full, new elements will be dropped. static MAX_BUFFER_SIZE = 2000; @@ -224,22 +249,27 @@ export class SessionStorageSendBuffer extends BaseSendBuffer implements ISendBuf let _bufferFullMessageSent = false; //Note: should not use config.namePrefix directly, because it will always refers to the latest namePrefix let _namePrefix = config?.namePrefix; + // TODO: add remove buffer override as well const { getItem, setItem } = config.bufferOverride || { getItem: utlGetSessionStorage, setItem: utlSetSessionStorage }; + let _maxRetryCnt = config.maxRetryCnt; dynamicProto(SessionStorageSendBuffer, this, (_self, _base) => { const bufferItems = _getBuffer(SessionStorageSendBuffer.BUFFER_KEY); - const notDeliveredItems = _getBuffer(SessionStorageSendBuffer.SENT_BUFFER_KEY); + const itemsInSentBuffer = _getBuffer(SessionStorageSendBuffer.SENT_BUFFER_KEY); + let previousItems = _getPreviousEvents(); + const notDeliveredItems = itemsInSentBuffer.concat(previousItems); let buffer = _self._set(bufferItems.concat(notDeliveredItems)); - + // If the buffer has too many items, drop items from the end. if (buffer.length > SessionStorageSendBuffer.MAX_BUFFER_SIZE) { buffer.length = SessionStorageSendBuffer.MAX_BUFFER_SIZE; } _setBuffer(SessionStorageSendBuffer.SENT_BUFFER_KEY, []); _setBuffer(SessionStorageSendBuffer.BUFFER_KEY, buffer); + - _self.enqueue = (payload: string) => { + _self.enqueue = (payload: IInternalStorageItem) => { if (_self.count() >= SessionStorageSendBuffer.MAX_BUFFER_SIZE) { // sent internal log only once per page view if (!_bufferFullMessageSent) { @@ -253,6 +283,14 @@ export class SessionStorageSendBuffer extends BaseSendBuffer implements ISendBuf return; } + payload.cnt = payload.cnt || 0; + // max retry is defined, and max retry is reached, do not add the payload to buffer + if (!isNullOrUndefined(_maxRetryCnt)) { + if (payload.cnt > _maxRetryCnt) { + // TODO: add log here on dropping payloads + return; + } + } _base.enqueue(payload); _setBuffer(SessionStorageSendBuffer.BUFFER_KEY, _self._get()); @@ -266,7 +304,7 @@ export class SessionStorageSendBuffer extends BaseSendBuffer implements ISendBuf _bufferFullMessageSent = false; }; - _self.markAsSent = (payload: string[]) => { + _self.markAsSent = (payload: IInternalStorageItem[]) => { _setBuffer(SessionStorageSendBuffer.BUFFER_KEY, _self._set(_removePayloadsFromBuffer(payload, _self._get()))); @@ -290,7 +328,7 @@ export class SessionStorageSendBuffer extends BaseSendBuffer implements ISendBuf } }; - _self.clearSent = (payload: string[]) => { + _self.clearSent = (payload: IInternalStorageItem[]) => { let sentElements = _getBuffer(SessionStorageSendBuffer.SENT_BUFFER_KEY); sentElements = _removePayloadsFromBuffer(payload, sentElements); @@ -317,10 +355,14 @@ export class SessionStorageSendBuffer extends BaseSendBuffer implements ISendBuf return newBuffer; } - function _removePayloadsFromBuffer(payloads: string[], buffer: string[]): string[] { - const remaining: string[] = []; + function _removePayloadsFromBuffer(payloads: IInternalStorageItem[], buffer: IInternalStorageItem[]): IInternalStorageItem[] { + const remaining: IInternalStorageItem[] = []; + let payloadStr: string[] = []; + arrForEach(payloads, (payload) => { + payloadStr.push(payload.item); + }); arrForEach(buffer, (value) => { - if (!isFunction(value) && arrIndexOf(payloads, value) === -1) { + if (!isFunction(value) && arrIndexOf(payloadStr, value.item) === -1) { remaining.push(value); } }); @@ -328,13 +370,17 @@ export class SessionStorageSendBuffer extends BaseSendBuffer implements ISendBuf return remaining; } - function _getBuffer(key: string): string[] { + function _getBuffer(key: string): IInternalStorageItem[] { let prefixedKey = key; + prefixedKey = _namePrefix ? _namePrefix + "_" + prefixedKey : prefixedKey; + return _getBufferBase(prefixedKey); + } + + function _getBufferBase(key: string): T[] { try { - prefixedKey = _namePrefix ? _namePrefix + "_" + prefixedKey : prefixedKey; - const bufferJson = getItem(logger, prefixedKey); + const bufferJson = getItem(logger, key); if (bufferJson) { - let buffer: string[] = getJSON().parse(bufferJson); + let buffer: T[] = getJSON().parse(bufferJson); if (isString(buffer)) { // When using some version prototype.js the stringify / parse cycle does not decode array's correctly buffer = getJSON().parse(buffer as any); @@ -347,14 +393,14 @@ export class SessionStorageSendBuffer extends BaseSendBuffer implements ISendBuf } catch (e) { _throwInternal(logger, eLoggingSeverity.CRITICAL, _eInternalMessageId.FailedToRestoreStorageBuffer, - " storage key: " + prefixedKey + ", " + getExceptionName(e), + " storage key: " + key + ", " + getExceptionName(e), { exception: dumpObj(e) }); } return []; } - function _setBuffer(key: string, buffer: string[]) { + function _setBuffer(key: string, buffer: IInternalStorageItem[]) { let prefixedKey = key; try { prefixedKey = _namePrefix ? _namePrefix + "_" + prefixedKey : prefixedKey; @@ -371,10 +417,61 @@ export class SessionStorageSendBuffer extends BaseSendBuffer implements ISendBuf { exception: dumpObj(e) }); } } + + // this removes buffer with prefix+key + function _getPreviousEvents() { + let items: IInternalStorageItem[] = []; + try { + arrForEach(PREVIOUS_KEYS, (key) => { + let events = _getItemsFromPreviousKey(key); + items = items.concat(events); + + // to make sure that we also transfer items from old prefixed + key buffer + if (_namePrefix) { + let prefixedKey = _namePrefix + "_" + key; + let prefixEvents = _getItemsFromPreviousKey(prefixedKey); + items = items.concat( prefixEvents); + } + }); + return items; + + + } catch(e) { + _throwInternal(logger, eLoggingSeverity.WARNING, + _eInternalMessageId.FailedToSetStorageBuffer, + "Transfer events from previous buffers: " + getExceptionName(e) + ". previous Buffer items can not be removed", + { exception: dumpObj(e) }); + + } + return []; + } + + // transform string[] to IInternalStorageItem[] + function _getItemsFromPreviousKey(key: string) { + try { + let items = _getBufferBase(key); + let transFormedItems: IInternalStorageItem[] = []; + arrForEach(items, (item) => { + let internalItem = { + item: item, + cnt: 0 // previous events will be default to 0 count + } as IInternalStorageItem; + transFormedItems.push(internalItem); + }); + // remove the session storage if we can add events back + utlRemoveSessionStorage(logger, key); + return transFormedItems; + + } catch (e) { + // eslint-disable-next-line no-empty + } + return []; + } + }); } - public enqueue(payload: string) { + public enqueue(payload: IInternalStorageItem) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } @@ -382,11 +479,11 @@ export class SessionStorageSendBuffer extends BaseSendBuffer implements ISendBuf // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } - public markAsSent(payload: string[]) { + public markAsSent(payload: IInternalStorageItem[]) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } - public clearSent(payload: string[]) { + public clearSent(payload: IInternalStorageItem[]) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } diff --git a/channels/applicationinsights-channel-js/src/Sender.ts b/channels/applicationinsights-channel-js/src/Sender.ts index 310237a78..a08fee86d 100644 --- a/channels/applicationinsights-channel-js/src/Sender.ts +++ b/channels/applicationinsights-channel-js/src/Sender.ts @@ -14,12 +14,12 @@ import { isFetchSupported, isNullOrUndefined, mergeEvtNamespace, objExtend, onConfigChange, parseResponse, prependTransports, runTargetUnload } from "@microsoft/applicationinsights-core-js"; import { IPromise } from "@nevware21/ts-async"; -import { ITimerHandler, isTruthy, objDeepFreeze, objDefine, scheduleTimeout } from "@nevware21/ts-utils"; +import { ITimerHandler, isNumber, isString, isTruthy, objDeepFreeze, objDefine, scheduleTimeout } from "@nevware21/ts-utils"; import { DependencyEnvelopeCreator, EventEnvelopeCreator, ExceptionEnvelopeCreator, MetricEnvelopeCreator, PageViewEnvelopeCreator, PageViewPerformanceEnvelopeCreator, TraceEnvelopeCreator } from "./EnvelopeCreator"; -import { ISenderConfig } from "./Interfaces"; +import { IInternalStorageItem, ISenderConfig } from "./Interfaces"; import { ArraySendBuffer, ISendBuffer, SessionStorageSendBuffer } from "./SendBuffer"; import { Serializer } from "./Serializer"; import { Sample } from "./TelemetryProcessors/Sample"; @@ -30,7 +30,8 @@ const EMPTY_STR = ""; const FetchSyncRequestSizeLimitBytes = 65000; // approx 64kb (the current Edge, Firefox and Chrome max limit) interface IInternalPayloadData extends IPayloadData { - oriPayload: string[]; + oriPayload: IInternalStorageItem[]; + retryCnt?: number; } @@ -73,7 +74,8 @@ const defaultAppInsightsChannelConfig: IConfigDefaults = objDeepF httpXHROverride: { isVal: isOverrideFn, v:UNDEFINED_VALUE }, alwaysUseXhrOverride: cfgDfBoolean(), transports: UNDEFINED_VALUE, - retryCodes: UNDEFINED_VALUE + retryCodes: UNDEFINED_VALUE, + maxRetryCnt: {isVal: isNumber, v:10} }); function _chkSampling(value: number) { @@ -92,7 +94,7 @@ const EnvelopeTypeCreator: { [key:string] : EnvelopeCreator } = { [RemoteDependencyData.dataType]: DependencyEnvelopeCreator }; -export type SenderFunction = (payload: string[], isAsync: boolean) => void | IPromise; +export type SenderFunction = (payload: string[] | IInternalStorageItem[], isAsync: boolean) => void | IPromise; export class Sender extends BaseTelemetryPlugin implements IChannelControls { @@ -375,16 +377,16 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { httpInterface = _sendPostMgr && _sendPostMgr.getSenderInst(theTransports, false); let xhrInterface = _sendPostMgr && _sendPostMgr.getFallbackInst(); - _xhrSend = (payload: string[], isAsync: boolean) => { + _xhrSend = (payload: IInternalStorageItem[], isAsync: boolean) => { return _doSend(xhrInterface, payload, isAsync); }; - _fallbackSend = (payload: string[], isAsync: boolean) => { // for fallback send, should NOT mark payload as sent again! + _fallbackSend = (payload: IInternalStorageItem[], isAsync: boolean) => { // for fallback send, should NOT mark payload as sent again! return _doSend(xhrInterface, payload, isAsync, false); }; httpInterface = _alwaysUseCustomSend? customInterface : (httpInterface || customInterface || xhrInterface); - _self._sender = (payload: string[], isAsync: boolean) => { + _self._sender = (payload: IInternalStorageItem[], isAsync: boolean) => { return _doSend(httpInterface, payload, isAsync); }; @@ -403,7 +405,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { syncInterface = _alwaysUseCustomSend? customInterface : (syncInterface || customInterface); if ((_alwaysUseCustomSend || senderConfig.unloadTransports || !_syncUnloadSender) && syncInterface) { - _syncUnloadSender = (payload: string[], isAsync: boolean) => { + _syncUnloadSender = (payload: IInternalStorageItem[], isAsync: boolean) => { return _doSend(syncInterface, payload, isAsync); }; } @@ -440,9 +442,13 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { // flush if we would exceed the max-size limit by adding this item const buffer = _self._buffer; _checkMaxSize(payload); + let payloadItem = { + item: payload, + cnt: 0 // inital cnt will always be 0 + } as IInternalStorageItem; // enqueue the payload - buffer.enqueue(payload); + buffer.enqueue(payloadItem); // ensure an invocation timeout is set _setupTimer(); @@ -466,8 +472,13 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { /** * xhr state changes */ - _self._xhrReadyStateChange = (xhr: XMLHttpRequest, payload: string[], countOfItemsInPayload: number) => { - return _xhrReadyStateChange(xhr, payload, countOfItemsInPayload); + _self._xhrReadyStateChange = (xhr: XMLHttpRequest, payload: string[] | IInternalStorageItem[], countOfItemsInPayload: number) => { + // since version 3.2.0, this function is no-op + if (_isStringArr(payload)) { + return; + } + return _xhrReadyStateChange(xhr, payload as IInternalStorageItem[],countOfItemsInPayload); + } /** @@ -543,71 +554,52 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { /** * error handler */ - _self._onError = (payload: string[], message: string, event?: ErrorEvent) => { - _throwInternal(_self.diagLog(), - eLoggingSeverity.WARNING, - _eInternalMessageId.OnError, - "Failed to send telemetry.", - { message }); - - _self._buffer && _self._buffer.clearSent(payload); + _self._onError = (payload: IInternalStorageItem[] | string[], message: string, event?: ErrorEvent) => { + // since version 3.1.3, string[] is no-op + if (_isStringArr(payload)) { + return; + } + return _onError(payload as IInternalStorageItem[], message, event); }; /** * partial success handler */ - _self._onPartialSuccess = (payload: string[], results: IBackendResponse) => { - const failed: string[] = []; - const retry: string[] = []; - - // Iterate through the reversed array of errors so that splicing doesn't have invalid indexes after the first item. - const errors = results.errors.reverse(); - for (const error of errors) { - const extracted = payload.splice(error.index, 1)[0]; - if (_isRetriable(error.statusCode)) { - retry.push(extracted); - } else { - // All other errors, including: 402 (Monthly quota exceeded) and 439 (Too many requests and refresh cache). - failed.push(extracted); - } - } - - if (payload.length > 0) { - _self._onSuccess(payload, results.itemsAccepted); - } - - if (failed.length > 0) { - _self._onError(failed, formatErrorMessageXhr(null, ["partial success", results.itemsAccepted, "of", results.itemsReceived].join(" "))); - } - - if (retry.length > 0) { - _resendPayload(retry); - - _throwInternal(_self.diagLog(), - eLoggingSeverity.WARNING, - _eInternalMessageId.TransmissionFailed, "Partial success. " + - "Delivered: " + payload.length + ", Failed: " + failed.length + - ". Will retry to send " + retry.length + " our of " + results.itemsReceived + " items"); + _self._onPartialSuccess = (payload: IInternalStorageItem[] | string[], results: IBackendResponse) => { + // since version 3.1.3, string[] is no-op + if (_isStringArr(payload)) { + return; } + return _onPartialSuccess(payload as IInternalStorageItem[], results); }; /** * success handler */ - _self._onSuccess = (payload: string[], countOfItemsInPayload: number) => { - _self._buffer && _self._buffer.clearSent(payload); + _self._onSuccess = (payload: IInternalStorageItem[] | string[], countOfItemsInPayload: number) => { + // since version 3.1.3, string[] is no-op + if (_isStringArr(payload)) { + return; + } + return _onSuccess(payload as IInternalStorageItem[], countOfItemsInPayload); + + //_self._buffer && _self._buffer.clearSent(payload); }; /** * xdr state changes */ - _self._xdrOnLoad = (xdr: IXDomainRequest, payload: string[]) => { - return _xdrOnLoad(xdr, payload); + _self._xdrOnLoad = (xdr: IXDomainRequest, payload: IInternalStorageItem[] | string[]) => { + // since version 3.1.3, string[] is no-op + if (_isStringArr(payload)) { + return; + } + return _xdrOnLoad(xdr, payload as IInternalStorageItem[]); } - function _xdrOnLoad (xdr: IXDomainRequest, payload: string[]) { + function _xdrOnLoad (xdr: IXDomainRequest, payload: IInternalStorageItem[]) { const responseText = _getResponseText(xdr); if (xdr && (responseText + "" === "200" || responseText === "")) { _consecutiveErrors = 0; @@ -680,12 +672,71 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { /** * xhr state changes */ - function _xhrReadyStateChange (xhr: XMLHttpRequest, payload: string[], countOfItemsInPayload: number) { + function _xhrReadyStateChange (xhr: XMLHttpRequest, payload: IInternalStorageItem[], countOfItemsInPayload: number) { if (xhr.readyState === 4) { _checkResponsStatus(xhr.status, payload, xhr.responseURL, countOfItemsInPayload, formatErrorMessageXhr(xhr), _getResponseText(xhr) || xhr.response); } } + /** + * error handler + */ + function _onError(payload: IInternalStorageItem[], message: string, event?: ErrorEvent) { + _throwInternal(_self.diagLog(), + eLoggingSeverity.WARNING, + _eInternalMessageId.OnError, + "Failed to send telemetry.", + { message }); + + _self._buffer && _self._buffer.clearSent(payload); + } + /** + * partial success handler + */ + function _onPartialSuccess(payload: IInternalStorageItem[], results: IBackendResponse) { + const failed: IInternalStorageItem[] = []; + const retry: IInternalStorageItem[] = []; + + // Iterate through the reversed array of errors so that splicing doesn't have invalid indexes after the first item. + const errors = results.errors.reverse(); + for (const error of errors) { + const extracted = payload.splice(error.index, 1)[0]; + if (_isRetriable(error.statusCode)) { + retry.push(extracted); + } else { + // All other errors, including: 402 (Monthly quota exceeded) and 439 (Too many requests and refresh cache). + failed.push(extracted); + } + } + + if (payload.length > 0) { + _self._onSuccess(payload, results.itemsAccepted); + } + + if (failed.length > 0) { + _self._onError(failed, formatErrorMessageXhr(null, ["partial success", results.itemsAccepted, "of", results.itemsReceived].join(" "))); + } + + if (retry.length > 0) { + _resendPayload(retry); + + _throwInternal(_self.diagLog(), + eLoggingSeverity.WARNING, + _eInternalMessageId.TransmissionFailed, "Partial success. " + + "Delivered: " + payload.length + ", Failed: " + failed.length + + ". Will retry to send " + retry.length + " our of " + results.itemsReceived + " items"); + } + } + + + /** + * success handler + */ + function _onSuccess(payload: IInternalStorageItem[], countOfItemsInPayload: number) { + _self._buffer && _self._buffer.clearSent(payload); + } + + function _getPayloadArr(payload?: IPayloadData) { try { if (payload) { @@ -823,7 +874,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { return _self._sample.isSampledIn(envelope); } - function _getOnComplete(payload: string[], status: number, headers: {[headerName: string]: string;}, response?: string) { + function _getOnComplete(payload: IInternalStorageItem[], status: number, headers: {[headerName: string]: string;}, response?: string) { // *********************************************************************************************** //TODO: handle other status codes @@ -834,7 +885,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { } } - function _doSend(sendInterface: IXHROverride, payload: string[], isAsync: boolean, markAsSent: boolean = true): void | IPromise { + function _doSend(sendInterface: IXHROverride, payload: IInternalStorageItem[], isAsync: boolean, markAsSent: boolean = true): void | IPromise { let onComplete = (status: number, headers: {[headerName: string]: string;}, response?: string) => { return _getOnComplete(payload, status, headers, response); } @@ -852,7 +903,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { return null; } - function _getPayload(payload: string[]): IInternalPayloadData { + function _getPayload(payload: IInternalStorageItem[]): IInternalPayloadData { if (isArray(payload) && payload.length > 0) { let batch = _self._buffer.batchPayloads(payload); let headers = _getHeaders(); @@ -897,7 +948,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { return false; } - function _checkResponsStatus(status: number, payload: string[], responseUrl: string, countOfItemsInPayload: number, errorMessage: string, res: any) { + function _checkResponsStatus(status: number, payload: IInternalStorageItem[], responseUrl: string, countOfItemsInPayload: number, errorMessage: string, res: any) { let response: IBackendResponse = null; if (!_self._appId) { response = parseResponse(res); @@ -975,7 +1026,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { return false; } - function _doUnloadSend(payload: string[], isAsync: boolean) { + function _doUnloadSend(payload: IInternalStorageItem[], isAsync: boolean) { if (_syncUnloadSender) { // We are unloading so always call the sender with sync set to false @@ -994,7 +1045,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { let data = internalPayload && internalPayload.oriPayload; if (!_disableBeaconSplit) { // Failed to send entire payload so try and split data and try to send as much events as possible - let droppedPayload: string[] = []; + let droppedPayload: IInternalStorageItem[] = []; for (let lp = 0; lp < data.length; lp++) { const thePayload = data[lp]; let arr = [thePayload]; @@ -1017,14 +1068,27 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { } + function _isStringArr(arr: string[] | IInternalStorageItem[]) { + try { + if (arr && arr.length){ + return (isString(arr[0])); + } + + } catch(e) { + //TODO: log, sender use IInternalStorageItem instead of string since 3.1.3 + } + return null; + + } + - function _fetchKeepAliveSender(payload: string[], isAsync: boolean) { + function _fetchKeepAliveSender(payload: IInternalStorageItem[], isAsync: boolean) { let transport = null; if (isArray(payload)) { let payloadSize = payload.length; for (let lp = 0; lp < payload.length; lp++) { - payloadSize += payload[lp].length; + payloadSize += payload[lp].item.length; } let syncFetchPayload = _sendPostMgr.getSyncFetchPayload(); @@ -1050,7 +1114,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { * Resend payload. Adds payload back to the send buffer and setup a send timer (with exponential backoff). * @param payload */ - function _resendPayload(payload: string[], linearFactor: number = 1) { + function _resendPayload(payload: IInternalStorageItem[], linearFactor: number = 1) { if (!payload || payload.length === 0) { return; } @@ -1060,6 +1124,8 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { _consecutiveErrors++; for (const item of payload) { + item.cnt = item.cnt || 0; // to make sure we have cnt for each payload + item.cnt ++; // when resend, increase cnt buffer.enqueue(item); } @@ -1160,6 +1226,8 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { } } + + /** * Validate UUID Format * Specs taken from https://tools.ietf.org/html/rfc4122 and breeze repo @@ -1276,9 +1344,12 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { /** * xhr state changes * @deprecated + * since version 3.2.0, if the payload is string[], this function is no-op (string[] is only used for backwards Compatibility) */ - public _xhrReadyStateChange(xhr: XMLHttpRequest, payload: string[], countOfItemsInPayload: number) { + public _xhrReadyStateChange(xhr: XMLHttpRequest, payload: string[] | IInternalStorageItem[], countOfItemsInPayload: number) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + // TODO: no-op + // add note to users, this will be removed } /** @@ -1298,30 +1369,37 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { /** * error handler + * @Internal + * since version 3.2.0, if the payload is string[], this function is no-op (string[] is only used for backwards Compatibility) */ - public _onError(payload: string[], message: string, event?: ErrorEvent) { + public _onError(payload: string[] | IInternalStorageItem[], message: string, event?: ErrorEvent) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } /** * partial success handler + * @Internal + * since version 3.2.0, if the payload is string[], this function is no-op (string[] is only used for backwards Compatibility) */ - public _onPartialSuccess(payload: string[], results: IBackendResponse) { + public _onPartialSuccess(payload: string[] | IInternalStorageItem[], results: IBackendResponse) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } /** * success handler + * @Internal + * since version 3.2.0, if the payload is string[], this function is no-op (string[] is only used for backwards Compatibility) */ - public _onSuccess(payload: string[], countOfItemsInPayload: number) { + public _onSuccess(payload: string[] | IInternalStorageItem[], countOfItemsInPayload: number) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } /** * xdr state changes * @deprecated + * since version 3.2.0, if the payload is string[], this function is no-op (string[] is only used for backwards Compatibility) */ - public _xdrOnLoad(xdr: IXDomainRequest, payload: string[]) { + public _xdrOnLoad(xdr: IXDomainRequest, payload: string[] | IInternalStorageItem[]) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } diff --git a/common/Tests/Framework/src/AITestClass.ts b/common/Tests/Framework/src/AITestClass.ts index 1bf14cbc3..4cbb73d0d 100644 --- a/common/Tests/Framework/src/AITestClass.ts +++ b/common/Tests/Framework/src/AITestClass.ts @@ -611,7 +611,11 @@ export class AITestClass { let resultPayload: any[] = []; if (spy.called && spy.args && spy.args.length > 0) { spy.args.forEach(call => { - call[0].forEach((message: string) => { + call[0].forEach((item: any) => { + let message = item; + if (typeof item !== "string") { + message = item.item; + } if (message) { // Ignore the internal SendBrowserInfoOnUserInit message (Only occurs when running tests in a browser) if (includeInit || message.indexOf("AI (Internal): 72 ") === -1) { diff --git a/common/config/rush/npm-shrinkwrap.json b/common/config/rush/npm-shrinkwrap.json index 4a147c5b3..64c96035f 100644 --- a/common/config/rush/npm-shrinkwrap.json +++ b/common/config/rush/npm-shrinkwrap.json @@ -297,17 +297,17 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@microsoft/api-extractor": { - "version": "7.43.0", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.43.0.tgz", - "integrity": "sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==", + "version": "7.43.1", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.43.1.tgz", + "integrity": "sha512-ohg40SsvFFgzHFAtYq5wKJc8ZDyY46bphjtnSvhSSlXpPTG7GHwyyXkn48UZiUCBwr2WC7TRC1Jfwz7nreuiyQ==", "dependencies": { - "@microsoft/api-extractor-model": "7.28.13", + "@microsoft/api-extractor-model": "7.28.14", "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "4.0.2", + "@rushstack/node-core-library": "4.1.0", "@rushstack/rig-package": "0.5.2", - "@rushstack/terminal": "0.10.0", - "@rushstack/ts-command-line": "4.19.1", + "@rushstack/terminal": "0.10.1", + "@rushstack/ts-command-line": "4.19.2", "lodash": "~4.17.15", "minimatch": "~3.0.3", "resolve": "~1.22.1", @@ -320,13 +320,13 @@ } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.28.13", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.13.tgz", - "integrity": "sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==", + "version": "7.28.14", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.14.tgz", + "integrity": "sha512-Bery/c8A8SsKPSvA82cTTuy/+OcxZbLRmKhPkk91/AJOQzxZsShcrmHFAGeiEqSIrv1nPZ3tKq9kfMLdCHmsqg==", "dependencies": { "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "4.0.2" + "@rushstack/node-core-library": "4.1.0" } }, "node_modules/@microsoft/api-extractor/node_modules/typescript": { @@ -1281,9 +1281,9 @@ } }, "node_modules/@rushstack/node-core-library": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz", - "integrity": "sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.1.0.tgz", + "integrity": "sha512-qz4JFBZJCf1YN5cAXa1dP6Mki/HrsQxc/oYGAGx29dF2cwF2YMxHoly0FBhMw3IEnxo5fMj0boVfoHVBkpkx/w==", "dependencies": { "fs-extra": "~7.0.1", "import-lazy": "~4.0.0", @@ -1311,11 +1311,11 @@ } }, "node_modules/@rushstack/terminal": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.10.0.tgz", - "integrity": "sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.10.1.tgz", + "integrity": "sha512-C6Vi/m/84IYJTkfzmXr1+W8Wi3MmBjVF/q3za91Gb3VYjKbpALHVxY6FgH625AnDe5Z0Kh4MHKWA3Z7bqgAezA==", "dependencies": { - "@rushstack/node-core-library": "4.0.2", + "@rushstack/node-core-library": "4.1.0", "supports-color": "~8.1.1" }, "peerDependencies": { @@ -1328,11 +1328,11 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.19.1.tgz", - "integrity": "sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.19.2.tgz", + "integrity": "sha512-cqmXXmBEBlzo9WtyUrHtF9e6kl0LvBY7aTSVX4jfnBfXWZQWnPq9JTFPlQZ+L/ZwjZ4HrNwQsOVvhe9oOucZkw==", "dependencies": { - "@rushstack/terminal": "0.10.0", + "@rushstack/terminal": "0.10.1", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" @@ -2644,9 +2644,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.731", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.731.tgz", - "integrity": "sha512-+TqVfZjpRz2V/5SPpmJxq9qK620SC5SqCnxQIOi7i/U08ZDcTpKbT7Xjj9FU5CbXTMUb4fywbIr8C7cGv4hcjw==" + "version": "1.4.733", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.733.tgz", + "integrity": "sha512-gUI9nhI2iBGF0OaYYLKOaOtliFMl+Bt1rY7VmEjwxOxqoYLub/D9xmduPEhbw2imE6gYkJKhIE5it+KE2ulVxQ==" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -2765,12 +2765,15 @@ } }, "node_modules/eslint-plugin-security": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-2.1.1.tgz", - "integrity": "sha512-7cspIGj7WTfR3EhaILzAPcfCo5R9FbeWvbgsPYWivSurTBKW88VQxtP3c4aWMG9Hz/GfJlJVdXEJ3c8LqS+u2w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-3.0.0.tgz", + "integrity": "sha512-2Ij7PkmXIF2cKwoVkEgemwoXbOnxg5UfdhdcpNxZwJxC/10dbsdhHISrTyJ/n8DUkt3yiN6P1ywEgcMGjIwHIw==", "peer": true, "dependencies": { "safe-regex": "^2.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/eslint-scope": { @@ -6512,17 +6515,17 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "@microsoft/api-extractor": { - "version": "7.43.0", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.43.0.tgz", - "integrity": "sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==", + "version": "7.43.1", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.43.1.tgz", + "integrity": "sha512-ohg40SsvFFgzHFAtYq5wKJc8ZDyY46bphjtnSvhSSlXpPTG7GHwyyXkn48UZiUCBwr2WC7TRC1Jfwz7nreuiyQ==", "requires": { - "@microsoft/api-extractor-model": "7.28.13", + "@microsoft/api-extractor-model": "7.28.14", "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "4.0.2", + "@rushstack/node-core-library": "4.1.0", "@rushstack/rig-package": "0.5.2", - "@rushstack/terminal": "0.10.0", - "@rushstack/ts-command-line": "4.19.1", + "@rushstack/terminal": "0.10.1", + "@rushstack/ts-command-line": "4.19.2", "lodash": "~4.17.15", "minimatch": "~3.0.3", "resolve": "~1.22.1", @@ -6539,13 +6542,13 @@ } }, "@microsoft/api-extractor-model": { - "version": "7.28.13", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.13.tgz", - "integrity": "sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==", + "version": "7.28.14", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.14.tgz", + "integrity": "sha512-Bery/c8A8SsKPSvA82cTTuy/+OcxZbLRmKhPkk91/AJOQzxZsShcrmHFAGeiEqSIrv1nPZ3tKq9kfMLdCHmsqg==", "requires": { "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "4.0.2" + "@rushstack/node-core-library": "4.1.0" } }, "@microsoft/dynamicproto-js": { @@ -7385,9 +7388,9 @@ } }, "@rushstack/node-core-library": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz", - "integrity": "sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.1.0.tgz", + "integrity": "sha512-qz4JFBZJCf1YN5cAXa1dP6Mki/HrsQxc/oYGAGx29dF2cwF2YMxHoly0FBhMw3IEnxo5fMj0boVfoHVBkpkx/w==", "requires": { "fs-extra": "~7.0.1", "import-lazy": "~4.0.0", @@ -7407,20 +7410,20 @@ } }, "@rushstack/terminal": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.10.0.tgz", - "integrity": "sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.10.1.tgz", + "integrity": "sha512-C6Vi/m/84IYJTkfzmXr1+W8Wi3MmBjVF/q3za91Gb3VYjKbpALHVxY6FgH625AnDe5Z0Kh4MHKWA3Z7bqgAezA==", "requires": { - "@rushstack/node-core-library": "4.0.2", + "@rushstack/node-core-library": "4.1.0", "supports-color": "~8.1.1" } }, "@rushstack/ts-command-line": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.19.1.tgz", - "integrity": "sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.19.2.tgz", + "integrity": "sha512-cqmXXmBEBlzo9WtyUrHtF9e6kl0LvBY7aTSVX4jfnBfXWZQWnPq9JTFPlQZ+L/ZwjZ4HrNwQsOVvhe9oOucZkw==", "requires": { - "@rushstack/terminal": "0.10.0", + "@rushstack/terminal": "0.10.1", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" @@ -8385,9 +8388,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.731", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.731.tgz", - "integrity": "sha512-+TqVfZjpRz2V/5SPpmJxq9qK620SC5SqCnxQIOi7i/U08ZDcTpKbT7Xjj9FU5CbXTMUb4fywbIr8C7cGv4hcjw==" + "version": "1.4.733", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.733.tgz", + "integrity": "sha512-gUI9nhI2iBGF0OaYYLKOaOtliFMl+Bt1rY7VmEjwxOxqoYLub/D9xmduPEhbw2imE6gYkJKhIE5it+KE2ulVxQ==" }, "encodeurl": { "version": "1.0.2", @@ -8493,9 +8496,9 @@ } }, "eslint-plugin-security": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-2.1.1.tgz", - "integrity": "sha512-7cspIGj7WTfR3EhaILzAPcfCo5R9FbeWvbgsPYWivSurTBKW88VQxtP3c4aWMG9Hz/GfJlJVdXEJ3c8LqS+u2w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-3.0.0.tgz", + "integrity": "sha512-2Ij7PkmXIF2cKwoVkEgemwoXbOnxg5UfdhdcpNxZwJxC/10dbsdhHISrTyJ/n8DUkt3yiN6P1ywEgcMGjIwHIw==", "peer": true, "requires": { "safe-regex": "^2.1.1" diff --git a/extensions/applicationinsights-analytics-js/Tests/Unit/src/AnalyticsPlugin.tests.ts b/extensions/applicationinsights-analytics-js/Tests/Unit/src/AnalyticsPlugin.tests.ts index e2042abdb..4528bf636 100644 --- a/extensions/applicationinsights-analytics-js/Tests/Unit/src/AnalyticsPlugin.tests.ts +++ b/extensions/applicationinsights-analytics-js/Tests/Unit/src/AnalyticsPlugin.tests.ts @@ -826,7 +826,7 @@ export class AnalyticsPluginTests extends AITestClass { }); this.throwInternalSpy = this.sandbox.spy(appInsights.core.logger, "throwInternal"); - sender._sender = (payload:string[], isAsync:boolean) => { + sender._sender = (payload:any[], isAsync:boolean) => { sender._onSuccess(payload, payload.length); }; this.sandbox.spy() @@ -842,10 +842,10 @@ export class AnalyticsPluginTests extends AITestClass { Assert.ok(!this.throwInternalSpy.called, "No internal errors"); }].concat(this.waitForException(1)) .concat(() => { - let isLocal = window.location.protocol === "file:"; let exp = this.trackSpy.args[0]; const payloadStr: string[] = this.getPayloadMessages(this.trackSpy); + if (payloadStr.length > 0) { const payload = JSON.parse(payloadStr[0]); const data = payload.data; @@ -909,7 +909,7 @@ export class AnalyticsPluginTests extends AITestClass { }); this.throwInternalSpy = this.sandbox.spy(appInsights.core.logger, "throwInternal"); - sender._sender = (payload:string[], isAsync:boolean) => { + sender._sender = (payload:any[], isAsync:boolean) => { sender._onSuccess(payload, payload.length); }; this.sandbox.spy() @@ -924,7 +924,6 @@ export class AnalyticsPluginTests extends AITestClass { Assert.ok(!this.throwInternalSpy.called, "No internal errors"); }].concat(this.waitForException(1)) .concat(() => { - let exp = this.trackSpy.args[0]; const payloadStr: string[] = this.getPayloadMessages(this.trackSpy); if (payloadStr.length > 0) { @@ -983,7 +982,7 @@ export class AnalyticsPluginTests extends AITestClass { }); this.throwInternalSpy = this.sandbox.spy(appInsights.core.logger, "throwInternal"); - sender._sender = (payload:string[], isAsync:boolean) => { + sender._sender = (payload:any[], isAsync:boolean) => { sender._onSuccess(payload, payload.length); }; this.sandbox.spy() @@ -1065,7 +1064,7 @@ export class AnalyticsPluginTests extends AITestClass { }); this.throwInternalSpy = this.sandbox.spy(appInsights.core.logger, "throwInternal"); - sender._sender = (payload:string[], isAsync:boolean) => { + sender._sender = (payload:any[], isAsync:boolean) => { sender._onSuccess(payload, payload.length); }; this.sandbox.spy() @@ -1914,10 +1913,13 @@ export class AnalyticsPluginTests extends AITestClass { } Assert.ok(true, "* [" + argCount + " of " + expectedCount + "] checking spy " + new Date().toISOString()); - try { if (argCount >= expectedCount) { - const payload = JSON.parse(this.trackSpy.args[0][0]); + let payloads: any = [] + this.trackSpy.args[0][0].forEach(item => { + payloads.push(item.item) + }); + const payload = JSON.parse(payloads); const baseType = payload.data.baseType; // call the appropriate Validate depending on the baseType switch (baseType) {