From 3a1fdb18d7cd9536f11de91f61957f283c7811c5 Mon Sep 17 00:00:00 2001 From: Karlie-777 <79606506+Karlie-777@users.noreply.github.com> Date: Fri, 13 Sep 2024 17:12:42 -0700 Subject: [PATCH] [Main][Task]28966399: Separate critical events and non-critical events for Offline Support (#2401) * add in memery split for offline support * update * update * update * update * update * update --- .../Unit/src/applicationinsights.e2e.tests.ts | 4 +- .../Tests/Unit/src/channel.tests.ts | 331 ++++++++++++++++-- .../Tests/Unit/src/offlinetimer.tests.ts | 140 +++++++- .../src/Interfaces/IOfflineProvider.ts | 13 +- .../offline-channel-js/src/OfflineChannel.ts | 222 +++++++----- common/config/rush/npm-shrinkwrap.json | 234 +++++++------ 6 files changed, 713 insertions(+), 231 deletions(-) diff --git a/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts b/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts index 6055b0c41..ce845d33e 100644 --- a/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts +++ b/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts @@ -2,7 +2,7 @@ import { AITestClass, Assert, PollingAssert, EventValidator, TraceValidator, Exc import { SinonSpy } from 'sinon'; import { ApplicationInsights } from '../../../src/applicationinsights-web' import { Sender } from '@microsoft/applicationinsights-channel-js'; -import { IDependencyTelemetry, ContextTagKeys, Event, Trace, Exception, Metric, PageView, PageViewPerformance, RemoteDependencyData, DistributedTracingModes, RequestHeaders, IAutoExceptionTelemetry, BreezeChannelIdentifier, IConfig } from '@microsoft/applicationinsights-common'; +import { IDependencyTelemetry, ContextTagKeys, Event, Trace, Exception, Metric, PageView, PageViewPerformance, RemoteDependencyData, DistributedTracingModes, RequestHeaders, IAutoExceptionTelemetry, BreezeChannelIdentifier, IConfig, EventPersistence } from '@microsoft/applicationinsights-common'; import { ITelemetryItem, getGlobal, newId, dumpObj, BaseTelemetryPlugin, IProcessTelemetryContext, __getRegisteredEvents, arrForEach, IConfiguration, ActiveStatus, FeatureOptInMode } from "@microsoft/applicationinsights-core-js"; import { TelemetryContext } from '@microsoft/applicationinsights-properties-js'; import { createAsyncResolvedPromise } from '@nevware21/ts-async'; @@ -677,7 +677,7 @@ export class ApplicationInsightsTests extends AITestClass { this._ai.trackEvent({ name: "offline event", properties: { "prop2": "value2" }, measurements: { "measurement2": 200 } }); inMemoTimer = offlineChannel["_getDbgPlgTargets"]()[3]; Assert.ok(inMemoTimer, "in memo timer should not be null"); - let inMemoBatch = offlineChannel["_getDbgPlgTargets"]()[1]; + let inMemoBatch = offlineChannel["_getDbgPlgTargets"]()[1][EventPersistence.Normal]; Assert.equal(inMemoBatch && inMemoBatch.count(), 1, "should have one event"); return true diff --git a/channels/offline-channel-js/Tests/Unit/src/channel.tests.ts b/channels/offline-channel-js/Tests/Unit/src/channel.tests.ts index c0d8b8af5..63b97d9d7 100644 --- a/channels/offline-channel-js/Tests/Unit/src/channel.tests.ts +++ b/channels/offline-channel-js/Tests/Unit/src/channel.tests.ts @@ -1,6 +1,6 @@ import { AITestClass, Assert, PollingAssert } from "@microsoft/ai-test-framework"; -import { DEFAULT_BREEZE_ENDPOINT, DEFAULT_BREEZE_PATH, IConfig } from "@microsoft/applicationinsights-common"; -import { AppInsightsCore, IConfiguration, arrForEach, getGlobal, getGlobalInst } from "@microsoft/applicationinsights-core-js"; +import { DEFAULT_BREEZE_ENDPOINT, DEFAULT_BREEZE_PATH, EventPersistence, IConfig } from "@microsoft/applicationinsights-common"; +import { AppInsightsCore, IConfiguration, arrForEach, getGlobal, getGlobalInst, objKeys } from "@microsoft/applicationinsights-core-js"; import { TestChannel, mockTelemetryItem } from "./TestHelper"; import { OfflineChannel } from "../../../src/OfflineChannel" import { IOfflineChannelConfiguration, eStorageProviders } from "../../../src/applicationinsights-offlinechannel-js"; @@ -15,6 +15,7 @@ export class ChannelTests extends AITestClass { private evtDiscard: any; private evtStore: any; private batchDrop: any; + private levelKeys: any; public testInitialize() { super.testInitialize(); @@ -31,6 +32,7 @@ export class ChannelTests extends AITestClass { this.evtSent = 0; this.evtStore = 0; this.batchDrop = 0; + this.levelKeys = [EventPersistence.Critical, EventPersistence.Normal]; } @@ -53,6 +55,7 @@ export class ChannelTests extends AITestClass { this.evtSent = 0; this.evtStore = 0; this.batchDrop = 0; + this.levelKeys = null; } @@ -87,8 +90,13 @@ export class ChannelTests extends AITestClass { offlineListener.setOnlineState(1); let evt = mockTelemetryItem(); channel.processTelemetry(evt); - let inMemoBatch = channel["_getDbgPlgTargets"]()[1]; - Assert.equal(inMemoBatch.count(), 0, "online should process next"); + let inMemoMap = channel["_getDbgPlgTargets"]()[1]; + let mapKeys = objKeys(inMemoMap); + Assert.deepEqual(mapKeys.length, this.levelKeys.length, "in memo map should have expected keys"); + arrForEach(this.levelKeys, (key) => { + let inMemoBatch = inMemoMap[key]; + Assert.equal(inMemoBatch.count(), 0, key + " in memo batch should exist"); + }); Assert.equal(this.evtDiscard, 0, "discard listener notification should not be called"); Assert.equal(this.evtStore, 0, "store listener notification should not be called"); @@ -129,8 +137,15 @@ export class ChannelTests extends AITestClass { offlineListener.setOnlineState(1); let evt = mockTelemetryItem(); channel.processTelemetry(evt); - let inMemoBatch = channel["_getDbgPlgTargets"]()[1]; - Assert.equal(inMemoBatch.count(), 0, "online should process next"); + + let inMemoMap = channel["_getDbgPlgTargets"]()[1]; + let mapKeys = objKeys(inMemoMap); + Assert.deepEqual(mapKeys.length, this.levelKeys.length, "in memo map should have expected keys"); + Assert.ok(inMemoMap, "inMemoMap should exist"); + arrForEach(this.levelKeys, (key) => { + let inMemoBatch = inMemoMap[key]; + Assert.equal(inMemoBatch.count(), 0, key + " in memo batch should exist"); + }); Assert.equal(this.evtDiscard, 0, "discard listener notification should not be called"); Assert.equal(this.evtStore, 0, "store listener notification should not be called"); @@ -184,19 +199,30 @@ export class ChannelTests extends AITestClass { let evt = mockTelemetryItem(); expectedStoreId.push(evt.ver); channel.processTelemetry(evt); - let inMemoBatch = channel["_getDbgPlgTargets"]()[1]; - Assert.equal(inMemoBatch.count(), 0, "online should process next"); + let inMemoMap = channel["_getDbgPlgTargets"]()[1]; + let mapKeys = objKeys(inMemoMap); + Assert.deepEqual(mapKeys.length, this.levelKeys.length, "in memo map should have expected keys"); + Assert.ok(inMemoMap, "inMemoMap should exist"); + arrForEach(this.levelKeys, (key) => { + let inMemoBatch = inMemoMap[key]; + Assert.equal(inMemoBatch.count(), 0, key + " in memo batch should exist"); + }); + + let inMemoBatch = inMemoMap[EventPersistence.Normal]; + Assert.equal(this.evtDiscard, 0, "discard listener notification should not be called"); Assert.equal(this.evtStore, 0, "store listener notification should not be called"); Assert.equal(this.batchDrop, 0, "batch drop listener notification should not be called"); offlineListener.setOnlineState(2); channel.processTelemetry(evt); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch.count(), 1, "offline should process"); this.clock.tick(2000); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch.count(), 0, "provider should store item"); let storage = AITestClass.orgLocalStorage; let storageKey = "AIOffline_1_dc.services.visualstudio.com"; @@ -219,8 +245,8 @@ export class ChannelTests extends AITestClass { evts = storageObj.evts; Assert.deepEqual(Object.keys(evts).length, 0, "storage should not have one event"); let request = requests[0]; - this.sendJsonResponse(request, {}, 200); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch.count(), 0, "in memo should not have item"); offlineListener.setOnlineState(2); @@ -273,6 +299,105 @@ export class ChannelTests extends AITestClass { } }); + this.testCase({ + name: "Channel: Process Telemetry with web provider when splitevts is set to true ", + useFakeTimers: true, + test: () => { + let window = getGlobalInst("window"); + let fakeXMLHttpRequest = (window as any).XMLHttpRequest; + this.coreConfig.extensionConfig = {["OfflineChannel"]: {inMemoMaxTime: 2000, splitEvts: true} as IOfflineChannelConfiguration}; + let sendChannel = new TestChannel(); + let storedEvts:any[] = []; + let expectedStoreId: any[] = []; + + this.core.initialize(this.coreConfig, [sendChannel]); + this.core.addNotificationListener({ + eventsDiscarded: (evts, reason) => { + this.evtDiscard += 1; + }, + offlineEventsStored: (evts) => { + this.evtStore += 1; + arrForEach(evts, (item) => { + storedEvts.push(item.ver); + }) + + }, + offlineBatchDrop(cnt, reason) { + this.batchDrop += 1; + } + }); + + let channel = new OfflineChannel(); + + channel.initialize(this.coreConfig, this.core,[]); + this.onDone(() => { + channel.teardown(); + }); + + this.clock.tick(1); + let offlineListener = channel.getOfflineListener() as any; + offlineListener.setOnlineState(1); + let evt = mockTelemetryItem(); + expectedStoreId.push(evt.ver); + channel.processTelemetry(evt); + let inMemoMap = channel["_getDbgPlgTargets"]()[1]; + let mapKeys = objKeys(inMemoMap); + Assert.deepEqual(mapKeys.length, this.levelKeys.length, "in memo map should have expected keys"); + Assert.ok(inMemoMap, "inMemoMap should exist"); + arrForEach(this.levelKeys, (key) => { + let inMemoBatch = inMemoMap[key]; + Assert.equal(inMemoBatch.count(), 0, key + " in memo batch should exist"); + }); + + Assert.equal(this.evtDiscard, 0, "discard listener notification should not be called"); + Assert.equal(this.evtStore, 0, "store listener notification should not be called"); + Assert.equal(this.batchDrop, 0, "batch drop listener notification should not be called"); + + offlineListener.setOnlineState(2); + // process EventPersistence.Normal event + channel.processTelemetry(evt); + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + let inMemoBatch = inMemoMap[EventPersistence.Normal]; + Assert.equal(inMemoBatch.count(), 1, "offline should process normal event"); + // process ventPersistence.Critical event + let criticalEvt = mockTelemetryItem(2); + channel.processTelemetry(criticalEvt); + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Critical]; + Assert.equal(inMemoBatch.count(), 1, "offline should process critical event"); + + + this.clock.tick(2000); + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; + Assert.equal(inMemoBatch.count(), 0, "provider should store normal item"); + inMemoBatch = inMemoMap[EventPersistence.Critical]; + Assert.equal(inMemoBatch.count(), 0, "provider should store critical normal item"); + let storage = AITestClass.orgLocalStorage; + let storageKey = "AIOffline_1_dc.services.visualstudio.com"; + let storageStr = storage.getItem(storageKey) as any; + Assert.ok(storageStr.indexOf("header1") > -1, "should contain expeceted header"); + + let storageObj = JSON.parse(storageStr); + let evts = storageObj.evts; + Assert.deepEqual(Object.keys(evts).length, 2, "storage should have two events"); + + let normalCnt = '"criticalCnt":0'; + let criticalCnt = '"criticalCnt":1'; + Assert.ok(storageStr.indexOf(normalCnt) > -1, "should contain expeceted critical count for normal event batches"); + Assert.ok(storageStr.indexOf(criticalCnt) > -1, "should contain expeceted critical count for critical event batches"); + + this.clock.tick(1); + + Assert.equal(this.evtDiscard, 0, "discard listener notification should not be called test1"); + Assert.equal(this.evtStore, 2, "store listener notification should be called two times test1"); + Assert.equal(this.batchDrop, 0, "batch drop listener notification should not be called test1"); + + channel.teardown(); + (window as any).XMLHttpRequest = fakeXMLHttpRequest; + } + }); + this.testCaseAsync({ name: "Channel: Process Telemetry with indexed db provider", @@ -323,17 +448,26 @@ export class ChannelTests extends AITestClass { offlineListener.setOnlineState(1); let evt = mockTelemetryItem(); channel.processTelemetry(evt); - let inMemoBatch = channel["_getDbgPlgTargets"]()[1]; - Assert.equal(inMemoBatch.count(), 0, "online should process next"); + let inMemoMap = channel["_getDbgPlgTargets"]()[1]; + let mapKeys = objKeys(inMemoMap); + Assert.deepEqual(mapKeys.length, this.levelKeys.length, "in memo map should have expected keys"); + Assert.ok(inMemoMap, "inMemoMap should exist"); + arrForEach(this.levelKeys, (key) => { + let inMemoBatch = inMemoMap[key]; + Assert.equal(inMemoBatch.count(), 0, key + " in memo batch should exist"); + }); + + let inMemoBatch = inMemoMap[EventPersistence.Normal]; offlineListener.setOnlineState(2); channel.processTelemetry(evt); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch.count(), 1, "offline should process"); this.clock.tick(2000); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch.count(), 0, "provider should store item"); this.clock.tick(10); @@ -415,9 +549,16 @@ export class ChannelTests extends AITestClass { let ver1 = evt1.ver; let evt2 = mockTelemetryItem(2); channel.processTelemetry(evt1); - let inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + let inMemoMap = channel["_getDbgPlgTargets"]()[1]; + let mapKeys = objKeys(inMemoMap); + Assert.deepEqual(mapKeys.length, this.levelKeys.length, "in memo map should have expected keys"); + Assert.ok(inMemoMap, "inMemoMap should exist"); + let inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch.count(), 1, "online should process evt1"); + + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; channel.processTelemetry(evt2); Assert.equal(inMemoBatch.count(), 1, "online should process evt2"); @@ -435,6 +576,8 @@ export class ChannelTests extends AITestClass { let invalidEvt = mockTelemetryItem(); channel.processTelemetry(invalidEvt); + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch.count(), 1, "online should not process invalid item"); this.clock.tick(1); Assert.equal(this.evtDiscard, 1, "discard listener notification should be called once test2"); @@ -449,6 +592,110 @@ export class ChannelTests extends AITestClass { }); + this.testCase({ + name: "Channel: add event when in Memory batch is full with splitEvts set to true", + useFakeTimers: true, + test: () => { + let channel = new OfflineChannel(); + let sendChannel = new TestChannel(); + // make sure in memo time is long enough + this.coreConfig.extensionConfig = {["OfflineChannel"]: {providers:[eStorageProviders.LocalStorage], inMemoMaxTime: 200000000, eventsLimitInMem: 1, splitEvts: true} as IOfflineChannelConfiguration}; + this.core.initialize(this.coreConfig,[channel, sendChannel]); + this.core.addNotificationListener({ + eventsDiscarded: (evts, reason) => { + this.evtDiscard += 1; + }, + offlineEventsStored: (evts) => { + this.evtStore += 1; + }, + offlineBatchSent: (batch) => { + this.evtSent += 1; + }, + offlineBatchDrop(cnt, reason) { + this.batchDrop += 1; + } + }); + + this.clock.tick(1); + + Assert.equal(this.evtDiscard, 0, "discard listener notification should not be called"); + Assert.equal(this.evtStore, 0, "store listener notification should not be called"); + Assert.equal(this.evtSent, 0, "sent listener notification should not be called"); + Assert.equal(this.batchDrop, 0, "batch drop listener notification should not be called"); + + let inMemoMap = channel["_getDbgPlgTargets"]()[1]; + Assert.ok(inMemoMap, "inMemoMap should exist"); + let mapKeys = objKeys(inMemoMap); + Assert.deepEqual(mapKeys.length, this.levelKeys.length, "in memo map should have expected keys"); + + let offlineListener = channel.getOfflineListener() as any; + offlineListener.setOnlineState(2); + + // eventsLimitInMem = 1, means for each persistent level, max number allowed for in memory events is 1 + + // process one critical event and one normal event (inMemoMap[nomral] should have 1 event and inMemoMap[critical] should have 1 event) + // then process another normal event (one normal event should be saved, inMemoMap[nomral] should have 1 event and inMemoMap[critical] should have 1 event) + let normalEvt1 = mockTelemetryItem(); + let ver1 = normalEvt1.ver; + let normalEvt2 = mockTelemetryItem(); + let criticalEvt1 = mockTelemetryItem(2); + let cVer1 = criticalEvt1.ver; + channel.processTelemetry(normalEvt1); + channel.processTelemetry(criticalEvt1); + + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + let inMemoBatch = inMemoMap[EventPersistence.Normal]; + Assert.equal(inMemoBatch.count(), 1, "online should process normal evt"); + inMemoBatch = inMemoMap[EventPersistence.Critical]; + Assert.equal(inMemoBatch.count(), 1, "online should process critical evt2"); + channel.processTelemetry(normalEvt2); + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; + Assert.equal(inMemoBatch.count(), 1, "normal event batch should have one event"); + inMemoBatch = inMemoMap[EventPersistence.Critical]; + Assert.equal(inMemoBatch.count(), 1, "critical event batch should have one event"); + + let storage = AITestClass.orgLocalStorage; + let storageKey = "AIOffline_1_dc.services.visualstudio.com"; + let storageStr = storage.getItem(storageKey) as any; + let storageObj = JSON.parse(storageStr); + let evts = storageObj.evts; + Assert.equal(evts && Object.keys(evts).length, 1, "should have one events"); + Assert.ok(storageStr.indexOf(ver1) > -1, "should contain only the first event"); + this.clock.tick(1); + Assert.equal(this.evtDiscard, 0, "discard listener notification should not called test1"); + Assert.equal(this.evtStore, 1, "store listener notification should be called once test1"); + Assert.equal(this.evtSent, 0, "sent listener notification should not be called test1"); + + // process another critical event + let criticalEvt2 = mockTelemetryItem(2); + channel.processTelemetry(criticalEvt2); + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; + Assert.equal(inMemoBatch.count(), 1, "normal event batch should have one event test1"); + inMemoBatch = inMemoMap[EventPersistence.Critical]; + Assert.equal(inMemoBatch.count(), 1, "critical event batch should have one event test1"); + + storage = AITestClass.orgLocalStorage; + storageKey = "AIOffline_1_dc.services.visualstudio.com"; + storageStr = storage.getItem(storageKey) as any; + storageObj = JSON.parse(storageStr); + evts = storageObj.evts; + Assert.equal(evts && Object.keys(evts).length, 2, "should have two events"); + Assert.ok(storageStr.indexOf(ver1) > -1, "should contain the first normal event"); + Assert.ok(storageStr.indexOf(cVer1) > -1, "should contain the first critical event"); + this.clock.tick(1); + Assert.equal(this.evtDiscard, 0, "discard listener notification should not called test2"); + Assert.equal(this.evtStore, 2, "store listener notification should be called twice test2"); + Assert.equal(this.evtSent, 0, "sent listener notification should not be called test2"); + + + channel.teardown(); + + } + + }); + this.testCase({ name: "Channel: drop batch notification should be handled with inMemo batch", useFakeTimers: true, @@ -474,7 +721,8 @@ export class ChannelTests extends AITestClass { }); this.clock.tick(1); - let inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + let inMemoMap = channel["_getDbgPlgTargets"]()[1]; + let inMemoBatch = inMemoMap[EventPersistence.Normal]; this.sandbox.stub((inMemoBatch) as any, "addEvent").callsFake((evt) => { return false; }); @@ -491,7 +739,8 @@ export class ChannelTests extends AITestClass { let evt = mockTelemetryItem(); channel.processTelemetry(evt); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch.count(), 0, "should not process evt"); this.clock.tick(1); @@ -543,8 +792,16 @@ export class ChannelTests extends AITestClass { return sender1(payload, oncomplete, sync) }); - let inMemoBatch = channel["_getDbgPlgTargets"]()[1]; - Assert.equal(inMemoBatch.count(), 0, "in memo has no events"); + let inMemoMap = channel["_getDbgPlgTargets"]()[1]; + let mapKeys = objKeys(inMemoMap); + Assert.equal(mapKeys.length, this.levelKeys.length, "in memo map should have expected keys"); + Assert.ok(inMemoMap, "inMemoMap should exist"); + arrForEach(this.levelKeys, (key) => { + let inMemoBatch = inMemoMap[key]; + Assert.equal(inMemoBatch.count(), 0, key + " in memo batch should exist"); + }); + + let inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(this.evtDiscard, 0, "discard listener notification should not be called"); Assert.equal(this.evtStore, 0, "store listener notification should not be called"); @@ -557,11 +814,14 @@ export class ChannelTests extends AITestClass { offlineListener.setOnlineState(2); let evt = mockTelemetryItem(); channel.processTelemetry(evt); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch.count(), 1, "offline should process evt"); this.clock.tick(300); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch.count(), 0, "in Memo should have no events remaining"); let storage = AITestClass.orgLocalStorage; let storageKey = "AIOffline_1_dc.services.visualstudio.com"; @@ -602,15 +862,25 @@ export class ChannelTests extends AITestClass { let offlineListener = channel.getOfflineListener() as any; offlineListener.setOnlineState(2); - let inMemoBatch = channel["_getDbgPlgTargets"]()[1]; - Assert.equal(inMemoBatch.count(), 0, "in memo has no events"); + let inMemoMap = channel["_getDbgPlgTargets"]()[1]; + let mapKeys = objKeys(inMemoMap); + Assert.deepEqual(mapKeys.length, this.levelKeys.length, "in memo map should have expected keys"); + Assert.ok(inMemoMap, "inMemoMap should exist"); + arrForEach(this.levelKeys, (key) => { + let inMemoBatch = inMemoMap[key]; + Assert.equal(inMemoBatch.count(), 0, key + " in memo batch should exist"); + }); + + let inMemoBatch = inMemoMap[EventPersistence.Normal]; + let config = channel["_getDbgPlgTargets"]()[0]; Assert.ok(config, "should have config"); Assert.equal(config.url, DEFAULT_BREEZE_ENDPOINT + DEFAULT_BREEZE_PATH, "should have expected url"); let evt = mockTelemetryItem(); channel.processTelemetry(evt); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch.count(), 1, "offline should process evt"); // should get url from online channel first @@ -619,7 +889,8 @@ export class ChannelTests extends AITestClass { this.core.config.endpointUrl = expectedUrl; this.core.config.instrumentationKey = "test1"; this.clock.tick(1); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch.count(), 1, "in memo has one events"); Assert.equal(inMemoBatch.endpoint(), expectedUrl, "in memo has expected url"); config = channel["_getDbgPlgTargets"]()[0]; @@ -632,7 +903,8 @@ export class ChannelTests extends AITestClass { this.core.config.instrumentationKey = "test2"; this.core.config.endpointUrl = expectedUrl1; this.clock.tick(1); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch.count(), 1, "in memo has one events"); Assert.equal(inMemoBatch.endpoint(), expectedUrl1, "in memo has expected url test1"); config = channel["_getDbgPlgTargets"]()[0]; @@ -647,7 +919,8 @@ export class ChannelTests extends AITestClass { this.core.config.endpointUrl = expectedUrl2; this.core.config.extensionConfig[sendChannel.identifier].endpointUrl = expectedUrl2; this.clock.tick(1); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch.count(), 1, "in memo has one events"); Assert.equal(inMemoBatch.endpoint(), expectedUrl2, "in memo has expected url test1"); config = channel["_getDbgPlgTargets"]()[0]; diff --git a/channels/offline-channel-js/Tests/Unit/src/offlinetimer.tests.ts b/channels/offline-channel-js/Tests/Unit/src/offlinetimer.tests.ts index 29872c2fd..7474951ec 100644 --- a/channels/offline-channel-js/Tests/Unit/src/offlinetimer.tests.ts +++ b/channels/offline-channel-js/Tests/Unit/src/offlinetimer.tests.ts @@ -80,18 +80,24 @@ export class Offlinetimer extends AITestClass { inMemoTimer = channel["_getDbgPlgTargets"]()[3]; Assert.ok(inMemoTimer, "in memo timer should be created"); Assert.ok(inMemoTimer.enabled, "in memo timer should be enabled"); - let inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + let inMemoMap = channel["_getDbgPlgTargets"]()[1]; + let inMemoBatch = inMemoMap[EventPersistence.Normal]; + //let inMemoBatch = channel["_getDbgPlgTargets"]()[1]; Assert.equal(inMemoBatch && inMemoBatch.count(), 2, "should have two events"); // offline, flush all events in memory, and processTelemetry is not called again this.clock.tick(2000); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + //inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch && inMemoBatch.count(), 0, "should have no event left"); inMemoTimer = channel["_getDbgPlgTargets"]()[3]; Assert.ok(!inMemoTimer.enabled, "in memo timer enabled should be false with no events in memory"); this.clock.tick(2000); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + //inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch && inMemoBatch.count(), 0, "should have no event left"); inMemoTimer = channel["_getDbgPlgTargets"]()[3]; Assert.ok(!inMemoTimer.enabled, "in memo timer enabled should be false with no events in memory and no processTelemtry is called"); @@ -102,11 +108,15 @@ export class Offlinetimer extends AITestClass { channel.processTelemetry(validEvt); inMemoTimer = channel["_getDbgPlgTargets"]()[3]; Assert.ok(inMemoTimer.enabled, "in memo timer should be enabled"); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; + //inMemoBatch = channel["_getDbgPlgTargets"]()[1]; Assert.equal(inMemoBatch && inMemoBatch.count(), 1, "should have one event left after flush"); this.clock.tick(2000); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; + //inMemoBatch = channel["_getDbgPlgTargets"]()[1]; Assert.equal(inMemoBatch && inMemoBatch.count(), 0, "should have no event left"); inMemoTimer = channel["_getDbgPlgTargets"]()[3]; Assert.ok(!inMemoTimer.enabled, "in memo timer should be canceld with no events in memory test1"); @@ -115,12 +125,16 @@ export class Offlinetimer extends AITestClass { channel.processTelemetry(validEvt); inMemoTimer = channel["_getDbgPlgTargets"]()[3]; Assert.ok(inMemoTimer.enabled, "in memo timer should be enabled"); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + //inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch && inMemoBatch.count(), 1, "should have one event left after flush"); offlineListener.setOnlineState(1); this.clock.tick(2000); - inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + //inMemoBatch = channel["_getDbgPlgTargets"]()[1]; + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; Assert.equal(inMemoBatch && inMemoBatch.count(), 0, "should have no event left"); inMemoTimer = channel["_getDbgPlgTargets"]()[3]; Assert.ok(!inMemoTimer.enabled, "in memo timer should be canceld with no events in memory test2"); @@ -135,6 +149,118 @@ export class Offlinetimer extends AITestClass { } }); + this.testCase({ + name: "InMemo Timer: Handle in memory timer with splitEvts set to true", + useFakeTimers: true, + test: () => { + this.coreConfig.extensionConfig = {["OfflineChannel"]: {providers:[eStorageProviders.LocalStorage], inMemoMaxTime: 2000, eventsLimitInMem: 1, splitEvts: true } as IOfflineChannelConfiguration}; + let channel = new OfflineChannel(); + let onlineChannel = new TestChannel(); + this.core.initialize(this.coreConfig,[channel, onlineChannel]); + + this.clock.tick(1); + let offlineListener = channel.getOfflineListener() as any; + + // online, processTelemetry is not called + offlineListener.setOnlineState(1); + let inMemoTimer = channel["_getDbgPlgTargets"]()[3]; + Assert.ok(!inMemoTimer, "in memo timer should be null"); + + // offline, processTelemetry is not called + offlineListener.setOnlineState(2); + inMemoTimer = channel["_getDbgPlgTargets"]()[3]; + Assert.ok(!inMemoTimer, "in memo timer should be null test1"); + + // online, processTelemetry is called + offlineListener.setOnlineState(1); + let evt = mockTelemetryItem(); + channel.processTelemetry(evt); + inMemoTimer = channel["_getDbgPlgTargets"]()[3]; + Assert.ok(!inMemoTimer, "in memo timer should be null test2"); + + // offline, processTelemetry is called with normal event + offlineListener.setOnlineState(2); + evt = mockTelemetryItem(); + channel.processTelemetry(evt); + inMemoTimer = channel["_getDbgPlgTargets"]()[3]; + Assert.ok(inMemoTimer, "in memo timer should be created test3"); + Assert.ok(inMemoTimer.enabled, "in memo timer should be enabled test1"); + let inMemoMap = channel["_getDbgPlgTargets"]()[1]; + let inMemoBatch = inMemoMap[EventPersistence.Normal]; + Assert.equal(inMemoBatch && inMemoBatch.count(), 1, "should have one normal event"); + + // offline, processTelemetry is called with critical event + offlineListener.setOnlineState(2); + let criticalEvt = mockTelemetryItem(2) as IPostTransmissionTelemetryItem; + channel.processTelemetry(criticalEvt); + inMemoTimer = channel["_getDbgPlgTargets"]()[3]; + Assert.ok(inMemoTimer, "in memo timer should be not null"); + Assert.ok(inMemoTimer.enabled, "in memo timer should be enabled test2"); + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Critical]; + Assert.equal(inMemoBatch && inMemoBatch.count(), 1, "should have one critical event"); + + // offline, processTelemetry is called with normal event again + offlineListener.setOnlineState(2); + evt = mockTelemetryItem(); + channel.processTelemetry(evt); + inMemoTimer = channel["_getDbgPlgTargets"]()[3]; + Assert.ok(inMemoTimer, "in memo timer should not be null test1"); + Assert.ok(inMemoTimer.enabled, "in memo timer should be enabled test2"); + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; + Assert.equal(inMemoBatch && inMemoBatch.count(), 1, "should have one normal event test1"); + inMemoBatch = inMemoMap[EventPersistence.Critical]; + Assert.equal(inMemoBatch && inMemoBatch.count(), 1, "should have one critical event test1"); + + // offline, flush all events in memory, and processTelemetry is not called again + this.clock.tick(2000); + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; + Assert.equal(inMemoBatch && inMemoBatch.count(), 0, "should have no noraml event left"); + inMemoBatch = inMemoMap[EventPersistence.Critical]; + Assert.equal(inMemoBatch && inMemoBatch.count(), 0, "should have no critical event left"); + inMemoTimer = channel["_getDbgPlgTargets"]()[3]; + Assert.ok(!inMemoTimer.enabled, "in memo timer enabled should be false with no events in memory"); + + this.clock.tick(2000); + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; + Assert.equal(inMemoBatch && inMemoBatch.count(), 0, "should have no noraml event left test1"); + inMemoBatch = inMemoMap[EventPersistence.Critical]; + Assert.equal(inMemoBatch && inMemoBatch.count(), 0, "should have no critical event left tes1 "); + inMemoTimer = channel["_getDbgPlgTargets"]()[3]; + Assert.ok(!inMemoTimer.enabled, "in memo timer enabled should be false with no events in memory"); + inMemoTimer = channel["_getDbgPlgTargets"]()[3]; + Assert.ok(!inMemoTimer.enabled, "in memo timer enabled should be false with no events in memory and no processTelemtry is called"); + + + // offline with one normal event saved in memory, and then online with processTelemetry called + channel.processTelemetry(evt); + inMemoTimer = channel["_getDbgPlgTargets"]()[3]; + Assert.ok(inMemoTimer.enabled, "in memo timer should be enabled"); + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; + Assert.equal(inMemoBatch && inMemoBatch.count(), 1, "should have one normal event left test1"); + + offlineListener.setOnlineState(1); + this.clock.tick(2000); + inMemoMap = channel["_getDbgPlgTargets"]()[1]; + inMemoBatch = inMemoMap[EventPersistence.Normal]; + Assert.equal(inMemoBatch && inMemoBatch.count(), 0, "should have no event left test1"); + inMemoTimer = channel["_getDbgPlgTargets"]()[3]; + Assert.ok(!inMemoTimer.enabled, "in memo timer should be canceld with no events in memory test2"); + + channel.processTelemetry(evt); + inMemoTimer = channel["_getDbgPlgTargets"]()[3]; + Assert.ok(!inMemoTimer.enabled, "in memo timer should be canceld when back online"); + + + channel.teardown(); + + } + }); + this.testCase({ name: "SendNextBatch Timer: Handle sendNextBatch timer", useFakeTimers: true, diff --git a/channels/offline-channel-js/src/Interfaces/IOfflineProvider.ts b/channels/offline-channel-js/src/Interfaces/IOfflineProvider.ts index bdc27e92a..82eda4edf 100644 --- a/channels/offline-channel-js/src/Interfaces/IOfflineProvider.ts +++ b/channels/offline-channel-js/src/Interfaces/IOfflineProvider.ts @@ -75,7 +75,9 @@ export interface IOfflineChannelConfiguration { indexedDbName?: string; /** - * [Optional] Identifies the maximum number of events to store in memory before sending to persistent storage. + * [Optional] Identifies the maximum number of events to store in each memory batch before sending to persistent storage. + * For versions > 3.3.2, new config splitEvts is added + * If splitEvts is set true, eventsLimitInMem will be applied to each persistent level batch */ eventsLimitInMem?: number; @@ -145,6 +147,15 @@ export interface IOfflineChannelConfiguration { */ overrideInstrumentationKey?: string; + /** + * Identifies when saving events into the persistent storage, events will be batched and saved separately based on persistence level + * this is useful to help reduce the loss of critical events during cleaning process + * but it will result in more frequest storage implementations. + * If it is set to false, all events will be saved into single in memory batch + * Default: false + */ + splitEvts?: boolean; + //TODO: add do sampling } diff --git a/channels/offline-channel-js/src/OfflineChannel.ts b/channels/offline-channel-js/src/OfflineChannel.ts index e4d133750..cebb6211d 100644 --- a/channels/offline-channel-js/src/OfflineChannel.ts +++ b/channels/offline-channel-js/src/OfflineChannel.ts @@ -13,7 +13,7 @@ import { eLoggingSeverity, mergeEvtNamespace, onConfigChange, runTargetUnload } from "@microsoft/applicationinsights-core-js"; import { IPromise, ITaskScheduler, createAsyncPromise, createTaskScheduler } from "@nevware21/ts-async"; -import { ITimerHandler, isFunction, isString, objDeepFreeze, scheduleTimeout } from "@nevware21/ts-utils"; +import { ITimerHandler, arrSlice, isFunction, isString, objDeepFreeze, objForEachKey, scheduleTimeout } from "@nevware21/ts-utils"; import { EVT_DISCARD_STR, EVT_SENT_STR, EVT_STORE_STR, batchDropNotification, callNotification, isGreaterThanZero, isValidPersistenceLevel } from "./Helpers/Utils"; @@ -33,6 +33,7 @@ const DefaultOfflineIdentifier = "OfflineChannel"; const DefaultBatchInterval = 15000; const DefaultInMemoMaxTime = 15000; const PostChannelIdentifier = "PostChannel"; +const PersistenceKeys = [EventPersistence.Critical, EventPersistence.Normal]; interface IUrlLocalStorageConfig { @@ -62,7 +63,8 @@ const defaultOfflineChannelConfig: IConfigDefaults maxSentBatchInterval: { isVal: isGreaterThanZero, v: DefaultBatchInterval}, primaryOnlineChannelId: [BreezeChannelIdentifier, PostChannelIdentifier], overrideInstrumentationKey: undefValue, - senderCfg: {} as IOfflineSenderConfig + senderCfg: {} as IOfflineSenderConfig, + splitEvts: false }); //TODO: add tests for sharedAnanlytics @@ -80,7 +82,6 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr // Internal properties used for tracking the current state, these are "true" internal/private properties for this instance let _hasInitialized; let _paused; - let _inMemoBatch: InMemoryBatch; let _sender: Sender; let _urlCfg: IUrlLocalStorageConfig; let _offlineListener: IOfflineListener; @@ -103,6 +104,8 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr let _notificationManager: INotificationManager | undefined; let _isLazyInit: boolean; let _dependencyPlugin: IChannelControls; + let _splitEvts: Boolean; + let _inMemoMap: { [priority: EventPersistence]: InMemoryBatch }; _initDefaults(); @@ -181,15 +184,15 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr //TODO: add function to better get level item.persistence = item.persistence || (item.baseData && item.baseData.persistence) || EventPersistence.Normal; // in case the level is in baseData - if (_shouldCacheEvent(_urlCfg, item) && _inMemoBatch) { + if (_shouldCacheEvent(_urlCfg, item) && _inMemoMap) { if (_overrideIkey) { item.iKey = _overrideIkey; } - let added = _inMemoBatch.addEvent(evt); + let added = _addEvtToMap(item); // inMemo is full if (!added) { - _flushInMemoItems(); - let retry = _inMemoBatch.addEvent(evt); + _flushInMemoItems(false, item.persistence); + let retry = _addEvtToMap(item); if (!retry) { _evtDropNotification([evt], EventsDiscardedReason.QueueFull); _throwInternal(_diagLogger, @@ -240,7 +243,7 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr _self.onunloadFlush = () => { if (!_paused) { - while (_inMemoBatch && _inMemoBatch.count()) { + while (_hasEvts()) { _flushInMemoItems(true); } // TODO: unloadprovider might send events out of order @@ -273,7 +276,7 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr _self["_getDbgPlgTargets"] = () => { - return [_urlCfg, _inMemoBatch, _senderInst, _inMemoFlushTimer, _sendNextBatchTimer]; + return [_urlCfg, _inMemoMap, _senderInst, _inMemoFlushTimer, _sendNextBatchTimer]; }; @@ -285,7 +288,6 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr _offlineListener = null; _diagLogger = null; _endpoint = null; - _inMemoBatch = null; _convertUndefined = undefValue; _maxBatchSize = null; _sendNextBatchTimer = null; @@ -299,6 +301,8 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr _evtsLimitInMemo = null; _isLazyInit = false; _dependencyPlugin = null; + _splitEvts= false; + _inMemoMap = null; } @@ -315,7 +319,8 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr if (!_inMemoFlushTimer) { _inMemoFlushTimer = scheduleTimeout(() => { _flushInMemoItems(); - if (_inMemoBatch && _inMemoBatch.count() && _inMemoFlushTimer) { + let hasEvts = _hasEvts(); + if (hasEvts && _inMemoFlushTimer) { _inMemoFlushTimer.refresh(); } _setSendNextTimer(); @@ -329,83 +334,99 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr } //flush only flush max batch size event, may still have events lefts - function _flushInMemoItems(unload?: boolean) { + function _flushInMemoItems(unload?: boolean, mapKey?: number | EventPersistence) { try { // TODO: add while loop to flush everything - let inMemo = _inMemoBatch; - let evts = inMemo && inMemo.getItems(); - if (!evts || !evts.length) { - return; - } - let payloadArr:string[] = []; - let size = 0; - let idx = -1; - let criticalCnt = 0; - arrForEach(evts, (evt, index) => { - let curEvt = evt as IPostTransmissionTelemetryItem - idx = index; - let payload = _getPayload(curEvt); - size += payload.length; - if (size > _maxBatchSize) { + let hasEvts = false; + // can use objForEachKey(_inMemoMap, (key)), but keys returned by this function is always string + arrForEach(PersistenceKeys, (key) => { + let inMemoBatch: InMemoryBatch = _inMemoMap[key]; + let inMemo = inMemoBatch; + let evts = inMemo && inMemo.getItems(); + if (!evts || !evts.length) { return; } - if(curEvt.persistence == EventPersistence.Critical) { - criticalCnt ++; + if (_splitEvts && mapKey && mapKey !== key) { + // if split evts is set to true + // specific mapkey is defined + // for key !== mapkey, only check if there are any events left in order to refresh timer + hasEvts = hasEvts || !!evts.length; + return; } - idx = index; - payloadArr.push(payload); - - }); - if (!payloadArr.length) { - return; - } - - let sentItems = evts.slice(0, idx + 1); - - _inMemoBatch = _inMemoBatch.createNew(_endpoint, inMemo.getItems().slice(idx + 1), _evtsLimitInMemo); - - let payloadData: IStorageTelemetryItem = null; - if (_offineSupport && _offineSupport.createOneDSPayload) { - payloadData = _offineSupport.createOneDSPayload(sentItems); - if (payloadData) { - payloadData.criticalCnt = criticalCnt; + let payloadArr:string[] = []; + let size = 0; + let idx = -1; + let criticalCnt = 0; + arrForEach(evts, (evt, index) => { + let curEvt = evt as IPostTransmissionTelemetryItem + idx = index; + let payload = _getPayload(curEvt); + size += payload.length; + if (size > _maxBatchSize) { + return; + } + if(curEvt.persistence == EventPersistence.Critical) { + criticalCnt ++; + } + idx = index; + payloadArr.push(payload); + + }); + if (!payloadArr.length) { + return; } - } else { - payloadData = _constructPayloadData(payloadArr, criticalCnt); - } - - - let callback: OfflineBatchStoreCallback = (res) => { - if (!res || !res.state) { - return null; + let sentItems = evts.slice(0, idx + 1); + if (idx + 1 < evts.length) { + // keep track if there is any remaining events + hasEvts = true; + } + _inMemoMap[key] = inMemoBatch.createNew(_endpoint, arrSlice(inMemo.getItems(),idx + 1), _evtsLimitInMemo); + + let payloadData: IStorageTelemetryItem = null; + if (_offineSupport && _offineSupport.createOneDSPayload) { + payloadData = _offineSupport.createOneDSPayload(sentItems); + if (payloadData) { + payloadData.criticalCnt = criticalCnt; + } + + } else { + payloadData = _constructPayloadData(payloadArr, criticalCnt); } - let state = res.state; - - if (state == eBatchStoreStatus.Failure) { - if (!unload) { - // for unload, just try to add each batch once - arrForEach(sentItems, (item) => { - _inMemoBatch.addEvent(item); - }); - _setupInMemoTimer(); + let callback: OfflineBatchStoreCallback = (res) => { + if (!res || !res.state) { + return null; + } + let state = res.state; + + if (state == eBatchStoreStatus.Failure) { + if (!unload) { + // for unload, just try to add each batch once + arrForEach(sentItems, (item) => { + _addEvtToMap(item); + }); + _setupInMemoTimer(); + + } else { + // unload, drop events + _evtDropNotification(sentItems, EventsDiscardedReason.NonRetryableStatus); + } + } else { - // unload, drop events - _evtDropNotification(sentItems, EventsDiscardedReason.NonRetryableStatus); + // if eBatchStoreStatus is success + _storeNotification(sentItems); } - - } else { - // if eBatchStoreStatus is success - _storeNotification(sentItems); + }; + + if (payloadData && _urlCfg && _urlCfg.batchHandler) { + let promise = _urlCfg.batchHandler.storeBatch(payloadData, callback, unload); + _queueStorageEvent("storeBatch", promise); } - }; - if (payloadData && _urlCfg && _urlCfg.batchHandler) { - let promise = _urlCfg.batchHandler.storeBatch(payloadData, callback, unload); - _queueStorageEvent("storeBatch", promise); - } - if (_inMemoBatch && !_inMemoBatch.count()) { + }); + // this is outside loop + if (!hasEvts) { _inMemoFlushTimer && _inMemoFlushTimer.cancel(); } @@ -632,14 +653,27 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr batchHandler: handler }; _evtsLimitInMemo = storageConfig.eventsLimitInMem; + _inMemoMap = _inMemoMap || {}; + let newMap = {}; // transfer previous events to new buffer - let evts = null; - let curEvts = _inMemoBatch && _inMemoBatch.getItems(); - if (curEvts && curEvts.length) { - evts = curEvts.slice(0); - _inMemoBatch.clear(); - } - _inMemoBatch = new InMemoryBatch(_self.diagLog(), curUrl, evts, _evtsLimitInMemo); + arrForEach(PersistenceKeys, (key) => { + // when init map, we will initize a in memo batch for each EventPersistence key + // evts to be transferred to new inMemo map + let evts = null; + + if ( _inMemoMap && _inMemoMap[key]) { + let inMemoBatch = _inMemoMap[key]; + let curEvts = inMemoBatch && inMemoBatch.getItems(); + if (curEvts && curEvts.length) { + evts = arrSlice(curEvts); + inMemoBatch && inMemoBatch.clear(); + } + } + newMap[key] = new InMemoryBatch(_self.diagLog(), curUrl, evts, _evtsLimitInMemo); + + + }); + _inMemoMap = newMap; _inMemoTimerOut = storageConfig.inMemoMaxTime; let onlineConfig = ctx.getExtCfg(_primaryChannelId, {}) || {}; _convertUndefined = onlineConfig.convertUndefined; @@ -647,6 +681,7 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr _setRetryTime(); _maxBatchInterval = storageConfig.maxSentBatchInterval; _maxBatchSize = storageConfig.maxBatchsize; + _splitEvts = storageConfig.splitEvts; } _urlCfg = urlConfig; _endpoint = curUrl; @@ -676,6 +711,29 @@ export class OfflineChannel extends BaseTelemetryPlugin implements IChannelContr return _dependencyPlugin; } + function _hasEvts() { + let hasEvts = false; + objForEachKey(_inMemoMap, (key) => { + let inMemoBatch = _inMemoMap[key]; + if (inMemoBatch && inMemoBatch.count()) { + hasEvts = true; + return -1; + } + }); + return hasEvts; + + } + + function _addEvtToMap(item:IPostTransmissionTelemetryItem) { + // if split evts is false, all events will be saved into EventPersistence.Normal batch + let mapKey = EventPersistence.Normal; + if (_splitEvts && item.persistence) { + mapKey = item.persistence; + } + let inMemoBatch = _inMemoMap[mapKey]; + return inMemoBatch.addEvent(item); + } + function _callNotification(evtName: string, theArgs: any[]) { callNotification(_notificationManager, evtName, theArgs); diff --git a/common/config/rush/npm-shrinkwrap.json b/common/config/rush/npm-shrinkwrap.json index 13707a6ec..a4be60c7d 100644 --- a/common/config/rush/npm-shrinkwrap.json +++ b/common/config/rush/npm-shrinkwrap.json @@ -1389,13 +1389,19 @@ } }, "node_modules/@shikijs/core": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.15.1.tgz", - "integrity": "sha512-DwkQTDNlhr7PwZMJswdvWIKts+2mqjIn8txByr88fhBRBtUSsIQR43RRoATjRrbeu4hyNTSTMBdxgp/vlxnxvA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.16.1.tgz", + "integrity": "sha512-aI0hBtw+a6KsJp2jcD4YuQqKpeCbURMZbhHVozDknJpm+KJqeMRkEnfBC8BaKE/5XC+uofPgCLsa/TkTk0Ba0w==", "dependencies": { + "@shikijs/vscode-textmate": "^9.2.0", "@types/hast": "^3.0.4" } }, + "node_modules/@shikijs/vscode-textmate": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.2.0.tgz", + "integrity": "sha512-5FinaOp6Vdh/dl4/yaOTh0ZeKch+rYS8DUb38V3GMKYVkdqzxw53lViRKUYkVILRiVQT7dcPC7VvAKOR73zVtQ==" + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -1627,16 +1633,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", - "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.4.0.tgz", + "integrity": "sha512-rg8LGdv7ri3oAlenMACk9e+AR4wUV0yrrG+XKsGKOK0EVgeEDqurkXMPILG2836fW4ibokTB5v4b6Z9+GYQDEw==", "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/type-utils": "8.3.0", - "@typescript-eslint/utils": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/scope-manager": "8.4.0", + "@typescript-eslint/type-utils": "8.4.0", + "@typescript-eslint/utils": "8.4.0", + "@typescript-eslint/visitor-keys": "8.4.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1660,15 +1666,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", - "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.4.0.tgz", + "integrity": "sha512-NHgWmKSgJk5K9N16GIhQ4jSobBoJwrmURaLErad0qlLjrpP5bECYg+wxVTGlGZmJbU03jj/dfnb6V9bw+5icsA==", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/typescript-estree": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/scope-manager": "8.4.0", + "@typescript-eslint/types": "8.4.0", + "@typescript-eslint/typescript-estree": "8.4.0", + "@typescript-eslint/visitor-keys": "8.4.0", "debug": "^4.3.4" }, "engines": { @@ -1688,13 +1694,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", - "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.4.0.tgz", + "integrity": "sha512-n2jFxLeY0JmKfUqy3P70rs6vdoPjHK8P/w+zJcV3fk0b0BwRXC/zxRTEnAsgYT7MwdQDt/ZEbtdzdVC+hcpF0A==", "peer": true, "dependencies": { - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0" + "@typescript-eslint/types": "8.4.0", + "@typescript-eslint/visitor-keys": "8.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1705,13 +1711,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz", - "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.4.0.tgz", + "integrity": "sha512-pu2PAmNrl9KX6TtirVOrbLPLwDmASpZhK/XU7WvoKoCUkdtq9zF7qQ7gna0GBZFN0hci0vHaSusiL2WpsQk37A==", "peer": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.3.0", - "@typescript-eslint/utils": "8.3.0", + "@typescript-eslint/typescript-estree": "8.4.0", + "@typescript-eslint/utils": "8.4.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1729,9 +1735,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", - "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.4.0.tgz", + "integrity": "sha512-T1RB3KQdskh9t3v/qv7niK6P8yvn7ja1mS7QK7XfRVL6wtZ8/mFs/FHf4fKvTA0rKnqnYxl/uHFNbnEt0phgbw==", "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1742,13 +1748,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", - "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.4.0.tgz", + "integrity": "sha512-kJ2OIP4dQw5gdI4uXsaxUZHRwWAGpREJ9Zq6D5L0BweyOrWsL6Sz0YcAZGWhvKnH7fm1J5YFE1JrQL0c9dd53A==", "peer": true, "dependencies": { - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/types": "8.4.0", + "@typescript-eslint/visitor-keys": "8.4.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1806,15 +1812,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", - "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.4.0.tgz", + "integrity": "sha512-swULW8n1IKLjRAgciCkTCafyTHHfwVQFt8DovmaF69sKbOxTSFMmIZaSHjqO9i/RV0wIblaawhzvtva8Nmm7lQ==", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/typescript-estree": "8.3.0" + "@typescript-eslint/scope-manager": "8.4.0", + "@typescript-eslint/types": "8.4.0", + "@typescript-eslint/typescript-estree": "8.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1828,12 +1834,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", - "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.4.0.tgz", + "integrity": "sha512-zTQD6WLNTre1hj5wp09nBIDiOc2U5r/qmzo7wxPn4ZgAjHql09EofqhF9WF+fZHzL5aCyaIpPcT2hyxl73kr9A==", "peer": true, "dependencies": { - "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/types": "8.4.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -3133,9 +3139,9 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.0.tgz", + "integrity": "sha512-bmwQPHFq/qiWp9CbNbCQU73klT+i5qwP/0tah3MGHp26vUt2YV4WkdtXRqOZo+H+4m38k8epFHOvO4BRuAuohw==", "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -4957,9 +4963,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -5802,11 +5808,12 @@ } }, "node_modules/shiki": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.15.1.tgz", - "integrity": "sha512-QPtVwbafyHmH9Z90iEZgZL4BhqFh5RMnRq2Bic0Cqp5lgbpbkn4nNmed0zzXbh/yPFs2PpkCviM9qcrbN+9zAA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.16.1.tgz", + "integrity": "sha512-tCJIMaxDVB1mEIJ5TvfZU7kCPB5eo9fli5+21Olc/bmyv+w8kye3JOp+LZRmGkAyT71hrkefQhTiY+o9mBikRQ==", "dependencies": { - "@shikijs/core": "1.15.1", + "@shikijs/core": "1.16.1", + "@shikijs/vscode-textmate": "^9.2.0", "@types/hast": "^3.0.4" } }, @@ -7569,13 +7576,19 @@ } }, "@shikijs/core": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.15.1.tgz", - "integrity": "sha512-DwkQTDNlhr7PwZMJswdvWIKts+2mqjIn8txByr88fhBRBtUSsIQR43RRoATjRrbeu4hyNTSTMBdxgp/vlxnxvA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.16.1.tgz", + "integrity": "sha512-aI0hBtw+a6KsJp2jcD4YuQqKpeCbURMZbhHVozDknJpm+KJqeMRkEnfBC8BaKE/5XC+uofPgCLsa/TkTk0Ba0w==", "requires": { + "@shikijs/vscode-textmate": "^9.2.0", "@types/hast": "^3.0.4" } }, + "@shikijs/vscode-textmate": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.2.0.tgz", + "integrity": "sha512-5FinaOp6Vdh/dl4/yaOTh0ZeKch+rYS8DUb38V3GMKYVkdqzxw53lViRKUYkVILRiVQT7dcPC7VvAKOR73zVtQ==" + }, "@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -7798,16 +7811,16 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", - "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.4.0.tgz", + "integrity": "sha512-rg8LGdv7ri3oAlenMACk9e+AR4wUV0yrrG+XKsGKOK0EVgeEDqurkXMPILG2836fW4ibokTB5v4b6Z9+GYQDEw==", "peer": true, "requires": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/type-utils": "8.3.0", - "@typescript-eslint/utils": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/scope-manager": "8.4.0", + "@typescript-eslint/type-utils": "8.4.0", + "@typescript-eslint/utils": "8.4.0", + "@typescript-eslint/visitor-keys": "8.4.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -7815,54 +7828,54 @@ } }, "@typescript-eslint/parser": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", - "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.4.0.tgz", + "integrity": "sha512-NHgWmKSgJk5K9N16GIhQ4jSobBoJwrmURaLErad0qlLjrpP5bECYg+wxVTGlGZmJbU03jj/dfnb6V9bw+5icsA==", "peer": true, "requires": { - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/typescript-estree": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/scope-manager": "8.4.0", + "@typescript-eslint/types": "8.4.0", + "@typescript-eslint/typescript-estree": "8.4.0", + "@typescript-eslint/visitor-keys": "8.4.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", - "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.4.0.tgz", + "integrity": "sha512-n2jFxLeY0JmKfUqy3P70rs6vdoPjHK8P/w+zJcV3fk0b0BwRXC/zxRTEnAsgYT7MwdQDt/ZEbtdzdVC+hcpF0A==", "peer": true, "requires": { - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0" + "@typescript-eslint/types": "8.4.0", + "@typescript-eslint/visitor-keys": "8.4.0" } }, "@typescript-eslint/type-utils": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz", - "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.4.0.tgz", + "integrity": "sha512-pu2PAmNrl9KX6TtirVOrbLPLwDmASpZhK/XU7WvoKoCUkdtq9zF7qQ7gna0GBZFN0hci0vHaSusiL2WpsQk37A==", "peer": true, "requires": { - "@typescript-eslint/typescript-estree": "8.3.0", - "@typescript-eslint/utils": "8.3.0", + "@typescript-eslint/typescript-estree": "8.4.0", + "@typescript-eslint/utils": "8.4.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", - "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.4.0.tgz", + "integrity": "sha512-T1RB3KQdskh9t3v/qv7niK6P8yvn7ja1mS7QK7XfRVL6wtZ8/mFs/FHf4fKvTA0rKnqnYxl/uHFNbnEt0phgbw==", "peer": true }, "@typescript-eslint/typescript-estree": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", - "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.4.0.tgz", + "integrity": "sha512-kJ2OIP4dQw5gdI4uXsaxUZHRwWAGpREJ9Zq6D5L0BweyOrWsL6Sz0YcAZGWhvKnH7fm1J5YFE1JrQL0c9dd53A==", "peer": true, "requires": { - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/types": "8.4.0", + "@typescript-eslint/visitor-keys": "8.4.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -7898,24 +7911,24 @@ } }, "@typescript-eslint/utils": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", - "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.4.0.tgz", + "integrity": "sha512-swULW8n1IKLjRAgciCkTCafyTHHfwVQFt8DovmaF69sKbOxTSFMmIZaSHjqO9i/RV0wIblaawhzvtva8Nmm7lQ==", "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/typescript-estree": "8.3.0" + "@typescript-eslint/scope-manager": "8.4.0", + "@typescript-eslint/types": "8.4.0", + "@typescript-eslint/typescript-estree": "8.4.0" } }, "@typescript-eslint/visitor-keys": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", - "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.4.0.tgz", + "integrity": "sha512-zTQD6WLNTre1hj5wp09nBIDiOc2U5r/qmzo7wxPn4ZgAjHql09EofqhF9WF+fZHzL5aCyaIpPcT2hyxl73kr9A==", "peer": true, "requires": { - "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/types": "8.4.0", "eslint-visitor-keys": "^3.4.3" } }, @@ -8848,9 +8861,9 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.0.tgz", + "integrity": "sha512-bmwQPHFq/qiWp9CbNbCQU73klT+i5qwP/0tah3MGHp26vUt2YV4WkdtXRqOZo+H+4m38k8epFHOvO4BRuAuohw==", "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -10223,9 +10236,9 @@ "integrity": "sha512-L7MXxUDtqr4PUaLFCDCXBfGV/6KLIuSEccizDI7JxT+c9x1G1v04BQ4+4oag84SHaCdrBgQAIs/Cqn+flwFPng==" }, "picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "picomatch": { "version": "2.3.1", @@ -10838,11 +10851,12 @@ "peer": true }, "shiki": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.15.1.tgz", - "integrity": "sha512-QPtVwbafyHmH9Z90iEZgZL4BhqFh5RMnRq2Bic0Cqp5lgbpbkn4nNmed0zzXbh/yPFs2PpkCviM9qcrbN+9zAA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.16.1.tgz", + "integrity": "sha512-tCJIMaxDVB1mEIJ5TvfZU7kCPB5eo9fli5+21Olc/bmyv+w8kye3JOp+LZRmGkAyT71hrkefQhTiY+o9mBikRQ==", "requires": { - "@shikijs/core": "1.15.1", + "@shikijs/core": "1.16.1", + "@shikijs/vscode-textmate": "^9.2.0", "@types/hast": "^3.0.4" } },