Skip to content

Enable Customer SDK Stats#2707

Open
JacksonWeber wants to merge 16 commits intomicrosoft:mainfrom
JacksonWeber:jacksonweber/enable-customer-sdk-stats
Open

Enable Customer SDK Stats#2707
JacksonWeber wants to merge 16 commits intomicrosoft:mainfrom
JacksonWeber:jacksonweber/enable-customer-sdk-stats

Conversation

@JacksonWeber
Copy link

This pull request introduces SDK statistics tracking and enhances event notification capabilities in the Application Insights SDK. The main focus is on enabling periodic SDK stats reporting, improving notification for telemetry event lifecycle (including retries), and supporting these features with new interfaces and configuration options.

SDK Statistics Tracking:

  • Added support for SDK statistics reporting, including new configuration options (SDK_STATS, SDK_STATS_VERSION, SDK_STATS_FLUSH_INTERVAL) and default enablement in the config. The stats listener is registered during initialization and properly cleaned up on unload. (AISKU/src/AISku.ts [1] [2] [3] [4] [5] [6]
  • Introduced ISdkStatsNotifCbk and related imports to facilitate SDK stats notification callbacks. (AISKU/src/AISku.ts AISKU/src/AISku.tsL14-R21)

Telemetry Event Notification Enhancements:

  • Extended the notification system to include a new eventsRetry event, allowing listeners to be notified when telemetry items are retried, along with the relevant HTTP status code. (shared/AppInsightsCore/src/constants/InternalConstants.ts [1] shared/AppInsightsCore/src/core/NotificationManager.ts [2] [3] [4]
  • Updated the Sender plugin to trigger notification callbacks for events sent, discarded, and retried, including logic to extract minimal telemetry items for notification purposes. (channels/applicationinsights-channel-js/src/Sender.ts [1] [2] [3] [4] [5]

Internal Data Structure Updates:

  • Added a bT (baseType) property to IInternalStorageItem for mapping telemetry types in SDK stats and notification extraction. (channels/applicationinsights-channel-js/src/Interfaces.ts [1] channels/applicationinsights-channel-js/src/Sender.ts [2]

These changes collectively improve observability, reliability, and extensibility of the telemetry pipeline, especially for SDK usage metrics and event retry handling.

@JacksonWeber JacksonWeber marked this pull request as ready for review February 24, 2026 18:07
@JacksonWeber JacksonWeber requested a review from a team as a code owner February 24, 2026 18:07
Copilot AI review requested due to automatic review settings February 24, 2026 18:07
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request introduces comprehensive SDK statistics tracking and enhances the notification system to support telemetry retry events. The main objective is to enable periodic reporting of SDK usage metrics (success, dropped, and retry counts) and provide visibility into the complete telemetry lifecycle including retry scenarios.

Changes:

  • Adds eventsRetry notification callback to INotificationManager and INotificationListener interfaces for tracking telemetry retry events with HTTP status codes
  • Implements SdkStatsNotificationCbk - a notification listener that accumulates telemetry counts by type and periodically reports them as metrics
  • Integrates SDK stats listener into AISku with automatic initialization (enabled by default) and proper cleanup during unload
  • Updates Sender to trigger notifications for sent, discarded, and retried events, storing baseType in IInternalStorageItem for telemetry type mapping
  • Adds comprehensive unit tests for the SDK stats notification callback covering all major scenarios

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
shared/AppInsightsCore/src/interfaces/ai/INotificationManager.ts Adds eventsRetry method signature to notification manager interface
shared/AppInsightsCore/src/interfaces/ai/INotificationListener.ts Adds optional eventsRetry callback to listener interface
shared/AppInsightsCore/src/index.ts Exports new SDK stats notification callback factory and interfaces
shared/AppInsightsCore/src/core/SdkStatsNotificationCbk.ts New file implementing the SDK stats accumulator and periodic metric flushing logic
shared/AppInsightsCore/src/core/NotificationManager.ts Implements eventsRetry notification dispatch to all registered listeners
shared/AppInsightsCore/src/constants/InternalConstants.ts Adds STR_EVENTS_RETRY constant for the new notification type
shared/AppInsightsCore/Tests/Unit/src/aiunittests.ts Registers new SDK stats notification callback test suite
shared/AppInsightsCore/Tests/Unit/src/ai/SdkStatsNotificationCbk.Tests.ts New comprehensive test suite covering all SDK stats functionality
channels/applicationinsights-channel-js/src/Sender.ts Adds notification triggers for sent/discarded/retry events and extracts telemetry items for notifications
channels/applicationinsights-channel-js/src/Interfaces.ts Adds bT (baseType) property to IInternalStorageItem for telemetry type mapping
AISKU/src/AISku.ts Integrates SDK stats listener with initialization, configuration, and unload handling

@JacksonWeber JacksonWeber changed the title Jacksonweber/enable customer sdk stats Enable Customer SDK Stats Feb 24, 2026
@JacksonWeber JacksonWeber force-pushed the jacksonweber/enable-customer-sdk-stats branch from 9ed6655 to 7eaafe0 Compare February 25, 2026 20:21
@JacksonWeber JacksonWeber changed the base branch from beta to main February 25, 2026 20:22
@JacksonWeber JacksonWeber requested a review from Copilot February 25, 2026 20:23
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Comment on lines +1421 to +1427
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);
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.
* 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.
_core.initialize(_config, [ _sender, properties, dependencies, _analyticsPlugin, _cfgSyncPlugin], logger, notificationManager);

// Register SDK Stats notification listener (on by default)
if (isFeatureEnabled(SDK_STATS, _config, true)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This initialization should be inside the onConfigChange below so that if/when the configuration is changed (by the config feature fetch) this can be enabled / disabled on the fly without needing to re-initialize the SDK (a new page load)

Copy link
Author

Choose a reason for hiding this comment

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

Moved the initialization.

// Notify listeners of successful send
let mgr = _getNotifyMgr();
if (mgr) {
let items = _extractTelemetryItems(payload);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Follow up PR -- lets try and minimize the need to call this, it might be a large PR as it will most likely require changing the whole serialization process (like we do in 1ds-post-js) where we convert to a string as late as possible so at this level we are still playing with the objects -- there is a gotya around the persistence to the session storage / array storage today for this though.

/**
* The track function to call when flushing metrics. Typically core.track().
*/
trk: (item: ITelemetryItem) => void;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think you actually need this as it you pass the IAppInsightsCore instance that has the track function that is shared across both AI and 1DS

Copy link
Author

Choose a reason for hiding this comment

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

Changed ISdkStatsConfig.trk: (item: ITelemetryItem) => void to ISdkStatsConfig.core: IAppInsightsCore. All internal calls now use cfg.core.track(item) instead of cfg.trk(item). Updated AISku.ts initialization to pass core: _core instead of the trk wrapper function.

},
lang: "JavaScript",
ver: SDK_STATS_VERSION,
int: SDK_STATS_FLUSH_INTERVAL
Copy link
Collaborator

Choose a reason for hiding this comment

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

As one of the items is the interval which should be dynamic, I think we should really be passing the common sdkStats config (from the shared Config) or pass down the full config (IConfiguration) to this function and if we want to cache it internally in a local variable (still reading the PR) then we would need to use the onCfgChange() helper to listen to changes.

Copy link
Author

Choose a reason for hiding this comment

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

The interval is updated to be passed through the config and used in the createSdkStatsNotifCbk. Since the SDK stats listener is now created/destroyed inside onConfigChange, the interval should be updated by destroying and recreating the listener when config changes.

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;
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be the common dynamic config so we can change via the CDN callback

Copy link
Author

Choose a reason for hiding this comment

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

Addressed as part of the above change to make interval dynamic.

}
}
}
_ensureTimer();
Copy link
Collaborator

Choose a reason for hiding this comment

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

unlikely, but minor. Only call if a new value was set

Copy link
Author

Choose a reason for hiding this comment

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

Added a changed flag in _incSuccess to ensure this isn't changed if a new value isn't set.

if (!_safeKey(code)) {
return;
}
var bucket: { [telType: string]: number };
Copy link
Collaborator

Choose a reason for hiding this comment

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

this could be simplified slightly and avoid the call to objHasOwn

let bucket = _droppedCount[code];
if (!bucket) {
    bucket = _droppedCount[code] = objCreate(null);
}

Copy link
Author

Choose a reason for hiding this comment

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

Replaced the objHasOwn + if/else pattern with the simplified pattern in the new _incBucketed helper function

return;
}
var bucket: { [telType: string]: number };
if (objHasOwn(_retryCounts, code)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

same as above

Copy link
Author

Choose a reason for hiding this comment

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

Addressed in above comment.

_ensureTimer();
}

function _incRetry(items: ITelemetryItem[], code: string) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

minification optimization, this function and _incDropped look mostly the same, could put the bulk in a common function and then pass the differences as parameters.

Copy link
Author

Choose a reason for hiding this comment

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

Merged _incDropped and _incRetry into a single _incBucketed(counters, items, code) helper function.

function _createMetric(name: string, value: number, props: { [key: string]: any }): ITelemetryItem {
// Merge standard dimensions
props[P_LANG] = cfg.lang;
props[P_VER] = cfg.ver;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minification optimization: As these are only actually used once, it's better to just use the names inline instead of creating constants.

The build system "counts" all objects that are used more than the number of bytes saved by using local variables and will automatically translate (using the DynamicConstants) as part of the build, reusing the same constant across the entire bundle / package.

Copy link
Author

Choose a reason for hiding this comment

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

Removed the P_LANG, P_VER, P_COMPUTE, P_TEL_TYPE, P_DROP_CODE, and P_RETRY_CODE constants. Replaced all usages with inline string literals ("language", "version", "computeType", "telemetry_type", "drop.code", "retry.code")


return {
name: name,
baseType: "MetricData",
Copy link
Collaborator

Choose a reason for hiding this comment

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

There is now an exported constant MetricDataType for the "MetricData" so could use that instead (Total bundle minification stuff)

Copy link
Author

Choose a reason for hiding this comment

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

Imported MetricDataType from "../telemetry/ai/DataTypes" and replaced the inline "MetricData" string in _createMetric with the MetricDataType constant.

}

// Flush dropped counts
for (code in _droppedCounts) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks like these could be collapsed as well into a single helper function

Copy link
Author

Choose a reason for hiding this comment

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

Created a unified helper to handle this, _flushBucketed().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants