Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AISKU/Tests/Unit/src/AISKUSize.Tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly:
}

export class AISKUSizeCheck extends AITestClass {
private readonly MAX_RAW_SIZE = 182;
private readonly MAX_BUNDLE_SIZE = 183;
private readonly MAX_RAW_SIZE = 183;
private readonly MAX_BUNDLE_SIZE = 184;
private readonly MAX_RAW_DEFLATE_SIZE = 74;
private readonly MAX_BUNDLE_DEFLATE_SIZE = 74;
private readonly rawFilePath = "../dist/es5/applicationinsights-web.min.js";
Expand Down
1 change: 1 addition & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
legacy key remains supported for customer compatibility and uses the same endpoint safety checks.
- Payload headers are isolated per request so a compressed asynchronous send cannot leak `Content-Encoding: gzip` into a later
uncompressed synchronous, unload, beacon, or fallback request.
- Skip channel telemetry reconstruction when the notification manager confirms there are no consumers for the relevant sent, discarded, or retry notification. Add the optional `hasListener(name: string)` hint to the core notification manager. Custom managers without this hint and overridden built-in notification handlers retain the existing dispatch behavior. Notifications with matching listeners, including SDK Stats, retain the existing reconstruction behavior.

## 3.4.5 (October 5th, 2026)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { AITestClass, Assert, PollingAssert } from "@microsoft/ai-test-framework";
import {
AppInsightsCore, createStatsMgr, FeatureOptInMode, IAppInsightsCore, IConfiguration, IInternalSdkStatsState,
AppInsightsCore, createSdkStatsNotifCbk, createStatsMgr, FeatureOptInMode, IAppInsightsCore, IConfiguration, IInternalSdkStatsState,
isBeaconsSupported, IStatsMgr, ITelemetryItem, TransportType
} from "@microsoft/applicationinsights-core-js";
import { objDefine } from "@nevware21/ts-utils";
import { arrForEach, objDefine } from "@nevware21/ts-utils";
import { SinonSpy } from "sinon";
import { Sender } from "../../../src/Sender";
import { ISenderConfig } from "../../../types/applicationinsights-channel-js";
Expand Down Expand Up @@ -281,6 +281,39 @@ export class InternalSdkStatsTests extends AITestClass {
}
});

this.testCase({
name: "SDK Stats counts network responses independently of notification listeners",
useFakeTimers: true,
useFakeServer: true,
fakeServerAutoRespond: false,
test: () => {
const config = this.createSenderConfig(TransportType.Xhr);
const { core, sender } = this.initializeCoreAndSender(config, "000e0000-e000-0000-a000-000000000000");
const checkResponses = () => {
arrForEach([200, 400, 503], (status) => {
sender._buffer.clear();
this.internalSdkStatsCountSpy.resetHistory();
this.processTelemetryAndFlush(sender, {
name: "customer event", baseType: "EventData", baseData: { name: "customer event" }
});
this.sendJsonResponse(this._getXhrRequests().slice(-1)[0], {}, status);
this.clock.tick(1);
this.assertInternalSdkStatsCall(status);
});
};

QUnit.assert.equal(core.getNotifyMgr().listeners.length, 0, "Start without notification listeners");
checkResponses();
core.addNotificationListener({ perfEvent: () => {} });
checkResponses();

core.config.extensionConfig[sender.identifier].enableSessionStorageBuffer = false;
this.clock.tick(1);
core.addNotificationListener(createSdkStatsNotifCbk(core, "test"));
checkResponses();
}
});

this.testCase({
name: "SDK Stats increments success count for xhr sender",
useFakeTimers: true,
Expand Down

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions channels/applicationinsights-channel-js/src/Sender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,9 +833,10 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls {

_self._buffer && _self._buffer.clearSent(payload);

// Notify listeners of discarded events (skip extraction entirely when no listeners are registered)
// Only the manager can determine whether its notification has consumers.
let mgr = _getNotifyMgr();
if (mgr && mgr.listeners && mgr.listeners.length) {
if (mgr && mgr.listeners && mgr.listeners.length &&
(!mgr.hasListener || mgr.hasListener("eventsDiscarded") !== false)) {
let items = _extractTelemetryItems(payload);
if (items) {
mgr.eventsDiscarded(items, 1 /* NonRetryableStatus */, statusCode);
Expand Down Expand Up @@ -887,9 +888,10 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls {
function _onSuccess(payload: IInternalStorageItem[], countOfItemsInPayload: number) {
_self._buffer && _self._buffer.clearSent(payload);

// Notify listeners of successful send (skip extraction entirely when no listeners are registered)
// Only the manager can determine whether its notification has consumers.
let mgr = _getNotifyMgr();
if (mgr && mgr.listeners && mgr.listeners.length) {
if (mgr && mgr.listeners && mgr.listeners.length &&
(!mgr.hasListener || mgr.hasListener("eventsSent") !== false)) {
let items = _extractTelemetryItems(payload);
if (items) {
mgr.eventsSent(items);
Expand Down Expand Up @@ -1460,7 +1462,8 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls {
*/
function _notifyRetry(payload: IInternalStorageItem[], statusCode: number) {
let mgr = _getNotifyMgr();
if (mgr && mgr.eventsRetry && mgr.listeners && mgr.listeners.length) {
if (mgr && mgr.eventsRetry && mgr.listeners && mgr.listeners.length &&
(!mgr.hasListener || mgr.hasListener("eventsRetry") !== false)) {
let items = _extractTelemetryItems(payload);
if (items) {
mgr.eventsRetry(items, statusCode);
Expand Down
2 changes: 1 addition & 1 deletion common/config/rush/pnpm-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"form-data": ">=2.5.6 <3.0.0",
"js-yaml": ">=4.3.1 <5.0.0",
"markdown-it": ">=14.2.0",
"decode-uri-component": ">=0.5.0",
"decode-uri-component": "0.5.0",
"@microsoft/api-extractor": "7.58.7",
"basic-ftp": ">=6.2.1",
"brace-expansion": ">=5.0.12 <6.0.0",
Expand Down
2 changes: 1 addition & 1 deletion common/config/rush/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly:
}

export class AppInsightsCoreSizeCheck extends AITestClass {
private readonly MAX_RAW_SIZE = 138;
private readonly MAX_RAW_SIZE = 139;
private readonly MAX_BUNDLE_SIZE = 139;
private readonly MAX_RAW_DEFLATE_SIZE = 56;
private readonly MAX_BUNDLE_DEFLATE_SIZE = 56;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import dynamicProto from "@microsoft/dynamicproto-js";
import { AITestClass, Assert } from "@microsoft/ai-test-framework";
import { arrForEach, objCreate } from "@nevware21/ts-utils";
import { NotificationManager } from "../../../../src/core/NotificationManager";
import { INotificationListener } from "../../../../src/interfaces/ai/INotificationListener";
import { INotificationManager } from "../../../../src/interfaces/ai/INotificationManager";
import { ITelemetryItem } from "../../../../src/interfaces/ai/ITelemetryItem";

class CustomNotificationManager extends NotificationManager {
constructor(callback: (events: ITelemetryItem[]) => void) {
super();
dynamicProto(CustomNotificationManager, this, (_self, _base) => {
_self.eventsSent = (events) => {
callback(events);
_base.eventsSent(events);
};
});
}
}

export class NotificationManagerTests extends AITestClass {
public registerTests() {
const notifications: Array<"eventsSent" | "eventsDiscarded" | "eventsRetry"> = [
"eventsSent", "eventsDiscarded", "eventsRetry"
];
arrForEach(notifications, (name) => {
this.testCase({
name: "NotificationManager: hasListener tracks listeners and instance overrides for " + name,
useFakeTimers: true,
test: () => {
const manager = new NotificationManager();
this.onDone(() => manager.unload(false));
Assert.equal(false, manager.hasListener(name), "No consumers before the first dispatch");
const unrelated: INotificationListener = { perfEvent: () => {} };
arrForEach(notifications, (other) => {
if (other !== name) {
unrelated[other] = this.sandbox.spy();
}
});
manager.addNotificationListener(unrelated);
Assert.equal(false, manager.hasListener(name), "Other notification callbacks are not consumers");

const callback = this.sandbox.spy();
const listener: INotificationListener = {};
listener[name] = callback;
manager.addNotificationListener(listener);
Assert.equal(true, manager.hasListener(name), "A matching callback is a consumer");
Assert.equal(0, callback.callCount, "Querying does not invoke the callback");
manager[name].call(manager, [], 1, 400);
Assert.equal(0, callback.callCount, "Dispatch remains asynchronous");
this.clock.tick(1);
Assert.equal(1, callback.callCount, "Dispatch reaches the matching listener");
Assert.equal(true, manager.hasListener(name), "Recognize the handler after DynamicProto caches it");

delete listener[name];
Assert.equal(false, manager.hasListener(name), "Deleting a callback takes effect immediately");
listener[name] = callback;
Assert.equal(true, manager.hasListener(name), "Restoring a callback takes effect immediately");
manager.removeNotificationListener(listener);
Assert.equal(false, manager.hasListener(name), "Removing the listener stops notification work");

const override = this.sandbox.stub(manager, name);
Assert.equal(true, manager.hasListener(name), "An instance override may handle notifications directly");
override.restore();
Assert.equal(false, manager.hasListener(name), "Restoring the built-in handler restores the optimization");
}
});

this.testCase({
name: "NotificationManager: hasListener preserves prototype overrides for " + name,
test: () => {
const manager = new NotificationManager();
this.onDone(() => manager.unload(false));
Assert.equal(false, manager.hasListener(name), "Unmodified manager has no consumers");
const override = this.sandbox.stub(NotificationManager.prototype, name);
Assert.equal(true, manager.hasListener(name), "A prototype override may handle notifications directly");
override.restore();
Assert.equal(false, manager.hasListener(name), "Restoring the prototype restores the optimization");
}
});
});

arrForEach([
"eventsSendRequest", "perfEvent", "offlineEventsStored", "offlineBatchSent", "offlineBatchDrop", "unload",
"futureNotification"
], (name) => {
this.testCase({
name: "NotificationManager: hasListener accepts other notification names: " + name,
test: () => {
const manager = new NotificationManager();
this.onDone(() => manager.unload(false));
Assert.equal(false, manager.hasListener(name), "No matching consumers");
const listener: INotificationListener & { futureNotification?: () => void } = {};
listener[name] = this.sandbox.spy();
manager.addNotificationListener(listener);
Assert.equal(true, manager.hasListener(name), "A matching callback is a consumer");
Assert.equal(0, listener[name].callCount, "Querying does not dispatch the notification");
delete listener[name];
Assert.equal(false, manager.hasListener(name), "Deleting the callback takes effect immediately");
listener[name] = this.sandbox.spy();
manager.removeNotificationListener(listener);
Assert.equal(false, manager.hasListener(name), "Removing the listener stops notification work");
}
});
});

this.testCase({
name: "NotificationManager: optional hint preserves interface-to-class compatibility",
test: () => {
const interfaceManager: INotificationManager = new NotificationManager();
const manager: NotificationManager = interfaceManager;
this.onDone(() => manager.unload(false));
Assert.equal(false, manager.hasListener("eventsSent"), "The built-in manager implements the optional hint");
manager.hasListener = undefined;
const legacyManager: INotificationManager = manager;
Assert.equal(undefined, legacyManager.hasListener, "Managers without the hint remain supported");
}
});

this.testCase({
name: "NotificationManager: inherited hints do not suppress wrapper notifications",
test: () => {
const manager = new NotificationManager();
const wrapper: NotificationManager = objCreate(manager);
this.onDone(() => manager.unload(false));
wrapper.eventsSent = this.sandbox.spy();
Assert.equal(false, manager.hasListener("eventsSent"), "The original manager has no consumers");
Assert.equal(true, wrapper.hasListener("eventsSent"), "An inherited hint cannot describe a wrapper's consumers");
}
});

this.testCase({
name: "NotificationManager: hasListener preserves subclass overrides",
useFakeTimers: true,
test: () => {
const callback = this.sandbox.spy();
const manager = new CustomNotificationManager(callback);
const other = new NotificationManager();
this.onDone(() => {
manager.unload(false);
other.unload(false);
});
Assert.equal(true, manager.hasListener("eventsSent"), "A subclass can consume events without listeners");
Assert.equal(false, other.hasListener("eventsSent"), "Other instances still skip unused notifications");
manager.eventsSent([]);
Assert.equal(1, callback.callCount, "The subclass handles the notification");
Assert.equal(true, manager.hasListener("eventsSent"), "The cached subclass implementation remains a consumer");

const listener = { eventsSent: this.sandbox.spy() };
manager.addNotificationListener(listener);
manager.eventsSent([]);
this.clock.tick(1);
Assert.equal(2, callback.callCount, "The subclass still handles the notification");
Assert.equal(1, listener.eventsSent.callCount, "Delegating to the base preserves listener delivery");
}
});
}
}
2 changes: 2 additions & 0 deletions shared/AppInsightsCore/Tests/Unit/src/aiunittests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { ThrottleMgrTest } from "./ai/ThrottleMgr.tests";
import { UtilTests } from "./ai/Util.tests";
import { W3CTraceStateModesTests } from "./trace/W3CTraceStateModes.tests";
import { SdkStatsNotificationCbkTests } from "./ai/SdkStatsNotificationCbk.Tests";
import { NotificationManagerTests } from "./ai/NotificationManager.Tests";

// 1ds core tests
import { CoreTest } from './ext/CoreTest';
Expand Down Expand Up @@ -68,6 +69,7 @@ export function runTests() {
new InternalSdkStatsTests(true).registerTests();
new SendPostManagerTests().registerTests();
new SdkStatsNotificationCbkTests().registerTests();
new NotificationManagerTests().registerTests();

// Application Insights Common tests (merged from AppInsightsCommon)
new ApplicationInsightsTests().registerTests();
Expand Down
Loading
Loading