Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
af220a3
Enable SDK Stats and route to the SDK Stats ingestion endpoint
JacksonWeber Jun 17, 2026
689e26d
Make SDK Stats destination configurable via dynamic/CDN config
JacksonWeber Jun 17, 2026
82dc7e7
Optimize repeated property access in SDK Stats endpoint detection
JacksonWeber Jun 17, 2026
dee400d
Re-enable SDK Stats unit tests with updated terminology
JacksonWeber Jun 17, 2026
bb2515c
Merge remote-tracking branch 'upstream/main' into jacksonweber/enable…
JacksonWeber Jun 18, 2026
a56505d
test: address SDK Stats unit test review feedback
JacksonWeber Jun 18, 2026
c9fb76b
test: read SDK Stats config from top-level config.stats
JacksonWeber Jun 18, 2026
adce17f
test: fix SDK Stats timer ticks, none/null opt-in expectations, core …
JacksonWeber Jun 18, 2026
515494d
test: wire stats manager to core in channel init test
JacksonWeber Jun 18, 2026
bf53f68
test: bump AISKU size budget for SDK Stats enablement
JacksonWeber Jun 18, 2026
3fff536
Merge remote-tracking branch 'upstream/main' into jacksonweber/enable…
JacksonWeber Jun 26, 2026
5fd01d9
test: bump core and AISKU bundle-size budgets for SDK Stats
JacksonWeber Jun 26, 2026
04410c8
fix: seed SDK Stats defaults into the global config for dynamic confi…
JacksonWeber Jun 29, 2026
5f9c780
test: bump core and AISKU deflate budgets for SDK Stats dynamic-confi…
JacksonWeber Jun 29, 2026
ff5f5fa
refactor(stats): remove createSdkStatsMgrConfig, read SDK Stats confi…
JacksonWeber Jul 8, 2026
68b9226
Merge remote-tracking branch 'upstream/main' into jacksonweber/enable…
JacksonWeber Jul 17, 2026
1a3fbb0
Read SDK Stats config from cfg/v1.json and gate on the enabled flag
JacksonWeber Jul 18, 2026
2a5a430
Rename internal StatsBeat to Internal SDK Stats
JacksonWeber Jul 18, 2026
ad93383
Use cfg/v1.json as sole authority for SDK Stats enabled state and des…
JacksonWeber Jul 20, 2026
f45b3b3
Refactor SDK Stats config handling to use local vars instead of store…
JacksonWeber Jul 20, 2026
d40a558
Remove IConfigDefaults cast by pinning onConfigChange to IConfiguration
JacksonWeber Jul 20, 2026
7ba04a4
Remove leftover commented-out _sdk/IInternalSdkConfiguration scaffolding
JacksonWeber Jul 21, 2026
9bd13ac
Remove unused StatsType/eStatsType from SDK Stats
JacksonWeber Jul 21, 2026
ed3ca0e
Reduce Internal SDK Stats minified size (-377 bytes raw, -127 bytes g…
JacksonWeber Jul 21, 2026
1adb831
Unload core in InternalSdkStats test cleanup to prevent hook leaks
JacksonWeber Jul 21, 2026
44e6681
Fix CodeQL prototype-pollution alert in SDK stats counters
JacksonWeber Jul 21, 2026
dbb7b7b
Address Copilot review feedback on SDK Stats
JacksonWeber Aug 4, 2026
769c93a
Source SDK Stats config url from dynamic config instead of hardcoding
JacksonWeber Aug 4, 2026
ae02d60
Persist SDK Stats across page loads
JacksonWeber Aug 5, 2026
c054410
Reduce SDK Stats EU region literals
JacksonWeber Aug 6, 2026
aa1c074
Cache SDK Stats endpoint resolution
JacksonWeber Aug 6, 2026
d7e823c
Report snippet version in SDK Stats version
JacksonWeber Aug 6, 2026
0792aba
Isolate SDK Stats from customer pipeline
JacksonWeber Aug 6, 2026
9ebe2b7
test: update core bundle size budget
JacksonWeber Aug 7, 2026
27e737b
test: provide config on SDK Stats core mock
JacksonWeber Aug 7, 2026
cfafa53
test: update AISKU bundle size budget
JacksonWeber Aug 7, 2026
a2315b2
Add dynamic SDK Stats sampling
JacksonWeber Aug 27, 2026
634a52c
test: update core raw bundle size budget
JacksonWeber Sep 1, 2026
106c3b4
test: avoid advancing shared SDK stats clock
JacksonWeber Sep 1, 2026
5324caa
feat: default SDK stats sampling to 100 percent
JacksonWeber Sep 1, 2026
c0f279e
fix: guard SDK stats persistence serialization
JacksonWeber Sep 2, 2026
4fd4a1a
docs: correct SDK stats API introduction versions
JacksonWeber Sep 2, 2026
7845282
test: use SDK stats test endpoint
JacksonWeber Sep 2, 2026
271a233
test: use SDK stats test host in core tests
JacksonWeber Sep 2, 2026
66e2bcb
feat: add dynamic feature throttling
JacksonWeber Sep 2, 2026
58984f5
fix: persist SDK stats feature throttling
JacksonWeber Sep 2, 2026
627cb4b
fix: preserve SDK version placeholder
JacksonWeber Sep 3, 2026
c2da799
fix: preserve numeric throttle config keys
JacksonWeber Sep 3, 2026
c26c05f
test: allow core bundle size variance
JacksonWeber Sep 3, 2026
f3f477b
perf: reduce SDK Stats bundle size
JacksonWeber Sep 4, 2026
ebe361d
test: allow AISKU bundle size variance
JacksonWeber Sep 4, 2026
a479cec
Merge main into SDK Stats branch and resolve version conflict
JacksonWeber Sep 9, 2026
f522598
Fix SDK Stats send limit for three-month window
JacksonWeber Sep 24, 2026
43c0599
Test AISKU SDK Stats dynamic configuration initialization
JacksonWeber Sep 24, 2026
6ca7fce
Use SDK Stats test cleanup and async polling helpers
JacksonWeber Sep 24, 2026
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
1 change: 0 additions & 1 deletion .aiAutoMinify.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"eLoggingSeverity",
"_eInternalMessageId",
"SendRequestReason",
"eStatsType",
"TelemetryUnloadReason",
"TelemetryUpdateReason",
"eTraceHeadersMode",
Expand Down
2 changes: 1 addition & 1 deletion AISKU/Tests/Manual/testVersionConflict.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
// onInit: null, // Once the application insights instance has loaded and initialized this callback function will be called with 1 argument -- the sdk instance (DO NOT ADD anything to the sdk.queue -- As they won't get called)
sri: true, // Custom optional value to specify whether fetching the snippet from integrity file and do integrity check
cfg: { // Application Insights Configuration
disableStatsBeat: false,
disableInternalSdkStats: false,
connectionString: "YOUR_INSTRUMENTATION_KEY",
}
});
Expand Down
8 changes: 4 additions & 4 deletions AISKU/Tests/Unit/src/AISKUSize.Tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly:
}

export class AISKUSizeCheck extends AITestClass {
private readonly MAX_RAW_SIZE = 177;
private readonly MAX_BUNDLE_SIZE = 177;
private readonly MAX_RAW_DEFLATE_SIZE = 71;
private readonly MAX_BUNDLE_DEFLATE_SIZE = 71;
private readonly MAX_RAW_SIZE = 181;
private readonly MAX_BUNDLE_SIZE = 183;
private readonly MAX_RAW_DEFLATE_SIZE = 74;
private readonly MAX_BUNDLE_DEFLATE_SIZE = 74;
private readonly rawFilePath = "../dist/es5/applicationinsights-web.min.js";
// Automatically updated by version scripts
private readonly currentVer = "3.4.4";
Expand Down
163 changes: 162 additions & 1 deletion AISKU/Tests/Unit/src/SdkStatsFeature.tests.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ApplicationInsights, IConfig, IConfiguration } from '../../../src/applicationinsights-web';
import { AITestClass, Assert } from '@microsoft/ai-test-framework';
import { FeatureOptInMode, ISdkStatsNotifCbk, onConfigChange } from '@microsoft/applicationinsights-core-js';
import {
_eInternalMessageId, FeatureOptInMode, ISdkStatsNotifCbk, onConfigChange, STATS_SDK_FEATURE
} from '@microsoft/applicationinsights-core-js';
import { AppInsightsSku } from '../../../src/AISku';
import { ICfgSyncMode } from '@microsoft/applicationinsights-cfgsync-js';

Expand Down Expand Up @@ -37,9 +39,12 @@ export class SdkStatsFeatureTests extends AITestClass {
public registerTests() {
this._testSdkStatsEnabledByDefault();
this._testSdkStatsDisabledViaFeatureOptIn();
this._testCustomerSdkStatsIgnoresInternalThrottle();
this._testSdkStatsDynamicEnableDisable();
this._testSdkStatsConfigDefaults();
this._testSdkStatsDynamicConfigChanges();
this._testInternalSdkStatsDynamicConfigInitialization();
this._testSnippetSdkVersion();
}

private _createAi(configOverrides?: Partial<IConfiguration & IConfig>): AppInsightsSku {
Expand Down Expand Up @@ -132,6 +137,34 @@ export class SdkStatsFeatureTests extends AITestClass {
});
}

private _testCustomerSdkStatsIgnoresInternalThrottle() {
this.testCase({
name: "SdkStatsFeature: dedicated SDK Stats throttle does not disable customer SDK Stats",
useFakeTimers: true,
test: () => {
let ai = this._createAi({
throttleMgrCfg: {
[STATS_SDK_FEATURE]: {
disabled: false,
limit: {
samplingRate: 0
}
}
}
});
this.clock.tick(1);

Assert.ok(this._findSdkStatsListener(ai),
"Customer SDK Stats should remain enabled when dedicated SDK Stats are throttled");
Assert.ok(ai["core"].getSdkStats({
cKey: TestInstrumentationKey,
endpoint: "https://example.endpoint.com",
sdkVer: "1.0.0"
}), "The throttle should apply to SDK Stats operations without disabling the manager");
}
});
}

private _testSdkStatsDynamicEnableDisable() {
this.testCase({
name: "SdkStatsFeature: disabling SdkStats feature dynamically removes the listener",
Expand Down Expand Up @@ -220,6 +253,11 @@ export class SdkStatsFeatureTests extends AITestClass {
let config = ai.config;
Assert.ok(config.sdkStats, "sdkStats config should exist after initialization");
Assert.equal(900000, config.sdkStats!.int, "int should default to 900000 (15 minutes)");
Assert.equal(100, config.throttleMgrCfg![STATS_SDK_FEATURE].limit!.samplingRate,
"Dedicated SDK Stats throttle should use the legacy default sampling rate");
Assert.equal(92 * 24 * 60 * 60 * 1000 / config.sdkStats!.int,
config.throttleMgrCfg![STATS_SDK_FEATURE].limit!.maxSendNumber,
"Dedicated SDK Stats throttle should cover 15-minute intervals across the longest three-month window");
}
});

Expand All @@ -230,13 +268,27 @@ export class SdkStatsFeatureTests extends AITestClass {
let ai = this._createAi({
sdkStats: {
int: 60000
},
throttleMgrCfg: {
[STATS_SDK_FEATURE]: {
limit: {
maxSendNumber: 48
}
}
}
});
this.clock.tick(1);

let config = ai.config;
Assert.ok(config.sdkStats, "sdkStats config should exist");
Assert.equal(60000, config.sdkStats!.int, "User-provided int should be preserved");
Assert.equal(48, config.throttleMgrCfg![STATS_SDK_FEATURE].limit!.maxSendNumber,
"User-provided maxSendNumber should be preserved");

config.throttleMgrCfg![STATS_SDK_FEATURE].limit!.maxSendNumber = 192;
this.clock.tick(1);
Assert.equal(192, config.throttleMgrCfg![STATS_SDK_FEATURE].limit!.maxSendNumber,
"maxSendNumber should support runtime updates");
}
});
}
Expand Down Expand Up @@ -303,4 +355,113 @@ export class SdkStatsFeatureTests extends AITestClass {
}
});
}

private _testInternalSdkStatsDynamicConfigInitialization() {
this.testCase({
name: "SdkStatsFeature: internal SDK Stats initializes when dynamic configuration arrives after loadAppInsights",
useFakeTimers: true,
useFakeServer: true,
test: () => {
const cfgUrl = "https://tst-data.stats.monitor.azure.com/cfg/v1.json";
const statsHost = "tst-data.stats.monitor.azure.com";
const statsIKey = "000e0000-e000-0000-a000-000000000000";
const state = {
cKey: TestInstrumentationKey,
endpoint: "https://dynamic-config.example.com/v2/track",
sdkVer: "1.0.0"
};
const storageKey = state.cKey + ":" + state.endpoint;
sessionStorage.removeItem(storageKey);
this.onDone(() => sessionStorage.removeItem(storageKey));
this.hookSendBeacon(() => {});
this.hookFetch((resolve) => resolve(new Response("{}", { status: 200 })));
let xhrSendSpy = this.sandbox.spy(XMLHttpRequest.prototype, "send");

let ai = this._createAi();
this.clock.tick(1);

Assert.ok(ai.config.stats, "loadAppInsights should seed the dynamic stats configuration");
Assert.equal(undefined, ai.config.stats.cfgUrl, "The SDK Stats config URL should not be supplied initially");
Assert.equal(undefined, ai.config.stats.iKey, "The SDK Stats instrumentation key should not be supplied initially");
let stats = ai.core.getSdkStats(state);
Assert.ok(stats && stats.enabled, "loadAppInsights should initialize the internal SDK Stats manager");
stats.countException(state.endpoint, "NetworkError");
this.clock.tick(1000);
Assert.equal(0, this.activeXhrRequests.length, "SDK Stats should not send before configuration arrives");

let fetchedUrls: string[] = [];
ai.updateCfg({
stats: {
shrtInt: 1,
cfgUrl: cfgUrl,
iKey: statsIKey,
overrideCfgFn: (url, oncomplete) => {
fetchedUrls.push(url);
oncomplete({ enabled: true, url: statsHost });
}
},
throttleMgrCfg: {
[_eInternalMessageId.DefaultThrottleMsgKey]: { disabled: false },
[STATS_SDK_FEATURE]: {
limit: { samplingRate: 1000000 },
interval: { dayInterval: 1 }
}
}
});
this.clock.tick(1002);

Assert.deepEqual([cfgUrl], fetchedUrls,
"The existing manager should use the dynamically supplied config fetcher and URL");
Assert.strictEqual(stats, ai.core.getSdkStats(state),
"Configuration should initialize sending without replacing the stats instance");
let requests = this.activeXhrRequests;
Assert.equal(1, requests.length, "The isolated SDK Stats sender should send the buffered counter");
Assert.equal("https://" + statsHost + "/v2/track", requests[0].url, "SDK Stats should use the remote-configured endpoint");
let payload = JSON.parse(xhrSendSpy.firstCall.args[0]);
Assert.equal(statsIKey, payload[0].iKey, "SDK Stats should use the dynamically supplied instrumentation key");
Assert.equal("exception", payload[0].data.baseData.metrics[0].name, "The buffered exception should be reported");
Assert.equal(1, payload[0].data.baseData.metrics[0].value, "The buffered exception count should be preserved");
requests[0].respond(200, {}, "");

ai.config.featureOptIn = {
[STATS_SDK_FEATURE]: { mode: FeatureOptInMode.disable }
};
this.clock.tick(1);
Assert.equal(null, ai.core.getSdkStats(state),
"Disabling the internal feature dynamically should remove its stats instance");
Assert.equal(false, stats.enabled, "The previous stats instance should be stopped");

ai.config.featureOptIn[STATS_SDK_FEATURE].mode = FeatureOptInMode.enable;
this.clock.tick(1);
let reenabledStats = ai.core.getSdkStats(state);
Assert.ok(reenabledStats && reenabledStats.enabled, "Re-enabling the internal feature should initialize stats again");
}
});
}

private _testSnippetSdkVersion() {
this.testCase({
name: "SdkStatsFeature: snippet version is included in the SDK Stats version",
useFakeTimers: true,
test: () => {
let config = {
connectionString: TestConnectionString,
stats: {},
extensionConfig: {
["AppInsightsCfgSyncPlugin"]: {
syncMode: ICfgSyncMode.Receive,
cfgUrl: ""
}
}
} as IConfiguration & IConfig;
let ai = new AppInsightsSku({ config: config, sv: "6", queue: [] } as any);
ai.loadAppInsights();
this._ai = ai;
this.clock.tick(1);

Assert.equal("6", ai.config.stats.snp, "SDK Stats config should contain snippet version 6");
}
});
}

}
56 changes: 45 additions & 11 deletions AISKU/src/AISku.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import {
IMetricTelemetry, INotificationManager, IOTelApi, IOTelSpanOptions, IPageViewPerformanceTelemetry, IPageViewTelemetry, IPlugin,
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
IThrottleMgrConfig, ITraceApi, ITraceProvider, ITraceTelemetry, IUnloadHook, OTelTimeInput, PropertiesPluginIdentifier,
STATS_SDK_FEATURE, ThrottleMgr, UnloadHandler, WatcherFunction, _eInternalMessageId, _throwInternal, addPageHideEventListener,
addPageUnloadEventListener, cfgDfMerge, cfgDfValidate, createDynamicConfig, createOTelApi, createProcessTelemetryContext,
createSdkStatsNotifCbk, createStatsMgr, 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 @@ -84,6 +84,14 @@ const default_throttle_config = {
interval: cfgDfMerge<IThrottleInterval>(default_interval)
} as IThrottleMgrConfig;

const sdk_stats_throttle_config = {
disabled: false,
limit: cfgDfMerge<IThrottleLimit>({
samplingRate: 100,
maxSendNumber: 8832 // Longest three-month window: 92 days * 24 hours * 4 reports per hour
})
} as IThrottleMgrConfig;

// We need to include all properties that we only reference that we want to be dynamically updatable here
// So they are converted even when not specified in the passed configuration
const defaultConfigValues: IConfigDefaults<IConfiguration & IConfig> = {
Expand All @@ -102,12 +110,13 @@ const defaultConfigValues: IConfigDefaults<IConfiguration & IConfig> = {
sdkStats: cfgDfMerge({
int: 900000
}),
throttleMgrCfg: cfgDfMerge<{[key:number]: IThrottleMgrConfig}>(
throttleMgrCfg: cfgDfMerge<{[key: string]: IThrottleMgrConfig}>(
{
[_eInternalMessageId.DefaultThrottleMsgKey]:cfgDfMerge<IThrottleMgrConfig>(default_throttle_config),
[_eInternalMessageId.InstrumentationKeyDeprecation]:cfgDfMerge<IThrottleMgrConfig>(default_throttle_config),
[_eInternalMessageId.SdkLdrUpdate]:cfgDfMerge<IThrottleMgrConfig>(default_throttle_config),
[_eInternalMessageId.CdnDeprecation]:cfgDfMerge<IThrottleMgrConfig>(default_throttle_config)
[_eInternalMessageId.CdnDeprecation]:cfgDfMerge<IThrottleMgrConfig>(default_throttle_config),
[STATS_SDK_FEATURE]: cfgDfMerge<IThrottleMgrConfig>(sdk_stats_throttle_config)
}
),
extensionConfig: cfgDfMerge<{[key: string]: any}>({
Expand Down Expand Up @@ -398,21 +407,46 @@ export class AppInsightsSku implements IApplicationInsights<IConfiguration & ICo
// initialize core
_core.initialize(_config, [ _sender, properties, dependencies, _analyticsPlugin, _cfgSyncPlugin], logger, notificationManager);

if (!_throttleMgr){
_throttleMgr = new ThrottleMgr(_core);
}

// Enable SDK Stats collection. The manager reads its configuration directly from the
// single global config (config.stats) and gates itself behind the SDK Stats feature
// flag, routing the resulting events to the distro-owned SDK Stats ingestion endpoint.
// The config url and iKey are supplied by config.stats, until both are present no
// SDK Stats are sent. Opt-out via featureOptIn "sdkStats".
let statsMgr = createStatsMgr();
_core.setStatsMgr(statsMgr);
_core.addUnloadHook(statsMgr.init<IConfiguration & IConfig>(_core, (statsConfig) => {
try {
let statsCore = new AppInsightsCore();
(statsConfig as IConfiguration & IConfig).maxBatchInterval = 1;
statsCore.initialize(statsConfig as IConfiguration & IConfig, [new Sender()]);
return statsCore;
} catch (e) {
_throwInternal(_core.logger, eLoggingSeverity.WARNING,
_eInternalMessageId.InternalSdkStatsManagerException, "Failed to create SDK Stats core");
return null;
}
}, STATS_SDK_FEATURE, _throttleMgr.useFeature));

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

objDefine(_self, "context", {
g: () => properties.context
});

if (!_throttleMgr){
_throttleMgr = new ThrottleMgr(_core);
}
let sdkSrc = _findSdkSourceFile();
if (sdkSrc && _self.context) {
_self.context.internal.sdkSrc = sdkSrc;
}
_updateSnippetProperties(_self.snippet);

if (_config.stats) {
_config.stats.snp = _snippetVersion;
}

// Empty queue of all api calls logged prior to sdk download
_self.emptyQueue();
Expand Down
Loading
Loading