diff --git a/AISKU/src/AISku.ts b/AISKU/src/AISku.ts index 51a55e209..9e3d43a0f 100644 --- a/AISKU/src/AISku.ts +++ b/AISKU/src/AISku.ts @@ -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, @@ -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, @@ -93,7 +97,8 @@ const defaultConfigValues: IConfigDefaults = { [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}>( { @@ -196,6 +201,7 @@ export class AppInsightsSku implements IApplicationInsights; + let _sdkStatsListener: ISdkStatsNotifCbk; dynamicProto(AppInsightsSku, this, (_self) => { _initDefaults(); @@ -390,6 +396,22 @@ export class AppInsightsSku implements IApplicationInsights { + if (p) { + items.push({ name: "", baseType: p.bT || "EventData" } as ITelemetryItem); + } + }); + 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); + } + } + } + /** diff --git a/shared/AppInsightsCore/src/constants/InternalConstants.ts b/shared/AppInsightsCore/src/constants/InternalConstants.ts index 12da0a457..f2c3258ff 100644 --- a/shared/AppInsightsCore/src/constants/InternalConstants.ts +++ b/shared/AppInsightsCore/src/constants/InternalConstants.ts @@ -19,6 +19,7 @@ export const STR_PRIORITY = "priority"; export const STR_EVENTS_SENT = "eventsSent"; export const STR_EVENTS_DISCARDED = "eventsDiscarded"; export const STR_EVENTS_SEND_REQUEST = "eventsSendRequest"; +export const STR_EVENTS_RETRY = "eventsRetry"; export const STR_PERF_EVENT = "perfEvent"; export const STR_OFFLINE_STORE = "offlineEventsStored"; export const STR_OFFLINE_SENT = "offlineBatchSent"; diff --git a/shared/AppInsightsCore/src/core/NotificationManager.ts b/shared/AppInsightsCore/src/core/NotificationManager.ts index d2a2834a5..e8397095f 100644 --- a/shared/AppInsightsCore/src/core/NotificationManager.ts +++ b/shared/AppInsightsCore/src/core/NotificationManager.ts @@ -5,7 +5,8 @@ import { IPromise, createAllPromise, createPromise, doAwaitResponse } from "@nev import { ITimerHandler, arrForEach, arrIndexOf, objDefine, safe, scheduleTimeout } from "@nevware21/ts-utils"; import { createDynamicConfig } from "../config/DynamicConfig"; import { - STR_EVENTS_DISCARDED, STR_EVENTS_SEND_REQUEST, STR_EVENTS_SENT, STR_OFFLINE_DROP, STR_OFFLINE_SENT, STR_OFFLINE_STORE, STR_PERF_EVENT + STR_EVENTS_DISCARDED, STR_EVENTS_RETRY, STR_EVENTS_SEND_REQUEST, STR_EVENTS_SENT, STR_OFFLINE_DROP, STR_OFFLINE_SENT, STR_OFFLINE_STORE, + STR_PERF_EVENT } from "../constants/InternalConstants"; import { IConfiguration } from "../interfaces/ai/IConfiguration"; import { INotificationListener } from "../interfaces/ai/INotificationListener"; @@ -147,6 +148,17 @@ export class NotificationManager implements INotificationManager { } }; + /** + * Notification for events being retried. + * @param events - The array of events that are being retried. + * @param statusCode - The HTTP status code that triggered the retry. + */ + _self.eventsRetry = (events: ITelemetryItem[], statusCode: number): void => { + _runListeners(_listeners, STR_EVENTS_RETRY, _asyncNotifications, (listener) => { + listener.eventsRetry(events, statusCode); + }); + }; + _self.offlineEventsStored = (events: ITelemetryItem[]): void => { if (events && events.length) { _runListeners(_listeners, STR_OFFLINE_STORE, _asyncNotifications, (listener) => { @@ -254,6 +266,15 @@ export class NotificationManager implements INotificationManager { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } + /** + * Notification for events being retried. + * @param events - The array of events that are being retried. + * @param statusCode - The HTTP status code that triggered the retry. + */ + eventsRetry?(events: ITelemetryItem[], statusCode: number): void { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } + /** * [Optional] This event is sent if you have enabled perf events, they are primarily used to track internal performance testing and debugging * the event can be displayed via the debug plugin extension. diff --git a/shared/AppInsightsCore/src/core/SdkStatsNotificationCbk.ts b/shared/AppInsightsCore/src/core/SdkStatsNotificationCbk.ts new file mode 100644 index 000000000..74da84fad --- /dev/null +++ b/shared/AppInsightsCore/src/core/SdkStatsNotificationCbk.ts @@ -0,0 +1,263 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +"use strict"; + +import { ITimerHandler, objCreate, objHasOwn, scheduleTimeout } from "@nevware21/ts-utils"; +import { INotificationListener } from "../interfaces/ai/INotificationListener"; +import { ITelemetryItem } from "../interfaces/ai/ITelemetryItem"; + +var FLUSH_INTERVAL = 900000; // 15 min default +var MET_SUCCESS = "Item_Success_Count"; +var MET_DROPPED = "Item_Dropped_Count"; +var MET_RETRY = "Item_Retry_Count"; +var P_LANG = "language"; +var P_VER = "version"; +var P_COMPUTE = "computeType"; +var P_TEL_TYPE = "telemetry_type"; +var P_DROP_CODE = "drop.code"; +var P_RETRY_CODE = "retry.code"; +var DROP_CLIENT_EXCEPTION = "CLIENT_EXCEPTION"; + +// Map baseType to spec telemetry_type values +var _typeMap: { [key: string]: string } = { + "EventData": "CUSTOM_EVENT", + "MetricData": "CUSTOM_METRIC", + "RemoteDependencyData": "DEPENDENCY", + "ExceptionData": "EXCEPTION", + "PageviewData": "PAGE_VIEW", + "PageviewPerformanceData": "PAGE_VIEW", + "MessageData": "TRACE", + "RequestData": "REQUEST", + "AvailabilityData": "AVAILABILITY" +}; + +/** + * Configuration interface for the SDK Stats notification callback. + */ +export interface ISdkStatsConfig { + /** + * The track function to call when flushing metrics. Typically core.track(). + */ + trk: (item: ITelemetryItem) => void; + /** + * SDK language identifier, e.g. "JavaScript" + */ + lang: string; + /** + * SDK version string. + */ + ver: string; + /** + * Flush interval override in ms (default 900000 = 15 min). + */ + int?: number; + /** + * Optional callback to flush the channel after enqueuing SDK stats metrics. + * Called by unload() after core.track() so the metrics get transmitted before teardown. + */ + fnFlush?: () => void; +} + +/** + * Extended INotificationListener interface for SDK Stats that includes flush and unload operations. + */ +export interface ISdkStatsNotifCbk extends INotificationListener { + /** + * Flush accumulated counts and emit metrics via the configured track function. + */ + flush: () => void; + /** + * Flush remaining counts and cancel the timer. + */ + unload: () => void; +} + +/** + * Creates an INotificationListener that accumulates success/dropped/retry counts and periodically + * flushes them as Item_Success_Count, Item_Dropped_Count, and Item_Retry_Count metrics via core.track(). + * @param cfg - The SDK stats configuration + * @returns An INotificationListener with flush and unload methods + */ +/*#__NO_SIDE_EFFECTS__*/ +export function createSdkStatsNotifCbk(cfg: ISdkStatsConfig): ISdkStatsNotifCbk { + var _successCounts: { [telType: string]: number } = objCreate(null); + var _droppedCounts: { [code: string]: { [telType: string]: number } } = objCreate(null); + var _retryCounts: { [code: string]: { [telType: string]: number } } = objCreate(null); + var _timer: ITimerHandler; + var _interval = cfg.int || FLUSH_INTERVAL; + + function _ensureTimer() { + if (!_timer) { + _timer = scheduleTimeout(_flush, _interval); + } + } + + function _getTelType(item: ITelemetryItem): string { + var bt = item.baseType; + return (bt && objHasOwn(_typeMap, bt) && _typeMap[bt]) || "CUSTOM_EVENT"; + } + + function _isSdkStatsMetric(item: ITelemetryItem): boolean { + var n = item.name; + return n === MET_SUCCESS || n === MET_DROPPED || n === MET_RETRY; + } + + function _incSuccess(items: ITelemetryItem[]) { + for (var i = 0; i < items.length; i++) { + if (!_isSdkStatsMetric(items[i])) { + var t = _getTelType(items[i]); + _successCounts[t] = (_successCounts[t] || 0) + 1; + } + } + _ensureTimer(); + } + + function _incDropped(items: ITelemetryItem[], code: string) { + var bucket: { [telType: string]: number }; + if (objHasOwn(_droppedCounts, code)) { + bucket = _droppedCounts[code]; + } else { + bucket = objCreate(null); + _droppedCounts[code] = bucket; + } + for (var i = 0; i < items.length; i++) { + if (!_isSdkStatsMetric(items[i])) { + var t = _getTelType(items[i]); + bucket[t] = (bucket[t] || 0) + 1; + } + } + _ensureTimer(); + } + + function _incRetry(items: ITelemetryItem[], code: string) { + var bucket: { [telType: string]: number }; + if (objHasOwn(_retryCounts, code)) { + bucket = _retryCounts[code]; + } else { + bucket = objCreate(null); + _retryCounts[code] = bucket; + } + for (var i = 0; i < items.length; i++) { + if (!_isSdkStatsMetric(items[i])) { + var t = _getTelType(items[i]); + bucket[t] = (bucket[t] || 0) + 1; + } + } + _ensureTimer(); + } + + function _createMetric(name: string, value: number, props: { [key: string]: any }): ITelemetryItem { + // Merge standard dimensions + props[P_LANG] = cfg.lang; + props[P_VER] = cfg.ver; + props[P_COMPUTE] = "unknown"; // Browser SDK cannot reliably detect compute type + + return { + name: name, + baseType: "MetricData", + baseData: { + name: name, + average: value, + sampleCount: 1, + properties: props + } + } as ITelemetryItem; + } + + function _mapDropCode(reason: number, sendType?: number): string { + // Maps eEventsDiscardedReason to spec drop.code values + // 1 = NonRetryableStatus → actual HTTP status code when available + if (reason === 1 && sendType) { + return "" + sendType; + } + return DROP_CLIENT_EXCEPTION; + } + + function _flush() { + if (_timer) { + _timer.cancel(); + _timer = null; + } + + var telType: string; + var code: string; + var cnt: number; + var bucket: { [telType: string]: number }; + + // Flush success counts + for (telType in _successCounts) { + if (objHasOwn(_successCounts, telType)) { + cnt = _successCounts[telType]; + if (cnt > 0) { + var successProps: { [key: string]: any } = {}; + successProps[P_TEL_TYPE] = telType; + cfg.trk(_createMetric(MET_SUCCESS, cnt, successProps)); + } + } + } + + // Flush dropped counts + for (code in _droppedCounts) { + if (objHasOwn(_droppedCounts, code)) { + bucket = _droppedCounts[code]; + for (telType in bucket) { + if (objHasOwn(bucket, telType)) { + cnt = bucket[telType]; + if (cnt > 0) { + var dropProps: { [key: string]: any } = {}; + dropProps[P_TEL_TYPE] = telType; + dropProps[P_DROP_CODE] = code; + cfg.trk(_createMetric(MET_DROPPED, cnt, dropProps)); + } + } + } + } + } + + // Flush retry counts + for (code in _retryCounts) { + if (objHasOwn(_retryCounts, code)) { + bucket = _retryCounts[code]; + for (telType in bucket) { + if (objHasOwn(bucket, telType)) { + cnt = bucket[telType]; + if (cnt > 0) { + var retryProps: { [key: string]: any } = {}; + retryProps[P_TEL_TYPE] = telType; + retryProps[P_RETRY_CODE] = code; + cfg.trk(_createMetric(MET_RETRY, cnt, retryProps)); + } + } + } + } + } + + // Reset accumulators + _successCounts = objCreate(null); + _droppedCounts = objCreate(null); + _retryCounts = objCreate(null); + } + + return { + eventsSent: _incSuccess, + eventsDiscarded: function (events: ITelemetryItem[], reason: number, sendType?: number) { + var code = _mapDropCode(reason, sendType); + _incDropped(events, code); + }, + eventsRetry: function (events: ITelemetryItem[], statusCode: number) { + var code = "" + statusCode; // numeric status code as string per spec + _incRetry(events, code); + }, + flush: _flush, + unload: function () { + // Flush remaining counts before unload + _flush(); + // Flush the channel so the metrics just enqueued actually get sent + cfg.fnFlush && cfg.fnFlush(); + if (_timer) { + _timer.cancel(); + _timer = null; + } + } + }; +} diff --git a/shared/AppInsightsCore/src/index.ts b/shared/AppInsightsCore/src/index.ts index cb5abe5d9..22669987d 100644 --- a/shared/AppInsightsCore/src/index.ts +++ b/shared/AppInsightsCore/src/index.ts @@ -39,6 +39,7 @@ export { parseResponse } from "./core/ResponseHelpers"; export { IXDomainRequest, IBackendResponse } from "./interfaces/ai/IXDomainRequest"; export { _ISenderOnComplete, _ISendPostMgrConfig, _ITimeoutOverrideWrapper, _IInternalXhrOverride } from "./interfaces/ai/ISenderPostManager"; export { SenderPostManager } from "./core/SenderPostManager"; +export { createSdkStatsNotifCbk, ISdkStatsConfig, ISdkStatsNotifCbk } from "./core/SdkStatsNotificationCbk"; //export { IStatsBeat, IStatsBeatConfig, IStatsBeatKeyMap as IStatsBeatEndpoints, IStatsBeatState} from "./interfaces/ai/IStatsBeat"; //export { IStatsEventData } from "./interfaces/ai/IStatsEventData"; //export { IStatsMgr, IStatsMgrConfig } from "./interfaces/ai/IStatsMgr"; diff --git a/shared/AppInsightsCore/src/interfaces/ai/INotificationListener.ts b/shared/AppInsightsCore/src/interfaces/ai/INotificationListener.ts index ef78b4b36..d14428e81 100644 --- a/shared/AppInsightsCore/src/interfaces/ai/INotificationListener.ts +++ b/shared/AppInsightsCore/src/interfaces/ai/INotificationListener.ts @@ -50,6 +50,14 @@ export interface INotificationListener { */ unload?(isAsync?: boolean): void | IPromise; + /** + * [Optional] A function called when events are being retried. + * @param events - The array of events that are being retried. + * @param statusCode - The HTTP status code that triggered the retry. + * @since 3.3.6 + */ + eventsRetry?(events: ITelemetryItem[], statusCode: number): void; + /** * [Optional] A function called when the offline events have been stored to the persistent storage * @param events - items that are stored in the persistent storage diff --git a/shared/AppInsightsCore/src/interfaces/ai/INotificationManager.ts b/shared/AppInsightsCore/src/interfaces/ai/INotificationManager.ts index 4457d643a..7a1d67af9 100644 --- a/shared/AppInsightsCore/src/interfaces/ai/INotificationManager.ts +++ b/shared/AppInsightsCore/src/interfaces/ai/INotificationManager.ts @@ -63,6 +63,14 @@ export interface INotificationManager { */ unload?(isAsync?: boolean): void | IPromise; + /** + * Notification for events being retried. + * @param events - The array of events that are being retried. + * @param statusCode - The HTTP status code that triggered the retry. + * @since 3.3.6 + */ + eventsRetry?(events: ITelemetryItem[], statusCode: number): void; + /** * [Optional] A function called when the offline events have been stored to the persistent storage * @param events - items that are stored in the persistent storage