Skip to content
54 changes: 44 additions & 10 deletions AISKU/src/AISku.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import {
IAutoExceptionTelemetry, IChannelControls, IConfig, IConfigDefaults, IConfiguration, ICookieMgr, ICustomProperties, IDependencyTelemetry,
IDiagnosticLogger, IDistributedTraceContext, IDynamicConfigHandler, IEventTelemetry, IExceptionTelemetry, ILoadedPlugin,
IMetricTelemetry, INotificationManager, IOTelApi, IOTelSpanOptions, IPageViewPerformanceTelemetry, IPageViewTelemetry, IPlugin,
IReadableSpan, IRequestHeaders, ISpanScope, ITelemetryContext as Common_ITelemetryContext, ITelemetryInitializerHandler, ITelemetryItem,
ITelemetryPlugin, ITelemetryUnloadState, IThrottleInterval, IThrottleLimit, IThrottleMgrConfig, ITraceApi, ITraceProvider,
ITraceTelemetry, IUnloadHook, OTelTimeInput, PropertiesPluginIdentifier, ThrottleMgr, UnloadHandler, WatcherFunction,
_eInternalMessageId, _throwInternal, addPageHideEventListener, addPageUnloadEventListener, cfgDfMerge, cfgDfValidate,
createDynamicConfig, createOTelApi, createProcessTelemetryContext, createTraceProvider, createUniqueNamespace, doPerf, eLoggingSeverity,
hasDocument, hasWindow, isArray, isFeatureEnabled, isFunction, isNullOrUndefined, isReactNative, isString, mergeEvtNamespace,
onConfigChange, parseConnectionString, proxyAssign, proxyFunctions, removePageHideEventListener, removePageUnloadEventListener, useSpan
IReadableSpan, IRequestHeaders, ISdkStatsNotifCbk, ISpanScope, ITelemetryContext as Common_ITelemetryContext,
ITelemetryInitializerHandler, ITelemetryItem, ITelemetryPlugin, ITelemetryUnloadState, IThrottleInterval, IThrottleLimit,
IThrottleMgrConfig, ITraceApi, ITraceProvider, ITraceTelemetry, IUnloadHook, OTelTimeInput, PropertiesPluginIdentifier, ThrottleMgr,
UnloadHandler, WatcherFunction, _eInternalMessageId, _throwInternal, addPageHideEventListener, addPageUnloadEventListener, cfgDfMerge,
cfgDfValidate, createDynamicConfig, createOTelApi, createProcessTelemetryContext, createSdkStatsNotifCbk, createTraceProvider,
createUniqueNamespace, doPerf, eLoggingSeverity, hasDocument, hasWindow, isArray, isFeatureEnabled, isFunction, isNullOrUndefined,
isReactNative, isString, mergeEvtNamespace, onConfigChange, parseConnectionString, proxyAssign, proxyFunctions,
removePageHideEventListener, removePageUnloadEventListener, useSpan
} from "@microsoft/applicationinsights-core-js";
import {
AjaxPlugin as DependenciesPlugin, DependencyInitializerFunction, DependencyListenerFunction, IDependencyInitializerHandler,
Expand Down Expand Up @@ -64,6 +65,9 @@ const IKEY_USAGE = "iKeyUsage";
const CDN_USAGE = "CdnUsage";
const SDK_LOADER_VER = "SdkLoaderVer";
const ZIP_PAYLOAD = "zipPayload";
const SDK_STATS = "SdkStats";
const SDK_STATS_VERSION = "#version#";
const SDK_STATS_FLUSH_INTERVAL = 900000; // 15 minutes in ms

const default_limit = {
samplingRate: 100,
Expand Down Expand Up @@ -93,7 +97,8 @@ const defaultConfigValues: IConfigDefaults<IConfiguration & IConfig> = {
[IKEY_USAGE]: {mode: FeatureOptInMode.enable}, //for versions after 3.1.2 (>= 3.2.0)
[CDN_USAGE]: {mode: FeatureOptInMode.disable},
[SDK_LOADER_VER]: {mode: FeatureOptInMode.disable},
[ZIP_PAYLOAD]: {mode: FeatureOptInMode.none}
[ZIP_PAYLOAD]: {mode: FeatureOptInMode.none},
[SDK_STATS]: {mode: FeatureOptInMode.enable}
},
throttleMgrCfg: cfgDfMerge<{[key:number]: IThrottleMgrConfig}>(
{
Expand Down Expand Up @@ -196,6 +201,7 @@ export class AppInsightsSku implements IApplicationInsights<IConfiguration & ICo
let _cdnSentMessage: boolean;
let _sdkVerSentMessage: boolean;
let _otelApi: ICachedValue<IOTelApi>;
let _sdkStatsListener: ISdkStatsNotifCbk;

dynamicProto(AppInsightsSku, this, (_self) => {
_initDefaults();
Expand Down Expand Up @@ -390,6 +396,19 @@ export class AppInsightsSku implements IApplicationInsights<IConfiguration & ICo
// initialize core
_core.initialize(_config, [ _sender, properties, dependencies, _analyticsPlugin, _cfgSyncPlugin], logger, notificationManager);

// Register SDK Stats notification listener (on by default)
if (isFeatureEnabled(SDK_STATS, _config, true)) {
_sdkStatsListener = createSdkStatsNotifCbk({
trk: function (item: ITelemetryItem) {
_core.track(item);
},
lang: "JavaScript",
ver: SDK_STATS_VERSION,
int: SDK_STATS_FLUSH_INTERVAL
});
_core.addNotificationListener(_sdkStatsListener);
}

// Initialize the initial OTel API
_otelApi = _initOTel(_self, "aisku", _onEnd, _onException);

Expand Down Expand Up @@ -592,10 +611,24 @@ export class AppInsightsSku implements IApplicationInsights<IConfiguration & ICo
}
}

_self.onunloadFlush(isAsync);

_removePageEventHandlers();

// Unload SDK Stats listener BEFORE flushing the channel.
// unload() calls core.track() to enqueue the accumulated metrics,
// then the single onunloadFlush below sends everything (regular telemetry + SDK stats).
if (_sdkStatsListener && _core) {
try {
_sdkStatsListener.unload();
_core.removeNotificationListener(_sdkStatsListener);
} catch (e) {
// Swallow any errors to ensure core.unload() is still called
}
_sdkStatsListener = null;
}

// Single flush sends both regular telemetry AND SDK stats metrics
_self.onunloadFlush(isAsync);

_core.unload && _core.unload(isAsync, _unloadCallback, cbTimeout);

return result;
Expand Down Expand Up @@ -710,6 +743,7 @@ export class AppInsightsSku implements IApplicationInsights<IConfiguration & ICo
_iKeySentMessage = false;
_cdnSentMessage = false;
_sdkVerSentMessage = false;
_sdkStatsListener = null;
_cfgSyncPlugin = new CfgSyncPlugin();
}

Expand Down
5 changes: 5 additions & 0 deletions channels/applicationinsights-channel-js/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export interface IInternalStorageItem {
* total retry count
*/
cnt?: number;
/**
* baseType of the original telemetry item, used for SDK stats telemetry_type mapping.
* @since 3.3.6
*/
bT?: string;
}

export interface ISenderConfig {
Expand Down
73 changes: 65 additions & 8 deletions channels/applicationinsights-channel-js/src/Sender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,8 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls {
_checkMaxSize(payload);
let payloadItem = {
item: payload,
cnt: 0 // inital cnt will always be 0
cnt: 0, // inital cnt will always be 0
bT: telemetryItem.baseType // store baseType for SDK stats telemetry_type mapping
} as IInternalStorageItem;

// enqueue the payload
Expand Down Expand Up @@ -626,12 +627,12 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls {
/**
* error handler
*/
_self._onError = (payload: IInternalStorageItem[] | string[], message: string, event?: ErrorEvent) => {
_self._onError = (payload: IInternalStorageItem[] | string[], message: string, event?: ErrorEvent, statusCode?: number) => {
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The public stub signature for _onError is missing the new statusCode parameter. The implementation at line 630 accepts a statusCode parameter, but the stub at line 1594 does not include it. Update the stub signature to include the optional statusCode parameter: public _onError(payload: string[] | IInternalStorageItem[], message: string, event?: ErrorEvent, statusCode?: number)

Copilot uses AI. Check for mistakes.
// since version 3.1.3, string[] is no-op
if (_isStringArr(payload)) {
return;
}
return _onError(payload as IInternalStorageItem[], message, event);
return _onError(payload as IInternalStorageItem[], message, event, statusCode);
};

/**
Expand Down Expand Up @@ -799,14 +800,23 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls {
/**
* error handler
*/
function _onError(payload: IInternalStorageItem[], message: string, event?: ErrorEvent) {
function _onError(payload: IInternalStorageItem[], message: string, event?: ErrorEvent, statusCode?: number) {
_throwInternal(_self.diagLog(),
eLoggingSeverity.WARNING,
_eInternalMessageId.OnError,
"Failed to send telemetry.",
{ message });

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

// Notify listeners of discarded events
let mgr = _getNotifyMgr();
if (mgr) {
let items = _extractTelemetryItems(payload);
if (items) {
mgr.eventsDiscarded(items, 1 /* NonRetryableStatus */, statusCode);
}
}
}
/**
* partial success handler
Expand Down Expand Up @@ -852,6 +862,15 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls {
*/
function _onSuccess(payload: IInternalStorageItem[], countOfItemsInPayload: number) {
_self._buffer && _self._buffer.clearSent(payload);

// Notify listeners of successful send
let mgr = _getNotifyMgr();
if (mgr) {
let items = _extractTelemetryItems(payload);
if (items) {
mgr.eventsSent(items);
}
}
}


Expand Down Expand Up @@ -1114,7 +1133,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls {
// Updates the end point url before retry
if(status === 301 || status === 307 || status === 308) {
if(!_checkAndUpdateEndPointUrl(responseUrl)) {
_self._onError(payload, errorMessage);
_self._onError(payload, errorMessage, undefined, status);
return;
}
}
Expand All @@ -1124,6 +1143,9 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls {
if (!_isRetryDisabled) {
const offlineBackOffMultiplier = 10; // arbritrary number
_resendPayload(payload, offlineBackOffMultiplier);

// Notify listeners of retry
_notifyRetry(payload, status);

_throwInternal(_self.diagLog(),
eLoggingSeverity.WARNING,
Expand All @@ -1133,12 +1155,16 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls {
}
if (!_isRetryDisabled && _isRetriable(status)) {
_resendPayload(payload);

// Notify listeners of retry
_notifyRetry(payload, status);

_throwInternal(_self.diagLog(),
eLoggingSeverity.WARNING,
_eInternalMessageId.TransmissionFailed, ". " +
"Response code " + status + ". Will retry to send " + payload.length + " items.");
} else {
_self._onError(payload, errorMessage);
_self._onError(payload, errorMessage, undefined, status);
}
} else {
// check if the xhr's responseURL or fetch's response.url is same as endpoint url
Expand All @@ -1153,7 +1179,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls {
if (response && !_isRetryDisabled) {
_self._onPartialSuccess(payload, response);
} else {
_self._onError(payload, errorMessage);
_self._onError(payload, errorMessage, undefined, status);
}
} else {
_consecutiveErrors = 0;
Expand Down Expand Up @@ -1388,6 +1414,37 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls {
}
}

/**
* Extracts minimal ITelemetryItem objects from IInternalStorageItem[] for notification dispatch.
* Uses the stored baseType (bT) to reconstruct telemetry items.
*/
function _extractTelemetryItems(payload: IInternalStorageItem[]): ITelemetryItem[] {
if (payload && payload.length) {
let items: ITelemetryItem[] = [];
arrForEach(payload, (p) => {
if (p) {
let baseType = p.bT || "EventData";
items.push({ name: baseType, baseType: baseType } as ITelemetryItem);
Comment on lines +1421 to +1427
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name field in extracted telemetry items is set to baseType (line 1427), which doesn't represent the actual telemetry item name. This creates synthetic items where name equals baseType (e.g., both set to "EventData"). While this may be intentional for notification purposes since the original name is not available in IInternalStorageItem, it could be misleading for listeners expecting accurate telemetry item information. Consider documenting this behavior in the function comment or using a more descriptive synthetic name pattern.

Copilot uses AI. Check for mistakes.
}
});
return items.length ? items : null;
}
return null;
}

/**
* Notify listeners of retry events.
*/
function _notifyRetry(payload: IInternalStorageItem[], statusCode: number) {
let mgr = _getNotifyMgr();
if (mgr && mgr.eventsRetry) {
let items = _extractTelemetryItems(payload);
if (items) {
mgr.eventsRetry(items, statusCode);
}
}
}



/**
Expand Down Expand Up @@ -1534,7 +1591,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls {
* @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[] | IInternalStorageItem[], message: string, event?: ErrorEvent) {
public _onError(payload: string[] | IInternalStorageItem[], message: string, event?: ErrorEvent, statusCode?: number) {
// @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging
}

Expand Down
Loading
Loading