From 2e888c5ba81881a391366f28a54183ca9f66cc4e Mon Sep 17 00:00:00 2001 From: siyuniu-ms <123212536+siyuniu-ms@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:53:43 -0700 Subject: [PATCH 01/72] [Main] Increase CDN Deprecation Message Sampling Rate to 40% (#2492) --- tools/config/config.json | 4 ++-- tools/config/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/config/config.json b/tools/config/config.json index 0512a51b5..bbc78e247 100644 --- a/tools/config/config.json +++ b/tools/config/config.json @@ -1,5 +1,5 @@ { - "version": "1.0.4", + "version": "1.0.5", "enabled": true, "featureOptIn": { "iKeyUsage": { @@ -53,7 +53,7 @@ "110": { "disabled": false, "limit": { - "samplingRate": 300000, + "samplingRate": 400000, "maxSendNumber": 1 }, "interval": { diff --git a/tools/config/package.json b/tools/config/package.json index 8af471eb1..3acad6576 100644 --- a/tools/config/package.json +++ b/tools/config/package.json @@ -1,7 +1,7 @@ { "name": "applicationinsights-web-config", "description": "Application Insights JavaScript SDK - Web Config for CfgSync Plugin", - "version": "1.0.4", + "version": "1.0.5", "copyright": "(c) Microsoft and contributors. All rights reserved.", "author": "Microsoft Application Insights Team", "repository": { From 8fe338926a342abcb21c0c000eb5a77fe7fee3fe Mon Sep 17 00:00:00 2001 From: Nev Wylie <54870357+MSNev@users.noreply.github.com> Date: Tue, 25 Mar 2025 17:23:18 -0700 Subject: [PATCH 02/72] [beta] Merge [main] into [beta] and set next release to minor --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 266e69773..a1922a127 100644 --- a/version.json +++ b/version.json @@ -2,7 +2,7 @@ "description": "The release value identifies the base version that will be applied via the tools/release-tools/setVersion.js", "usage": "When creating a new release you should update this value directly or via the eg. 'npm run setVersion -- 3.2.0' or 'npm run setVersion -- -patch' or 'npm run setVersion -- -minor'", "release": "3.3.6", - "next": "patch", + "next": "minor", "pkgs": { "@microsoft/applicationinsights-web": { "package": "package.json", From 8728e90215fde2bc49b229219a3203ead8182759 Mon Sep 17 00:00:00 2001 From: Nev <54870357+MSNev@users.noreply.github.com> Date: Wed, 26 Mar 2025 09:34:43 -0700 Subject: [PATCH 03/72] Update AISKU/Tests/Unit/src/AISKUSize.Tests.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- AISKU/Tests/Unit/src/AISKUSize.Tests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts index a1a231906..2343d0056 100644 --- a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts +++ b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts @@ -85,8 +85,8 @@ export class AISKUSizeCheck extends AITestClass { } public testCleanup() { - utlRemoveSessionStorage(null as any, "AI_sentBuffer", ); - utlRemoveSessionStorage(null as any, "AI_buffer", ); + utlRemoveSessionStorage(null as any, "AI_sentBuffer"); + utlRemoveSessionStorage(null as any, "AI_buffer"); } public registerTests() { From 3097337f5df484870bf332339dad5859e11a030b Mon Sep 17 00:00:00 2001 From: Nev <54870357+MSNev@users.noreply.github.com> Date: Wed, 26 Mar 2025 09:34:52 -0700 Subject: [PATCH 04/72] Update AISKU/Tests/Perf/src/AISKUPerf.Tests.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- AISKU/Tests/Perf/src/AISKUPerf.Tests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AISKU/Tests/Perf/src/AISKUPerf.Tests.ts b/AISKU/Tests/Perf/src/AISKUPerf.Tests.ts index dfa43312e..b3ab253a9 100644 --- a/AISKU/Tests/Perf/src/AISKUPerf.Tests.ts +++ b/AISKU/Tests/Perf/src/AISKUPerf.Tests.ts @@ -152,8 +152,8 @@ export class AISKUPerf extends AITestClass { } public testCleanup() { - utlRemoveSessionStorage(null as any, "AI_sentBuffer", ); - utlRemoveSessionStorage(null as any, "AI_buffer", ); + utlRemoveSessionStorage(null as any, "AI_sentBuffer"); + utlRemoveSessionStorage(null as any, "AI_buffer"); } public registerTests() { From 9946ed43837cdcabe49abf2f7f8040cca8f0331b Mon Sep 17 00:00:00 2001 From: Nev <54870357+MSNev@users.noreply.github.com> Date: Tue, 1 Apr 2025 17:01:57 -0700 Subject: [PATCH 05/72] [main] Add stale bot for assigned issues and prs (#2495) --- .github/workflows/stale-assigned.yml | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/stale-assigned.yml diff --git a/.github/workflows/stale-assigned.yml b/.github/workflows/stale-assigned.yml new file mode 100644 index 000000000..7aceb75ca --- /dev/null +++ b/.github/workflows/stale-assigned.yml @@ -0,0 +1,30 @@ +name: 'Stale issue handler' +on: + workflow_dispatch: + schedule: + - cron: '0 7 * * *' + +permissions: + actions: write + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + id: stale + with: + stale-issue-message: 'This Issue will be closed in 30 days. Please remove the "Stale" label or comment to avoid closure with no action.' + stale-pr-message: 'This PR will be closed in 30 days. Please remove the "Stale" label or comment to avoid closure with no action.' + operations-per-run: 200 + days-before-stale: 360 + days-before-close: 30 + stale-issue-label: 'stale' + stale-pr-label: 'stale' + close-issue-label: 'closed' + close-pr-label: 'closed' + exempt-issue-labels: 'bug,enhancement,documentation,waiting,keep' + exempt-pr-labels: 'waiting,keep' + include-only-assigned: true \ No newline at end of file From ffbd877438608fa6c187259c0cb7d09642829b38 Mon Sep 17 00:00:00 2001 From: siyuniu-ms <123212536+siyuniu-ms@users.noreply.github.com> Date: Wed, 2 Apr 2025 12:31:13 -0700 Subject: [PATCH 06/72] move to beta again (#2496) Co-authored-by: Nev <54870357+MSNev@users.noreply.github.com> --- tools/github-page-script-injection/injectScript.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/github-page-script-injection/injectScript.js b/tools/github-page-script-injection/injectScript.js index 47dbea078..be1a15e4b 100644 --- a/tools/github-page-script-injection/injectScript.js +++ b/tools/github-page-script-injection/injectScript.js @@ -31,8 +31,8 @@ let scriptContent = fs.readFileSync(scriptFilePath, 'utf8'); // Replace the placeholder string with the actual connection string //const connectionString = 'InstrumentationKey=1ae9e7ce-18f1-4e14-8fc0-acbf0ed28895;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/'; let iKeyForCfgSync = "1ae9e7ce-18f1-4e14-8fc0-acbf0ed28895"; -//let cfgUrl = "https://js.monitor.azure.com/beta/ai.config.1.cfg.json"; -let cfgUrl = "https://js.monitor.azure.com/nightly/ai_test.config.1-nightly3.cfg.json"; +let cfgUrl = "https://js.monitor.azure.com/beta/ai.config.1.cfg.json"; +// let cfgUrl = "https://js.monitor.azure.com/nightly/ai_test.config.1-nightly3.cfg.json"; scriptContent = scriptContent.replace(`connectionString: "YOUR_CONNECTION_STRING"`, `instrumentationKey: "${iKeyForCfgSync}",\n \texpCfg: {\n\t\tinclScripts: true,\n\t\t\n\t\tmaxLogs: 100},\n \t\textensionConfig: {\n\t\t\t"AppInsightsCfgSyncPlugin": {\n\t\t\t\tcfgUrl:"${cfgUrl}"\n\t\t\t}\n\t\t}`); //scriptContent = scriptContent.replace('YOUR_CONNECTION_STRING', connectionString); scriptContent = ``; From 9a33ccd01542250f01aed2f992f82a3b9636fdfc Mon Sep 17 00:00:00 2001 From: Nev <54870357+MSNev@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:43:56 -0700 Subject: [PATCH 07/72] [beta] Update publishing scripts for nightly beta releases (#2498) --- AISKU/scripts/publishAzReleaseToCdn.ps1 | 2 +- AISKU/scripts/setAzActiveCdnVersion.ps1 | 2 +- .../scripts/publishAzReleaseToCdn.ps1 | 2 +- .../scripts/setAzActiveCdnVersion.ps1 | 2 +- .../scripts/publishAzReleaseToCdn.ps1 | 2 +- .../scripts/setAzActiveCdnVersion.ps1 | 2 +- .../scripts/publishAzReleaseToCdn.ps1 | 2 +- .../scripts/setAzActiveCdnVersion.ps1 | 2 +- tools/chrome-debug-extension/scripts/publishAzReleaseToCdn.ps1 | 2 +- tools/chrome-debug-extension/scripts/setAzActiveCdnVersion.ps1 | 2 +- tools/config/scripts/publishAzReleaseToCdn.ps1 | 2 +- tools/config/scripts/setAzActiveCdnVersion.ps1 | 2 +- tools/release-tools/setVersion.js | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/AISKU/scripts/publishAzReleaseToCdn.ps1 b/AISKU/scripts/publishAzReleaseToCdn.ps1 index 99aacadc3..4367b754a 100644 --- a/AISKU/scripts/publishAzReleaseToCdn.ps1 +++ b/AISKU/scripts/publishAzReleaseToCdn.ps1 @@ -139,7 +139,7 @@ elseif ($version.type -eq "dev" -or $version.type -eq "beta") { # Publish to release type folder folder PublishFiles $releaseFiles "$($version.type)" $cacheControl1Year $contentType $overwrite } -elseif ($version.type -eq "nightly" -or $version.type -eq "nightly3") { +elseif ($version.type -like "nightly*") { # Publish to release nightly folder folder PublishFiles $releaseFiles "nightly" $cacheControl1Year $contentType $overwrite } diff --git a/AISKU/scripts/setAzActiveCdnVersion.ps1 b/AISKU/scripts/setAzActiveCdnVersion.ps1 index c83e1680e..97472c382 100644 --- a/AISKU/scripts/setAzActiveCdnVersion.ps1 +++ b/AISKU/scripts/setAzActiveCdnVersion.ps1 @@ -87,7 +87,7 @@ Function Validate-Params Write-LogFailure "Container [$container] is not valid for version type [$($version.type)]" } } - elseif ($version.type -eq "nightly" -or $version.type -eq "nightly3") { + elseif ($version.type -like "nightly*") { if ("nightly" -ne $container) { Write-LogFailure "Container [$container] is not valid for version type [$($version.type)]" } diff --git a/extensions/applicationinsights-clickanalytics-js/scripts/publishAzReleaseToCdn.ps1 b/extensions/applicationinsights-clickanalytics-js/scripts/publishAzReleaseToCdn.ps1 index 3b55f7908..df287b003 100644 --- a/extensions/applicationinsights-clickanalytics-js/scripts/publishAzReleaseToCdn.ps1 +++ b/extensions/applicationinsights-clickanalytics-js/scripts/publishAzReleaseToCdn.ps1 @@ -138,7 +138,7 @@ elseif ($version.type -eq "dev" -or $version.type -eq "beta") { # Publish to release type folder folder PublishFiles $releaseFiles "$($version.type)/ext" $cacheControl1Year $contentType $overwrite } -elseif ($version.type -eq "nightly" -or $version.type -eq "nightly3") { +elseif ($version.type -like "nightly*") { # Publish to release nightly folder folder PublishFiles $releaseFiles "nightly/ext" $cacheControl1Year $contentType $overwrite } diff --git a/extensions/applicationinsights-clickanalytics-js/scripts/setAzActiveCdnVersion.ps1 b/extensions/applicationinsights-clickanalytics-js/scripts/setAzActiveCdnVersion.ps1 index 012f0088c..513f093a2 100644 --- a/extensions/applicationinsights-clickanalytics-js/scripts/setAzActiveCdnVersion.ps1 +++ b/extensions/applicationinsights-clickanalytics-js/scripts/setAzActiveCdnVersion.ps1 @@ -87,7 +87,7 @@ Function Validate-Params Write-LogFailure "Container [$container] is not valid for version type [$($version.type)]" } } - elseif ($version.type -eq "nightly" -or $version.type -eq "nightly3") { + elseif ($version.type -like "nightly*") { if ("nightly" -ne $container) { Write-LogFailure "Container [$container] is not valid for version type [$($version.type)]" } diff --git a/extensions/applicationinsights-debugplugin-js/scripts/publishAzReleaseToCdn.ps1 b/extensions/applicationinsights-debugplugin-js/scripts/publishAzReleaseToCdn.ps1 index aacdf5cff..d25ac9548 100644 --- a/extensions/applicationinsights-debugplugin-js/scripts/publishAzReleaseToCdn.ps1 +++ b/extensions/applicationinsights-debugplugin-js/scripts/publishAzReleaseToCdn.ps1 @@ -138,7 +138,7 @@ elseif ($version.type -eq "dev" -or $version.type -eq "beta") { # Publish to release type folder folder PublishFiles $releaseFiles "$($version.type)/ext" $cacheControl1Year $contentType $overwrite } -elseif ($version.type -eq "nightly" -or $version.type -eq "nightly3") { +elseif ($version.type -like "nightly*") { # Publish to release nightly folder folder PublishFiles $releaseFiles "nightly/ext" $cacheControl1Year $contentType $overwrite } diff --git a/extensions/applicationinsights-debugplugin-js/scripts/setAzActiveCdnVersion.ps1 b/extensions/applicationinsights-debugplugin-js/scripts/setAzActiveCdnVersion.ps1 index 856208956..a968001e0 100644 --- a/extensions/applicationinsights-debugplugin-js/scripts/setAzActiveCdnVersion.ps1 +++ b/extensions/applicationinsights-debugplugin-js/scripts/setAzActiveCdnVersion.ps1 @@ -87,7 +87,7 @@ Function Validate-Params Write-LogFailure "Container [$container] is not valid for version type [$($version.type)]" } } - elseif ($version.type -eq "nightly" -or $version.type -eq "nightly3") { + elseif ($version.type -like "nightly*") { if ("nightly" -ne $container) { Write-LogFailure "Container [$container] is not valid for version type [$($version.type)]" } diff --git a/extensions/applicationinsights-perfmarkmeasure-js/scripts/publishAzReleaseToCdn.ps1 b/extensions/applicationinsights-perfmarkmeasure-js/scripts/publishAzReleaseToCdn.ps1 index a9b9b1051..e166a7e43 100644 --- a/extensions/applicationinsights-perfmarkmeasure-js/scripts/publishAzReleaseToCdn.ps1 +++ b/extensions/applicationinsights-perfmarkmeasure-js/scripts/publishAzReleaseToCdn.ps1 @@ -138,7 +138,7 @@ elseif ($version.type -eq "dev" -or $version.type -eq "beta") { # Publish to release type folder folder PublishFiles $releaseFiles "$($version.type)/ext" $cacheControl1Year $contentType $overwrite } -elseif ($version.type -eq "nightly" -or $version.type -eq "nightly3") { +elseif ($version.type -like "nightly*") { # Publish to release nightly folder folder PublishFiles $releaseFiles "nightly/ext" $cacheControl1Year $contentType $overwrite } diff --git a/extensions/applicationinsights-perfmarkmeasure-js/scripts/setAzActiveCdnVersion.ps1 b/extensions/applicationinsights-perfmarkmeasure-js/scripts/setAzActiveCdnVersion.ps1 index bb7e033ec..6a489cc64 100644 --- a/extensions/applicationinsights-perfmarkmeasure-js/scripts/setAzActiveCdnVersion.ps1 +++ b/extensions/applicationinsights-perfmarkmeasure-js/scripts/setAzActiveCdnVersion.ps1 @@ -87,7 +87,7 @@ Function Validate-Params Write-LogFailure "Container [$container] is not valid for version type [$($version.type)]" } } - elseif ($version.type -eq "nightly" -or $version.type -eq "nightly3") { + elseif ($version.type -like "nightly*") { if ("nightly" -ne $container) { Write-LogFailure "Container [$container] is not valid for version type [$($version.type)]" } diff --git a/tools/chrome-debug-extension/scripts/publishAzReleaseToCdn.ps1 b/tools/chrome-debug-extension/scripts/publishAzReleaseToCdn.ps1 index 9b96d2002..0bdc2dd93 100644 --- a/tools/chrome-debug-extension/scripts/publishAzReleaseToCdn.ps1 +++ b/tools/chrome-debug-extension/scripts/publishAzReleaseToCdn.ps1 @@ -127,7 +127,7 @@ elseif ($version.type -eq "dev" -or $version.type -eq "beta") { # Publish to release type folder folder PublishFiles $releaseFiles "$($version.type)/tools" $cacheControl1Year $contentType $overwrite } -elseif ($version.type -eq "nightly" -or $version.type -eq "nightly3") { +elseif ($version.type -like "nightly*") { # Publish to release nightly folder folder PublishFiles $releaseFiles "nightly/tools" $cacheControl1Year $contentType $overwrite } diff --git a/tools/chrome-debug-extension/scripts/setAzActiveCdnVersion.ps1 b/tools/chrome-debug-extension/scripts/setAzActiveCdnVersion.ps1 index f2412bbb4..91cc80821 100644 --- a/tools/chrome-debug-extension/scripts/setAzActiveCdnVersion.ps1 +++ b/tools/chrome-debug-extension/scripts/setAzActiveCdnVersion.ps1 @@ -87,7 +87,7 @@ Function Validate-Params Write-LogFailure "Container [$container] is not valid for version type [$($version.type)]" } } - elseif ($version.type -eq "nightly" -or $version.type -eq "nightly3") { + elseif ($version.type -like "nightly*") { if ("nightly" -ne $container) { Write-LogFailure "Container [$container] is not valid for version type [$($version.type)]" } diff --git a/tools/config/scripts/publishAzReleaseToCdn.ps1 b/tools/config/scripts/publishAzReleaseToCdn.ps1 index 99f0db08c..404dcf5b2 100644 --- a/tools/config/scripts/publishAzReleaseToCdn.ps1 +++ b/tools/config/scripts/publishAzReleaseToCdn.ps1 @@ -153,7 +153,7 @@ elseif ($version.type -eq "dev" -or $version.type -eq "beta") { # PublishFiles $releaseFiles "$($version.type)" $cacheControl1Year $contentType $overwrite PublishFiles $testFiles "$($version.type)" $cacheControl1Year $contentType $overwrite } -elseif ($version.type -eq "nightly" -or $version.type -eq "nightly3") { +elseif ($version.type -like "nightly*") { # Publish to release nightly folder folder # PublishFiles $releaseFiles "nightly" $cacheControl1Year $contentType $overwrite PublishFiles $testFiles "nightly" $cacheControl1Year $contentType $overwrite diff --git a/tools/config/scripts/setAzActiveCdnVersion.ps1 b/tools/config/scripts/setAzActiveCdnVersion.ps1 index 4e5684ddb..afdcdad40 100644 --- a/tools/config/scripts/setAzActiveCdnVersion.ps1 +++ b/tools/config/scripts/setAzActiveCdnVersion.ps1 @@ -87,7 +87,7 @@ Function Validate-Params Write-LogFailure "Container [$container] is not valid for version type [$($version.type)]" } } - elseif ($version.type -eq "nightly" -or $version.type -eq "nightly3") { + elseif ($version.type -like "nightly*") { if ("nightly" -ne $container) { Write-LogFailure "Container [$container] is not valid for version type [$($version.type)]" } diff --git a/tools/release-tools/setVersion.js b/tools/release-tools/setVersion.js index dde02a3f3..c750c1479 100644 --- a/tools/release-tools/setVersion.js +++ b/tools/release-tools/setVersion.js @@ -406,7 +406,7 @@ function updatePublishConfig(package, newVersion) { } // Set the publishing tag - if (details.type === "nightly" || details.type === "dev" || details.type === "beta" || details.type === "alpha") { + if (details.type === "nightly" || details.type === "nightlybeta" || details.type === "dev" || details.type === "beta" || details.type === "alpha") { console.log(` Type - [${details.type}] - ${majorVersion}`); package.publishConfig.tag = details.type + (majorVersion !== "0" ? majorVersion : ""); } else { From 82c7981fbcc6cd7f851f3e6967ac015fa05b47a4 Mon Sep 17 00:00:00 2001 From: siyuniu-ms <123212536+siyuniu-ms@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:57:52 -0700 Subject: [PATCH 08/72] [main] support adding cross origin resource policy for #1851 (#2423) * add header * add test * Update IConfiguration.ts * rename * based on comment * Update Sender.tests.ts * local variable * Update Sender.ts * slight change test * Update Sender.ts --- .../Tests/Unit/src/Sender.tests.ts | 71 +++++++++++++++++++ .../src/Interfaces.ts | 14 ++++ .../src/Sender.ts | 13 +++- 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts b/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts index 4ebd8e378..cb7431121 100644 --- a/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts +++ b/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts @@ -2739,6 +2739,77 @@ export class SenderTests extends AITestClass { } }); + this.testCase({ + name: 'Users could set the cross-origin header via request', + useFakeTimers: true, + test: () => { + let core = new AppInsightsCore(); + let id = this._sender.identifier; + let coreConfig = { + instrumentationKey: 'abc', + isBeaconApiDisabled: true, + extensionConfig: { + [this._sender.identifier]: { + corsPolicy: "cross-origin", + } + } + } + core.initialize(coreConfig, [this._sender]); + + let sendBeaconCalled = false; + this.hookSendBeacon((url: string) => { + sendBeaconCalled = true; + return true; + }); + + const telemetryItem: ITelemetryItem = { + name: 'fake item', + iKey: 'iKey', + baseType: 'some type', + baseData: {} + }; + + try { + this._sender.processTelemetry(telemetryItem); + this.clock.tick(30000); + } catch(e) { + QUnit.assert.ok(false); + } + const CrossOriginResourcePolicyHeader: string = "X-Set-Cross-Origin-Resource-Policy"; + QUnit.assert.equal(1, this._getXhrRequests().length, "xhr sender is called"); + let headers = this._getXhrRequests()[0].requestHeaders; + QUnit.assert.ok(headers.hasOwnProperty(CrossOriginResourcePolicyHeader)); + QUnit.assert.equal(headers[CrossOriginResourcePolicyHeader], 'cross-origin'); + QUnit.assert.notOk(this._getXhrRequests()[0].requestHeaders.hasOwnProperty('testHeader')); + + // dynamic change + core.config.extensionConfig[this._sender.identifier].corsPolicy = "same-origin"; + this.clock.tick(1); + try { + this._sender.processTelemetry(telemetryItem); + this.clock.tick(30000); + } catch(e) { + QUnit.assert.ok(false); + } + headers = this._getXhrRequests()[1].requestHeaders; + QUnit.assert.ok(headers.hasOwnProperty(CrossOriginResourcePolicyHeader)); + QUnit.assert.equal(headers[CrossOriginResourcePolicyHeader], 'same-origin'); + QUnit.assert.notOk(this._getXhrRequests()[1].requestHeaders.hasOwnProperty('testHeader')); + + // dynamic change to null + core.config.extensionConfig[this._sender.identifier].corsPolicy = null; + this.clock.tick(1); + try { + this._sender.processTelemetry(telemetryItem); + this.clock.tick(30000); + } catch(e) { + QUnit.assert.ok(false); + } + headers = this._getXhrRequests()[2].requestHeaders; + QUnit.assert.notOk(this._getXhrRequests()[2].requestHeaders.hasOwnProperty(CrossOriginResourcePolicyHeader)); + } + }); + this.testCase({ name: 'Users are allowed to add customHeaders when endpointUrl is not Breeze.', test: () => { diff --git a/channels/applicationinsights-channel-js/src/Interfaces.ts b/channels/applicationinsights-channel-js/src/Interfaces.ts index 1df9c6625..3a0d975ef 100644 --- a/channels/applicationinsights-channel-js/src/Interfaces.ts +++ b/channels/applicationinsights-channel-js/src/Interfaces.ts @@ -166,6 +166,20 @@ export interface ISenderConfig { * @since 3.2.0 */ maxRetryCnt?: number; + + /** + * [Optional] Specifies the Cross-Origin Resource Policy (CORP) for the endpoint. + * This value is included in the response header as `Cross-Origin-Resource-Policy`, + * which helps control how resources can be shared across different origins. + * + * Possible values: + * - `same-site`: Allows access only from the same site. + * - `same-origin`: Allows access only from the same origin (protocol, host, and port). + * - `cross-origin`: Allows access from any origin. + * + * @since 3.3.7 + */ + corsPolicy?: string; } export interface IBackendResponse { diff --git a/channels/applicationinsights-channel-js/src/Sender.ts b/channels/applicationinsights-channel-js/src/Sender.ts index 992520ecc..9e76c676d 100644 --- a/channels/applicationinsights-channel-js/src/Sender.ts +++ b/channels/applicationinsights-channel-js/src/Sender.ts @@ -78,9 +78,12 @@ const defaultAppInsightsChannelConfig: IConfigDefaults = objDeepF alwaysUseXhrOverride: cfgDfBoolean(), transports: UNDEFINED_VALUE, retryCodes: UNDEFINED_VALUE, + corsPolicy: UNDEFINED_VALUE, maxRetryCnt: {isVal: isNumber, v:10} }); +const CrossOriginResourcePolicyHeader: string = "X-Set-Cross-Origin-Resource-Policy"; + function _chkSampling(value: number) { return !isNaN(value) && value > 0 && value <= 100; } @@ -268,7 +271,6 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { let ctx = createProcessTelemetryContext(null, config, core); // getExtCfg only finds undefined values from core let senderConfig = ctx.getExtCfg(identifier, defaultAppInsightsChannelConfig); - let curExtUrl = senderConfig.endpointUrl; // if it is not inital change (_endpointUrl has value) // if current sender endpoint url is not changed directly @@ -283,6 +285,15 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { } } + let corsPolicy = senderConfig.corsPolicy; + if (corsPolicy){ + if (corsPolicy === "same-origin" || corsPolicy === "same-site" || corsPolicy === "cross-origin") { + this.addHeader(CrossOriginResourcePolicyHeader, corsPolicy); + } + } else { + delete _headers[CrossOriginResourcePolicyHeader]; + } + if(isPromiseLike(senderConfig.instrumentationKey)) { // if it is promise, means the endpoint url is from core.endpointurl senderConfig.instrumentationKey = config.instrumentationKey as any; From a864ed59e64429d3bb532072e16362f161e0e727 Mon Sep 17 00:00:00 2001 From: Karlie-777 <79606506+Karlie-777@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:30:56 -0700 Subject: [PATCH 09/72] [Main][Task]31248254: Add Monitor Tests for CDN OPTIONS Calls (#2491) * add cdn montioring tests * update * update * update * update * update * update --- .../Unit/src/applicationinsights.e2e.tests.ts | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts b/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts index e4448490e..14cac09ed 100644 --- a/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts +++ b/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts @@ -9,6 +9,7 @@ import { createAsyncResolvedPromise } from '@nevware21/ts-async'; import { CONFIG_ENDPOINT_URL } from '../../../src/InternalConstants'; import { OfflineChannel } from '@microsoft/applicationinsights-offlinechannel-js'; import { IStackFrame } from '@microsoft/applicationinsights-common/src/Interfaces/Contracts/IStackFrame'; +import { utcNow } from '@nevware21/ts-utils'; function _checkExpectedFrame(expectedFrame: IStackFrame, actualFrame: IStackFrame, index: number) { Assert.equal(expectedFrame.assembly, actualFrame.assembly, index + ") Assembly is not as expected"); @@ -160,6 +161,7 @@ export class ApplicationInsightsTests extends AITestClass { this.addDependencyPluginTests(); this.addPropertiesPluginTests(); this.addCDNOverrideTests(); + this.addCdnMonitorTests(); } public addGenericE2ETests(): void { @@ -725,6 +727,154 @@ export class ApplicationInsightsTests extends AITestClass { }); } + public addCdnMonitorTests(): void { + this.testCaseAsync({ + name: "E2E.GenericTests: Fetch Current CDN V3", + stepDelay: 1, + useFakeServer: false, + useFakeFetch: false, + fakeFetchAutoRespond: false, + steps: [() => { + // Use beta endpoint to pre-test any changes before public V3 cdn + let random = utcNow(); + // Under Cors Mode, Options request will be auto-triggered + try { + fetch(`https://js.monitor.azure.com/beta/ai.3.gbl.min.js?${random}`, { + method: "GET" + }).then((res) => { + this._ctx.res = res; + res.text().then((val) => { + this._ctx.val = val; + }); + }); + } catch (e) { + Assert.ok(false, "Fetch Error: " + e); + } + + }].concat(PollingAssert.createPollingAssert(() => { + + if (this._ctx && this._ctx.res && this._ctx.val) { + let res = this._ctx.res; + let status = res.status; + if (status === 200) { + // for Response headers: + // content-type: text/javascript; charset=utf-8 + // x-ms-meta-aijssdksrc: should present + // x-ms-meta-aijssdkver should present + let headers = res.headers; + let headerCnt = 0; + headers.forEach((val, key) => { + if (key === "content-type") { + Assert.deepEqual(val, "text/javascript; charset=utf-8", "should have correct content-type response header"); + headerCnt ++; + } + if (key === "x-ms-meta-aijssdksrc") { + Assert.ok(val, "should have sdk src response header"); + headerCnt ++; + } + if (key === "x-ms-meta-aijssdkver") { + Assert.ok(val, "should have version number for response header"); + headerCnt ++; + } + }); + Assert.equal(headerCnt, 3, "all expected headers should be present"); + return true; + } + return false; + } + return false; + }, "Wait for response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "E2E.GenericTests: Fetch Current CDN V2", + stepDelay: 1, + useFakeServer: false, + useFakeFetch: false, + fakeFetchAutoRespond: false, + steps: [() => { + // Use public endpoint for V2 + let random = utcNow(); + // Under Cors Mode, Options request will be triggered + fetch(`https://js.monitor.azure.com/scripts/b/ai.2.gbl.min.js?${random}`, { + method: "GET" + }).then((res) => { + this._ctx.res = res; + res.text().then((val) => { + this._ctx.val = val; + }); + }); + + }].concat(PollingAssert.createPollingAssert(() => { + if (this._ctx && this._ctx.res && this._ctx.val) { + let res = this._ctx.res; + let status = res.status; + if (status === 200) { + // for Response headers: + // content-type: text/javascript; charset=utf-8 + // x-ms-meta-aijssdksrc: should present + // x-ms-meta-aijssdkver should present + let headers = res.headers; + let headerCnt = 0; + headers.forEach((val, key) => { + if (key === "content-type") { + Assert.deepEqual(val, "text/javascript; charset=utf-8", "should have correct content-type response header"); + headerCnt ++; + } + if (key === "x-ms-meta-aijssdksrc") { + Assert.ok(val, "should have sdk src response header"); + headerCnt ++; + } + if (key === "x-ms-meta-aijssdkver") { + Assert.ok(val, "should have version number for response header"); + headerCnt ++; + } + }); + Assert.equal(headerCnt, 3, "all expected headers should be present"); + return true; + } + return false; + } + return false; + }, "Wait for response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "E2E.GenericTests: Fetch Static Web CDN V3", + stepDelay: 1, + useFakeServer: false, + useFakeFetch: false, + fakeFetchAutoRespond: false, + steps: [() => { + // Use beta endpoint to pre-test any changes before public V3 cdn + let random = utcNow(); + // Under Cors Mode, Options request will be auto-triggered + try { + fetch(`https://js0.tst.applicationinsights.io/scripts/b/ai.3.gbl.min.js?${random}`, { + method: "GET" + }).then((res) => { + this._ctx.res = res; + if (res.ok) { + res.text().then((val) => { + this._ctx.val = val; + }); + } + }).catch((e) => { + this._ctx.err = e.message; + }) + } catch (e) { + this._ctx.err = e; + } + }].concat(PollingAssert.createPollingAssert(() => { + + if (this._ctx && this._ctx.err) { + return true; + } + return false; + }, "Wait for response" + new Date().toISOString(), 60, 1000) as any) + }); + } + public addAsyncTests(): void { this.testCaseAsync({ name: "E2E.GenericTests: Send events with offline support", From 6258457d60cabbfd34348187b9aa21f166508250 Mon Sep 17 00:00:00 2001 From: Nev <54870357+MSNev@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:52:36 -0700 Subject: [PATCH 10/72] [main] Minor fixes for handling ikey promises with dynamic changes (#2500) - Additional Minification improvements --- AISKU/src/AISku.ts | 82 +++--- .../src/JavaScriptSDK/AppInsightsCore.ts | 234 +++++++++--------- 2 files changed, 162 insertions(+), 154 deletions(-) diff --git a/AISKU/src/AISku.ts b/AISKU/src/AISku.ts index 84b93fa73..4efcf6eef 100644 --- a/AISKU/src/AISku.ts +++ b/AISKU/src/AISku.ts @@ -103,12 +103,27 @@ function _chkDiagLevel(value: number) { return value && value > 0; } +function _parseCs(config: IConfiguration & IConfig, configCs: string | IPromise) { + return createSyncPromise((resolve, reject) => { + doAwaitResponse(configCs, (res) => { + let curCs = res && res.value; + let parsedCs = null; + if (!res.rejected && curCs) { + // replace cs with resolved values in case of circular promises + config.connectionString = curCs; + parsedCs = parseConnectionString(curCs); + } + + // if can't resolve cs promise, null will be returned + resolve(parsedCs); + }); + }); +} + /** * Application Insights API * @group Entrypoint * @group Classes - * @class Initialization - * @implements {IApplicationInsights} */ export class AppInsightsSku implements IApplicationInsights { public snippet: Snippet; @@ -200,60 +215,47 @@ export class AppInsightsSku implements IApplicationInsights { // Will get recalled if any referenced values are changed _addUnloadHook(onConfigChange(cfgHandler, () => { - let configCs = _config.connectionString; - - function _parseCs() { - return createSyncPromise((resolve, reject) => { - doAwaitResponse(configCs, (res) => { - let curCs = res && res.value; - let parsedCs = null; - if (!res.rejected && curCs) { - // replace cs with resolved values in case of circular promises - _config.connectionString = curCs; - parsedCs = parseConnectionString(curCs); - } - // if can't resolve cs promise, null will be returned - resolve(parsedCs); - }); - }); - - } + let configCs = _config.connectionString; if (isPromiseLike(configCs)) { let ikeyPromise = createSyncPromise((resolve, reject) => { - _parseCs().then((cs) => { - let ikey = _config.instrumentationKey; - ikey = cs && cs.instrumentationkey || ikey; - resolve(ikey); - }).catch((e) => { - // parseCs will always resolve(unless timeout) - // return null in case any error happens - resolve(null); + doAwaitResponse(_parseCs(_config, configCs), (rsp) => { + if (!rsp.rejected) { + let ikey = _config.instrumentationKey; + let cs = rsp.value; + ikey = cs && cs.instrumentationkey || ikey; + resolve(ikey); + } else { + // parseCs will always resolve(unless timeout) + // return null in case any error happens + resolve(null); + } }); - }); let url: IPromise | string = _config.userOverrideEndpointUrl; if (isNullOrUndefined(url)) { url = createSyncPromise((resolve, reject) => { - _parseCs().then((cs) => { - let url = _config.endpointUrl; - let ingest = cs && cs.ingestionendpoint; - url = ingest? ingest + DEFAULT_BREEZE_PATH : url; - resolve(url); - }).catch((e) => { - // parseCs will always resolve(unless timeout) - // return null in case any error happens - resolve(null); + doAwaitResponse(_parseCs(_config, configCs), (rsp) => { + if (!rsp.rejected) { + let url = _config.endpointUrl; + let cs = rsp.value; + let ingest = cs && cs.ingestionendpoint; + url = ingest? ingest + DEFAULT_BREEZE_PATH : url; + resolve(url); + } else { + // parseCs will always resolve(unless timeout) + // return null in case any error happens + resolve(null); + } }); - }); } _config.instrumentationKey = ikeyPromise; _config.endpointUrl = url; - } + if (isString(configCs) && configCs) { // confirm if promiselike function present // handle cs promise here diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/AppInsightsCore.ts b/shared/AppInsightsCore/src/JavaScriptSDK/AppInsightsCore.ts index 8c06d42db..53801d16c 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/AppInsightsCore.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/AppInsightsCore.ts @@ -348,121 +348,11 @@ export class AppInsightsCore im // This will be "re-run" if the referenced config properties are changed _addUnloadHook(_configHandler.watch((details) => { let rootCfg = details.cfg; - - let isPending = _activeStatus === eActiveStatus.PENDING; - - if (isPending){ - // means waiting for previous promises to be resolved, won't apply new changes - return; - } _initInMemoMaxSize = rootCfg.initInMemoMaxSize || maxInitQueueSize; - // app Insights core only handle ikey and endpointurl, aisku will handle cs - let ikey = rootCfg.instrumentationKey; - let endpointUrl = rootCfg.endpointUrl; // do not need to validate endpoint url, if it is null, default one will be set by sender - - if (isNullOrUndefined(ikey)) { - _instrumentationKey = null; - // if new ikey is null, set status to be inactive, all new events will be saved in memory or dropped - _activeStatus = ActiveStatus.INACTIVE; - let msg = "Please provide instrumentation key"; - - if (!_isInitialized) { - // only throw error during initialization - throwError(msg); - } else { - _throwInternal(_logger, eLoggingSeverity.CRITICAL, _eInternalMessageId.InvalidInstrumentationKey, msg); - _releaseQueues(); - } - return; - - } - - let promises: IPromise[] = []; - if (isPromiseLike(ikey)) { - promises.push(ikey); - _instrumentationKey = null; // reset current local ikey variable (otherwise it will always be the previous ikeys if timeout is called before promise cb) - } else { - // string - _instrumentationKey = ikey; - } - - if (isPromiseLike(endpointUrl)) { - promises.push(endpointUrl); - _endpoint = null; // reset current local endpoint variable (otherwise it will always be the previous urls if timeout is called before promise cb) - } else { - // string or null - _endpoint = endpointUrl; - } - - // at least have one promise - if (promises.length) { - // reset to false for new dynamic changes - _isStatusSet = false; - _activeStatus = eActiveStatus.PENDING; - let initTimeout = isNotNullOrUndefined(rootCfg.initTimeOut)? rootCfg.initTimeOut : maxInitTimeout; // rootCfg.initTimeOut could be 0 - let allPromises = createSyncAllSettledPromise(promises); - _initTimer = scheduleTimeout(() => { - // set _isStatusSet to true - // set active status - // release queues - _initTimer = null; - if (!_isStatusSet) { - _setStatus(); - } - - }, initTimeout); - - doAwaitResponse(allPromises, (response) => { - try { - if (_isStatusSet) { - // promises take too long to resolve, ignore them - // active status should be set by timeout already - return; - } - - if (!response.rejected) { - let values = response.value; - if (values && values.length) { - // ikey - let ikeyRes = values[0]; - _instrumentationKey = ikeyRes && ikeyRes.value; - - // endpoint - if (values.length > 1) { - let endpointRes = values[1]; - _endpoint = endpointRes && endpointRes.value; - - } - - } - if (_instrumentationKey) { - // if ikey is null, no need to trigger extra dynamic changes for extensions - config.instrumentationKey = _instrumentationKey; // set config.instrumentationKey for extensions to consume - config.endpointUrl = _endpoint; // set config.endpointUrl for extensions to consume - } - - } - - // set _isStatusSet to true - // set active status - // release queues - _setStatus(); - - } catch (e) { - if (!_isStatusSet){ - _setStatus(); - } - } - - }); - } else { - // means no promises - _setStatus(); - - } + + _handleIKeyEndpointPromises(rootCfg); - //_instrumentationKey = details.cfg.instrumentationKey; // Mark the extensionConfig and all first level keys as referenced // This is so that calls to getExtCfg() will always return the same object // Even when a user may "re-assign" the plugin properties (or it's unloaded/reloaded) @@ -470,8 +360,6 @@ export class AppInsightsCore im objForEachKey(extCfg, (key) => { details.ref(extCfg, key); }); - - })); _notificationManager = notificationManager; @@ -647,6 +535,124 @@ export class AppInsightsCore im return _startLogPoller(true); }; + function _handleIKeyEndpointPromises(theConfig: IConfiguration) { + // app Insights core only handle ikey and endpointurl, aisku will handle cs + // But we want to reference these config values so that if any future changes are made + // this will trigger the re-run of the watch function + // and the ikey and endpointUrl will be set to the new values + let ikey = theConfig.instrumentationKey; + let endpointUrl = theConfig.endpointUrl; // do not need to validate endpoint url, if it is null, default one will be set by sender + + // Check if we are waiting for previous promises to be resolved, won't apply new changes + if (_activeStatus !== eActiveStatus.PENDING) { + if (isNullOrUndefined(ikey)) { + _instrumentationKey = null; + + // if new ikey is null, set status to be inactive, all new events will be saved in memory or dropped + _activeStatus = ActiveStatus.INACTIVE; + let msg = "Please provide instrumentation key"; + + if (!_isInitialized) { + // only throw error during initialization + throwError(msg); + } else { + _throwInternal(_logger, eLoggingSeverity.CRITICAL, _eInternalMessageId.InvalidInstrumentationKey, msg); + _releaseQueues(); + } + + return; + } + + let promises: IPromise[] = []; + if (isPromiseLike(ikey)) { + promises.push(ikey); + _instrumentationKey = null; // reset current local ikey variable (otherwise it will always be the previous ikeys if timeout is called before promise cb) + } else { + // string + _instrumentationKey = ikey; + } + + if (isPromiseLike(endpointUrl)) { + promises.push(endpointUrl); + _endpoint = null; // reset current local endpoint variable (otherwise it will always be the previous urls if timeout is called before promise cb) + } else { + // string or null + _endpoint = endpointUrl; + } + + // at least have one promise + if (promises.length) { + _waitForInitPromises(theConfig, promises); + } else { + // means no promises + _setStatus(); + } + } + } + + function _waitForInitPromises(theConfig: IConfiguration, promises: IPromise[]) { + // reset to false for new dynamic changes + _isStatusSet = false; + _activeStatus = eActiveStatus.PENDING; + let initTimeout = isNotNullOrUndefined(theConfig.initTimeOut)? theConfig.initTimeOut : maxInitTimeout; // theConfig.initTimeOut could be 0 + let allPromises = createSyncAllSettledPromise(promises); + + if (_initTimer) { + // Stop any previous timer + _initTimer.cancel(); + } + + _initTimer = scheduleTimeout(() => { + // set _isStatusSet to true + // set active status + // release queues + _initTimer = null; + if (!_isStatusSet) { + _setStatus(); + } + }, initTimeout); + + doAwaitResponse(allPromises, (response) => { + try { + if (_isStatusSet) { + // promises take too long to resolve, ignore them + // active status should be set by timeout already + return; + } + + if (!response.rejected) { + let values = response.value; + if (values && values.length) { + // ikey + let ikeyRes = values[0]; + _instrumentationKey = ikeyRes && ikeyRes.value; + + // endpoint + if (values.length > 1) { + let endpointRes = values[1]; + _endpoint = endpointRes && endpointRes.value; + } + } + + if (_instrumentationKey) { + // if ikey is null, no need to trigger extra dynamic changes for extensions + theConfig.instrumentationKey = _instrumentationKey; // set config.instrumentationKey for extensions to consume + theConfig.endpointUrl = _endpoint; // set config.endpointUrl for extensions to consume + } + } + + // set _isStatusSet to true + // set active status + // release queues + _setStatus(); + } catch (e) { + if (!_isStatusSet){ + _setStatus(); + } + } + }); + } + function _setStatus() { _isStatusSet = true; if (isNullOrUndefined(_instrumentationKey)) { From 10053b1bad32796e22d663573d6d88ebc52e9662 Mon Sep 17 00:00:00 2001 From: siyuniu-ms <123212536+siyuniu-ms@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:52:46 -0700 Subject: [PATCH 11/72] [chrome debug tool] publish 0.8.0 for manifect V3 (#2499) --- tools/chrome-debug-extension/package.json | 2 +- version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/chrome-debug-extension/package.json b/tools/chrome-debug-extension/package.json index 2adc08d3b..895fafbed 100644 --- a/tools/chrome-debug-extension/package.json +++ b/tools/chrome-debug-extension/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/applicationinsights-chrome-debug-extension", - "version": "0.7.6", + "version": "0.8.0", "description": "A chrome based browser extension that provides a real time view of what's happening in Application Insights including what telemetry is being logged by the web application", "homepage": "https://github.com/microsoft/ApplicationInsights-JS/tree/main/tools/chrome-debug-extension#readme", "keywords": [ diff --git a/version.json b/version.json index 266e69773..b8c988482 100644 --- a/version.json +++ b/version.json @@ -58,7 +58,7 @@ }, "@microsoft/applicationinsights-chrome-debug-extension": { "package": "tools/chrome-debug-extension/package.json", - "release": "0.7.6" + "release": "0.8.0" }, "applicationinsights-web-config": { "package": "tools/config/package.json", From a8d229e1d8c21d3a3906ee9db936f17db1cad216 Mon Sep 17 00:00:00 2001 From: Karlie-777 <79606506+Karlie-777@users.noreply.github.com> Date: Wed, 16 Apr 2025 18:37:29 -0700 Subject: [PATCH 12/72] [Main][Task]31233527:Change Default RequestSizeLimitBytes (#2501) * add post channel config * update * udpate * update * update * update * udpate * update * update * update --- AISKU/Tests/Unit/src/AISKUSize.Tests.ts | 4 +- .../Tests/Unit/src/AISKULightSize.Tests.ts | 4 +- channels/1ds-post-js/src/HttpManager.ts | 2 +- channels/1ds-post-js/src/Serializer.ts | 3 +- common/config/rush/npm-shrinkwrap.json | 258 ++++++++++-------- .../Unit/src/AnalyticsExtensionSize.tests.ts | 2 +- .../test/Unit/src/FileSizeCheckTest.ts | 2 +- .../Unit/src/AppInsightsCoreSize.Tests.ts | 4 +- 8 files changed, 151 insertions(+), 128 deletions(-) diff --git a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts index a1a231906..638a76ce8 100644 --- a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts +++ b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts @@ -54,8 +54,8 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class AISKUSizeCheck extends AITestClass { - private readonly MAX_RAW_SIZE = 145; - private readonly MAX_BUNDLE_SIZE = 145; + private readonly MAX_RAW_SIZE = 146; + private readonly MAX_BUNDLE_SIZE = 146; private readonly MAX_RAW_DEFLATE_SIZE = 58; private readonly MAX_BUNDLE_DEFLATE_SIZE = 58; private readonly rawFilePath = "../dist/es5/applicationinsights-web.min.js"; diff --git a/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts b/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts index 80cf44a22..ab5e74f0b 100644 --- a/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts +++ b/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts @@ -51,8 +51,8 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class AISKULightSizeCheck extends AITestClass { - private readonly MAX_RAW_SIZE = 91; - private readonly MAX_BUNDLE_SIZE = 91; + private readonly MAX_RAW_SIZE = 92; + private readonly MAX_BUNDLE_SIZE = 92; private readonly MAX_RAW_DEFLATE_SIZE = 38; private readonly MAX_BUNDLE_DEFLATE_SIZE = 38; private readonly rawFilePath = "../dist/es5/applicationinsights-web-basic.min.js"; diff --git a/channels/1ds-post-js/src/HttpManager.ts b/channels/1ds-post-js/src/HttpManager.ts index bb3caf047..a27121ede 100644 --- a/channels/1ds-post-js/src/HttpManager.ts +++ b/channels/1ds-post-js/src/HttpManager.ts @@ -774,7 +774,7 @@ export class HttpManager { // Make sure we have a payload object thePayload = thePayload || _serializer.createPayload(retryCount, isTeardown, isSynchronous, isReducedPayload, sendReason, sendType); - + // Add the batch to the current payload if (!_serializer.appendPayload(thePayload, theBatch, maxEventsPerBatch)) { // Entire batch was not added so send the payload and retry adding this batch diff --git a/channels/1ds-post-js/src/Serializer.ts b/channels/1ds-post-js/src/Serializer.ts index 821eb0b91..8b688eca6 100644 --- a/channels/1ds-post-js/src/Serializer.ts +++ b/channels/1ds-post-js/src/Serializer.ts @@ -30,7 +30,8 @@ import { mathMin, strSubstr } from "@nevware21/ts-utils"; */ const _MAX_STRING_JOINS = 20; -const RequestSizeLimitBytes = 3984588; // approx 3.8 Mb +// Max Size set by One Collector: https://msazure.visualstudio.com/OneDsCollector/_git/Collector?path=/Services/Azure/CollectorWorkerRoleAzure/ServiceConfiguration.Cloud.cscfg +const RequestSizeLimitBytes = 3145728; // approx 3.15 Mb const BeaconRequestSizeLimitBytes = 65000; // approx 64kb (the current Edge, Firefox and Chrome max limit) const MaxRecordSize = 2000000; // approx 2 Mb const MaxBeaconRecordSize = mathMin(MaxRecordSize, BeaconRequestSizeLimitBytes); diff --git a/common/config/rush/npm-shrinkwrap.json b/common/config/rush/npm-shrinkwrap.json index 6ae4cff99..b843d7618 100644 --- a/common/config/rush/npm-shrinkwrap.json +++ b/common/config/rush/npm-shrinkwrap.json @@ -183,9 +183,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", "peer": true, "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -210,9 +210,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "peer": true, "dependencies": { "@eslint/object-schema": "^2.1.6", @@ -235,6 +235,15 @@ "node": "*" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", @@ -248,9 +257,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", - "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "peer": true, "dependencies": { "ajv": "^6.12.4", @@ -305,9 +314,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", - "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", + "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -323,18 +332,30 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", "peer": true, "dependencies": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.13.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -402,42 +423,42 @@ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@microsoft/api-extractor": { - "version": "7.51.1", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.51.1.tgz", - "integrity": "sha512-VoFvIeYXme8QctXDkixy1KIn750kZaFy2snAEOB3nhDFfbBcJNEcvBrpCIQIV09MqI4g9egKUkg+/12WMRC77w==", + "version": "7.52.3", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.52.3.tgz", + "integrity": "sha512-QEs6l8h7p9eOSHrQ9NBBUZhUuq+j/2QKcRgigbSs2YQepKz8glvsqmsUOp+nvuaY60ps7KkpVVYQCj81WLoMVQ==", "dependencies": { - "@microsoft/api-extractor-model": "7.30.3", + "@microsoft/api-extractor-model": "7.30.5", "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", - "@rushstack/node-core-library": "5.11.0", + "@rushstack/node-core-library": "5.13.0", "@rushstack/rig-package": "0.5.3", - "@rushstack/terminal": "0.15.0", - "@rushstack/ts-command-line": "4.23.5", + "@rushstack/terminal": "0.15.2", + "@rushstack/ts-command-line": "4.23.7", "lodash": "~4.17.15", "minimatch": "~3.0.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", - "typescript": "5.7.3" + "typescript": "5.8.2" }, "bin": { "api-extractor": "bin/api-extractor" } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.30.3", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.3.tgz", - "integrity": "sha512-yEAvq0F78MmStXdqz9TTT4PZ05Xu5R8nqgwI5xmUmQjWBQ9E6R2n8HB/iZMRciG4rf9iwI2mtuQwIzDXBvHn1w==", + "version": "7.30.5", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.5.tgz", + "integrity": "sha512-0ic4rcbcDZHz833RaTZWTGu+NpNgrxVNjVaor0ZDUymfDFzjA/Uuk8hYziIUIOEOSTfmIQqyzVwlzxZxPe7tOA==", "dependencies": { "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", - "@rushstack/node-core-library": "5.11.0" + "@rushstack/node-core-library": "5.13.0" } }, "node_modules/@microsoft/api-extractor/node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -516,9 +537,9 @@ } }, "node_modules/@nevware21/ts-utils": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.8.tgz", - "integrity": "sha512-62Y1mHgSu99IK4BRKC3sxdj/uIBHy6SDof3WUd29jom2HQy8sGCUdbYtFwMOkbUS6rahkL11Eg/ImtwsQsCnyw==" + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.12.1.tgz", + "integrity": "sha512-rAoErmxI9IW5BKGp8WK1FPG6dqGmxDjArgMxAf+It/+z8FQ5y9d/yDcPNNBfwY6QGpHqSaHRHFt083+L9uh7eg==" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -1404,9 +1425,9 @@ } }, "node_modules/@rushstack/node-core-library": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.11.0.tgz", - "integrity": "sha512-I8+VzG9A0F3nH2rLpPd7hF8F7l5Xb7D+ldrWVZYegXM6CsKkvWc670RlgK3WX8/AseZfXA/vVrh0bpXe2Y2UDQ==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.13.0.tgz", + "integrity": "sha512-IGVhy+JgUacAdCGXKUrRhwHMTzqhWwZUI+qEPcdzsb80heOw0QPbhhoVsoiMF7Klp8eYsp7hzpScMXmOa3Uhfg==", "dependencies": { "ajv": "~8.13.0", "ajv-draft-04": "~1.0.0", @@ -1451,11 +1472,11 @@ } }, "node_modules/@rushstack/terminal": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.0.tgz", - "integrity": "sha512-vXQPRQ+vJJn4GVqxkwRe+UGgzNxdV8xuJZY2zem46Y0p3tlahucH9/hPmLGj2i9dQnUBFiRnoM9/KW7PYw8F4Q==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.2.tgz", + "integrity": "sha512-7Hmc0ysK5077R/IkLS9hYu0QuNafm+TbZbtYVzCMbeOdMjaRboLKrhryjwZSRJGJzu+TV1ON7qZHeqf58XfLpA==", "dependencies": { - "@rushstack/node-core-library": "5.11.0", + "@rushstack/node-core-library": "5.13.0", "supports-color": "~8.1.1" }, "peerDependencies": { @@ -1468,11 +1489,11 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "4.23.5", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.5.tgz", - "integrity": "sha512-jg70HfoK44KfSP3MTiL5rxsZH7X1ktX3cZs9Sl8eDu1/LxJSbPsh0MOFRC710lIuYYSgxWjI5AjbCBAl7u3RxA==", + "version": "4.23.7", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.7.tgz", + "integrity": "sha512-Gr9cB7DGe6uz5vq2wdr89WbVDKz0UeuFEn5H2CfWDe7JvjFFaiV15gi6mqDBTbHhHCWS7w8mF1h3BnIfUndqdA==", "dependencies": { - "@rushstack/terminal": "0.15.0", + "@rushstack/terminal": "0.15.2", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" @@ -1620,9 +1641,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" }, "node_modules/@types/file-saver": { "version": "2.0.7", @@ -1730,9 +1751,9 @@ "integrity": "sha512-II+C1wgzUia0g+tGAH+PBb4XiTm8/C/i6sN23r21NNskBYOYrv+qnW0tFQ/IxZzKVwrK4CTglf8YO3poJUclQA==" }, "node_modules/@types/react": { - "version": "16.14.62", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.62.tgz", - "integrity": "sha512-BWf7hqninZav6nerxXj+NeZT/mTpDeG6Lk2zREHAy63CrnXoOGPGtNqTFYFN/sqpSaREDP5otVV88axIXmKfGA==", + "version": "16.14.63", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.63.tgz", + "integrity": "sha512-s83gano0fRBVEw3ejdLpjgvU83F0LIeeuXqdxfPZF/Sc2bhr60tEqCK1zZ+aLirBwRSD6V5zCtOsEjcwKow3JQ==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "^0.16", @@ -1785,16 +1806,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz", - "integrity": "sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz", + "integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==", "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/type-utils": "8.26.0", - "@typescript-eslint/utils": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/type-utils": "8.30.1", + "@typescript-eslint/utils": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1814,15 +1835,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.0.tgz", - "integrity": "sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz", + "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/typescript-estree": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/typescript-estree": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "debug": "^4.3.4" }, "engines": { @@ -1838,13 +1859,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz", - "integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", + "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", "peer": true, "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0" + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1855,13 +1876,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz", - "integrity": "sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz", + "integrity": "sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==", "peer": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.26.0", - "@typescript-eslint/utils": "8.26.0", + "@typescript-eslint/typescript-estree": "8.30.1", + "@typescript-eslint/utils": "8.30.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -1878,9 +1899,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.0.tgz", - "integrity": "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", + "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1891,13 +1912,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz", - "integrity": "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", + "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", "peer": true, "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1953,15 +1974,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.0.tgz", - "integrity": "sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", + "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/typescript-estree": "8.26.0" + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/typescript-estree": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1976,12 +1997,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz", - "integrity": "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", + "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", "peer": true, "dependencies": { - "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/types": "8.30.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2015,9 +2036,9 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "peer": true, "bin": { "acorn": "bin/acorn" @@ -2450,9 +2471,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001701", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001701.tgz", - "integrity": "sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==", + "version": "1.0.30001714", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz", + "integrity": "sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==", "funding": [ { "type": "opencollective", @@ -2898,9 +2919,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.5.110", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.110.tgz", - "integrity": "sha512-/p/OvOm6AfLtQteAHTUWwf+Vhh76PlluagzQlSnxMoOJ4R6SmAScWBrVev6rExJoUhP9zudN9+lBxoYUEmC1HQ==" + "version": "1.5.137", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz", + "integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA==" }, "node_modules/emoji-regex-xs": { "version": "1.0.0", @@ -2968,17 +2989,18 @@ } }, "node_modules/eslint": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", - "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", + "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.0", "@eslint/core": "^0.12.0", - "@eslint/eslintrc": "^3.3.0", - "@eslint/js": "9.21.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.24.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2990,7 +3012,7 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", + "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", @@ -3054,9 +3076,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "peer": true, "dependencies": { "esrecurse": "^4.3.0", @@ -6560,9 +6582,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "peer": true, "engines": { "node": ">=18.12" @@ -6653,9 +6675,9 @@ } }, "node_modules/typedoc/node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", "bin": { "yaml": "bin.mjs" }, diff --git a/extensions/applicationinsights-analytics-js/Tests/Unit/src/AnalyticsExtensionSize.tests.ts b/extensions/applicationinsights-analytics-js/Tests/Unit/src/AnalyticsExtensionSize.tests.ts index d72053af7..f61505caf 100644 --- a/extensions/applicationinsights-analytics-js/Tests/Unit/src/AnalyticsExtensionSize.tests.ts +++ b/extensions/applicationinsights-analytics-js/Tests/Unit/src/AnalyticsExtensionSize.tests.ts @@ -51,7 +51,7 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class AnalyticsExtensionSizeCheck extends AITestClass { - private readonly MAX_DEFLATE_SIZE = 24; + private readonly MAX_DEFLATE_SIZE = 25; private readonly rawFilePath = "../dist/es5/applicationinsights-analytics-js.min.js"; private readonly prodFilePaath = "../browser/es5/applicationinsights-analytics-js.min.js" diff --git a/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts b/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts index bac557272..d2fa9d3ac 100644 --- a/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts +++ b/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts @@ -51,7 +51,7 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class FileSizeCheckTest extends AITestClass { - private readonly MAX_BUNDLE_SIZE = 68; + private readonly MAX_BUNDLE_SIZE = 69; private readonly MAX_DEFLATE_SIZE = 29; private readonly bundleFilePath = "../bundle/es5/ms.core.min.js"; diff --git a/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts index a624e8008..c8da7b41d 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts @@ -51,8 +51,8 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class AppInsightsCoreSizeCheck extends AITestClass { - private readonly MAX_RAW_SIZE = 65; - private readonly MAX_BUNDLE_SIZE = 65; + private readonly MAX_RAW_SIZE = 67; + private readonly MAX_BUNDLE_SIZE = 67; private readonly MAX_RAW_DEFLATE_SIZE = 28; private readonly MAX_BUNDLE_DEFLATE_SIZE = 28; private readonly rawFilePath = "../dist/es5/applicationinsights-core-js.min.js"; From bb528091da3e21186ee3a7233171e9259db90858 Mon Sep 17 00:00:00 2001 From: siyuniu-ms <123212536+siyuniu-ms@users.noreply.github.com> Date: Thu, 24 Apr 2025 15:37:25 -0700 Subject: [PATCH 13/72] [main][stats beat] implement stats beat in application insights (#2489) --- AISKU/Tests/Manual/testVersionConflict.html | 10 +- .../Unit/src/applicationinsights.e2e.tests.ts | 2 +- .../Tests/Unit/src/StatsBeat.tests.ts | 255 ++++++++++++++++ .../Tests/Unit/src/aichannel.tests.ts | 2 + .../src/SendBuffer.ts | 2 + .../src/Sender.ts | 92 ++++-- .../Tests/Unit/src/cfgsynchelper.tests.ts | 8 +- .../Tests/Unit/src/StatsBeat.Tests.ts | 280 ++++++++++++++++++ .../Tests/Unit/src/aiunittests.ts | 3 + .../IAppInsightsCore.ts | 3 + .../IConfiguration.ts | 19 ++ .../JavaScriptSDK.Interfaces/IStatsBeat.ts | 16 + .../IStatsBeatEvent.ts | 10 + .../JavaScriptSDK.Interfaces/IXHROverride.ts | 1 - .../src/JavaScriptSDK/AppInsightsCore.ts | 36 ++- .../src/JavaScriptSDK/NetworkStatsbeat.ts | 22 ++ .../src/JavaScriptSDK/SenderPostManager.ts | 28 +- .../src/JavaScriptSDK/StatsBeat.ts | 203 +++++++++++++ .../src/applicationinsights-core-js.ts | 2 + 19 files changed, 952 insertions(+), 42 deletions(-) create mode 100644 channels/applicationinsights-channel-js/Tests/Unit/src/StatsBeat.tests.ts create mode 100644 shared/AppInsightsCore/Tests/Unit/src/StatsBeat.Tests.ts create mode 100644 shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IStatsBeat.ts create mode 100644 shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IStatsBeatEvent.ts create mode 100644 shared/AppInsightsCore/src/JavaScriptSDK/NetworkStatsbeat.ts create mode 100644 shared/AppInsightsCore/src/JavaScriptSDK/StatsBeat.ts diff --git a/AISKU/Tests/Manual/testVersionConflict.html b/AISKU/Tests/Manual/testVersionConflict.html index 2b7f4ca0e..1e1b5e997 100644 --- a/AISKU/Tests/Manual/testVersionConflict.html +++ b/AISKU/Tests/Manual/testVersionConflict.html @@ -10,7 +10,8 @@

Microsoft Application Insights JavaScript SDK - AISKU

- --> \ No newline at end of file diff --git a/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts b/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts index 14cac09ed..45973ccb1 100644 --- a/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts +++ b/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts @@ -154,7 +154,7 @@ export class ApplicationInsightsTests extends AITestClass { } public registerTests() { - this.addDynamicConfigTests() + this.addDynamicConfigTests(); this.addGenericE2ETests(); this.addAnalyticsApiTests(); this.addAsyncTests(); diff --git a/channels/applicationinsights-channel-js/Tests/Unit/src/StatsBeat.tests.ts b/channels/applicationinsights-channel-js/Tests/Unit/src/StatsBeat.tests.ts new file mode 100644 index 000000000..b204ecc86 --- /dev/null +++ b/channels/applicationinsights-channel-js/Tests/Unit/src/StatsBeat.tests.ts @@ -0,0 +1,255 @@ +import { AITestClass, Assert, PollingAssert } from "@microsoft/ai-test-framework"; +import { AppInsightsCore, getWindow, IPayloadData, ITelemetryItem, TransportType } from "@microsoft/applicationinsights-core-js"; +import { Sender } from "../../../src/Sender"; +import { SinonSpy, SinonStub } from "sinon"; +import { ISenderConfig } from "../../../types/applicationinsights-channel-js"; +import { isBeaconApiSupported } from "@microsoft/applicationinsights-common"; + +export class StatsbeatTests extends AITestClass { + private _core: AppInsightsCore; + private _sender: Sender; + private statsbeatCountSpy: SinonSpy; + private fetchStub: sinon.SinonStub; + private beaconStub: sinon.SinonStub; + private trackSpy: SinonSpy; + + public testInitialize() { + this._core = new AppInsightsCore(); + this._sender = new Sender(); + } + + public testFinishedCleanup() { + if (this._sender && this._sender.isInitialized()) { + this._sender.pause(); + this._sender._buffer.clear(); + this._sender.teardown(); + } + this._sender = null; + this._core = null; + if (this.statsbeatCountSpy) { + this.statsbeatCountSpy.restore(); + } + if (this.fetchStub) { + this.fetchStub.restore(); + } + if (this.beaconStub) { + this.beaconStub.restore(); + } + if (this.trackSpy) { + this.trackSpy.restore(); + } + } + + private initializeCoreAndSender(config: any, instrumentationKey: string) { + const sender = new Sender(); + const core = new AppInsightsCore(); + const coreConfig = { + instrumentationKey, + _sdk: { stats: true }, + extensionConfig: { [sender.identifier]: config } + }; + + core.initialize(coreConfig, [sender]); + this.statsbeatCountSpy = this.sandbox.spy(core.getStatsBeat(), "count"); + this.trackSpy = this.sandbox.spy(core, "track"); + + this.onDone(() => { + sender.teardown(); + }); + + return { core, sender }; + } + + private createSenderConfig(transportType: TransportType) { + return { + endpointUrl: "https://test", + emitLineDelimitedJson: false, + maxBatchInterval: 15000, + maxBatchSizeInBytes: 102400, + disableTelemetry: false, + enableSessionStorageBuffer: true, + isRetryDisabled: false, + isBeaconApiDisabled: false, + disableXhr: false, + onunloadDisableFetch: false, + onunloadDisableBeacon: false, + namePrefix: "", + samplingPercentage: 100, + customHeaders: [{ header: "header", value: "val" }], + convertUndefined: "", + eventsLimitInMem: 10000, + transports: [transportType] + }; + } + + private processTelemetryAndFlush(sender: Sender, telemetryItem: ITelemetryItem) { + try { + sender.processTelemetry(telemetryItem, null); + sender.flush(); + } catch (e) { + QUnit.assert.ok(false, "Unexpected error during telemetry processing"); + } + this.clock.tick(900000); // Simulate time passing for statsbeat to be sent + } + + private assertStatsbeatCall(statusCode: number, eventName: string) { + Assert.equal(this.statsbeatCountSpy.callCount, 1, "Statsbeat count should be called once"); + Assert.equal(this.statsbeatCountSpy.firstCall.args[0], statusCode, `Statsbeat count should be called with status ${statusCode}`); + const data = JSON.stringify(this.statsbeatCountSpy.firstCall.args[1]); + Assert.ok(data.includes("startTime"), "Statsbeat count should be called with startTime set"); + const statsbeatEvent = this.trackSpy.firstCall.args[0]; + Assert.equal(statsbeatEvent.baseType, "MetricData", "Statsbeat event should be of type MetricData"); + Assert.equal(statsbeatEvent.baseData.name, eventName, `Statsbeat event should be of type ${eventName}`); + } + + public registerTests() { + this.testCase({ + name: "Statsbeat initializes when stats is true", + test: () => { + const config = { + _sdk: { stats: true }, + instrumentationKey: "Test-iKey" + }; + + this._core.initialize(config, [this._sender]); + const statsbeat = this._core.getStatsBeat(); + + QUnit.assert.ok(statsbeat, "Statsbeat is initialized"); + QUnit.assert.ok(statsbeat.isInitialized(), "Statsbeat is marked as initialized"); + } + }); + + this.testCaseAsync({ + name: "Statsbeat increments success count when fetch sender is called once", + useFakeTimers: true, + useFakeServer: true, + stepDelay: 100, + steps: [ + () => { + this.fetchStub = this.sandbox.stub(window, "fetch").callsFake(() => { // only fetch is supported to stub, why? + return Promise.resolve(new Response("{}", { status: 200, statusText: "OK" })); + }); + + const config = this.createSenderConfig(TransportType.Fetch); + const { sender } = this.initializeCoreAndSender(config, "000e0000-e000-0000-a000-000000000000"); + + const telemetryItem: ITelemetryItem = { + name: "fake item", + iKey: "testIkey2;ingestionendpoint=testUrl1", + baseType: "some type", + baseData: {} + }; + + this.processTelemetryAndFlush(sender, telemetryItem); + + } + ].concat(PollingAssert.createPollingAssert(() => { + if (this.statsbeatCountSpy.called && this.fetchStub.called) { + this.assertStatsbeatCall(200, "Request_Success_Count"); + return true; + } + return false; + }, "Waiting for fetch sender and Statsbeat count to be called") as any) + }); + + this.testCaseAsync({ + name: "Statsbeat increments throttle count when fetch sender is called with status 439", + useFakeTimers: true, + stepDelay: 100, + steps: [ + () => { + this.fetchStub = this.sandbox.stub(window, "fetch").callsFake(() => { + return Promise.resolve(new Response("{}", { status: 439, statusText: "Too Many Requests" })); + }); + + const config = this.createSenderConfig(TransportType.Fetch); + const { sender } = this.initializeCoreAndSender(config, "000e0000-e000-0000-a000-000000000000"); + + const telemetryItem: ITelemetryItem = { + name: "fake item", + iKey: "testIkey2;ingestionendpoint=testUrl1", + baseType: "some type", + baseData: {} + }; + + this.processTelemetryAndFlush(sender, telemetryItem); + } + ].concat(PollingAssert.createPollingAssert(() => { + if (this.statsbeatCountSpy.called && this.fetchStub.called) { + this.assertStatsbeatCall(439, "Throttle_Count"); + return true; + } + return false; + }, "Waiting for fetch sender and Statsbeat count to be called") as any) + }); + + this.testCaseAsync({ + name: "Statsbeat increments success count for beacon sender", + useFakeTimers: true, + stepDelay: 100, + steps: [ + () => { + const config = this.createSenderConfig(TransportType.Beacon); + const { sender } = this.initializeCoreAndSender(config, "000e0000-e000-0000-a000-000000000000"); + + const telemetryItem: ITelemetryItem = { + name: "fake item", + iKey: "testIkey2;ingestionendpoint=testUrl1", + baseType: "some type", + baseData: {} + }; + let sendBeaconCalled = false; + this.hookSendBeacon((url: string) => { + sendBeaconCalled = true; + return true; + }); + QUnit.assert.ok(isBeaconApiSupported(), "Beacon API is supported"); + this.processTelemetryAndFlush(sender, telemetryItem); + } + ].concat(PollingAssert.createPollingAssert(() => { + if (this.statsbeatCountSpy.called) { + this.assertStatsbeatCall(200, "Request_Success_Count"); + return true; + } + return false; + }, "Waiting for beacon sender and Statsbeat count to be called") as any) + }); + + + this.testCaseAsync({ + name: "Statsbeat increments success count for xhr sender", + useFakeTimers: true, + useFakeServer: true, + stepDelay: 100, + fakeServerAutoRespond: true, + steps: [ + () => { + let window = getWindow(); + let fakeXMLHttpRequest = (window as any).XMLHttpRequest; // why we do this? + let config = this.createSenderConfig(TransportType.Xhr) && {disableSendBeaconSplit: true}; + const { sender } = this.initializeCoreAndSender(config, "000e0000-e000-0000-a000-000000000000"); + console.log("xhr sender called", this._getXhrRequests().length); + + const telemetryItem: ITelemetryItem = { + name: "fake item", + iKey: "testIkey2;ingestionendpoint=testUrl1", + baseType: "some type", + baseData: {} + }; + this.processTelemetryAndFlush(sender, telemetryItem); + QUnit.assert.equal(1, this._getXhrRequests().length, "xhr sender is called"); + console.log("xhr sender is called", this._getXhrRequests().length); + (window as any).XMLHttpRequest = fakeXMLHttpRequest; + + } + ].concat(PollingAssert.createPollingAssert(() => { + if (this.statsbeatCountSpy.called) { + this.assertStatsbeatCall(200, "Request_Success_Count"); + console.log("Statsbeat count called with success count for xhr sender"); + return true; + } + return false; + }, "Waiting for xhr sender and Statsbeat count to be called", 60, 1000) as any) + }); +} +} \ No newline at end of file diff --git a/channels/applicationinsights-channel-js/Tests/Unit/src/aichannel.tests.ts b/channels/applicationinsights-channel-js/Tests/Unit/src/aichannel.tests.ts index b4dd43ee5..97061376c 100644 --- a/channels/applicationinsights-channel-js/Tests/Unit/src/aichannel.tests.ts +++ b/channels/applicationinsights-channel-js/Tests/Unit/src/aichannel.tests.ts @@ -1,9 +1,11 @@ import { SenderTests } from "./Sender.tests"; import { SampleTests } from "./Sample.tests"; import { GlobalTestHooks } from "./GlobalTestHooks.Test"; +import { StatsbeatTests } from "./StatsBeat.tests"; export function runTests() { new GlobalTestHooks().registerTests(); new SenderTests().registerTests(); new SampleTests().registerTests(); + // new StatsbeatTests().registerTests(); } \ No newline at end of file diff --git a/channels/applicationinsights-channel-js/src/SendBuffer.ts b/channels/applicationinsights-channel-js/src/SendBuffer.ts index 91ea6d401..239425269 100644 --- a/channels/applicationinsights-channel-js/src/SendBuffer.ts +++ b/channels/applicationinsights-channel-js/src/SendBuffer.ts @@ -103,6 +103,8 @@ abstract class BaseSendBuffer { if (!isNullOrUndefined(_maxRetryCnt)) { if (payload.cnt > _maxRetryCnt) { // TODO: add log here on dropping payloads + // will log statsbeat exception later here + return; } } diff --git a/channels/applicationinsights-channel-js/src/Sender.ts b/channels/applicationinsights-channel-js/src/Sender.ts index 9e76c676d..5a06d257c 100644 --- a/channels/applicationinsights-channel-js/src/Sender.ts +++ b/channels/applicationinsights-channel-js/src/Sender.ts @@ -7,20 +7,20 @@ import { import { ActiveStatus, BaseTelemetryPlugin, IAppInsightsCore, IBackendResponse, IChannelControls, IConfigDefaults, IConfiguration, IDiagnosticLogger, IInternalOfflineSupport, INotificationManager, IPayloadData, IPlugin, IProcessTelemetryContext, - IProcessTelemetryUnloadContext, ITelemetryItem, ITelemetryPluginChain, ITelemetryUnloadState, IXDomainRequest, IXHROverride, - OnCompleteCallback, SendPOSTFunction, SendRequestReason, SenderPostManager, TransportType, _ISendPostMgrConfig, _ISenderOnComplete, - _eInternalMessageId, _throwInternal, _warnToConsole, arrForEach, cfgDfBoolean, cfgDfValidate, createProcessTelemetryContext, - createUniqueNamespace, dateNow, dumpObj, eLoggingSeverity, formatErrorMessageXdr, formatErrorMessageXhr, getExceptionName, getIEVersion, - isArray, isBeaconsSupported, isFetchSupported, isNullOrUndefined, mergeEvtNamespace, objExtend, onConfigChange, parseResponse, - prependTransports, runTargetUnload + IProcessTelemetryUnloadContext, IStatsBeatConfig, IStatsBeatEvent, ITelemetryItem, ITelemetryPluginChain, ITelemetryUnloadState, + IXDomainRequest, IXHROverride, OnCompleteCallback, SendPOSTFunction, SendRequestReason, SenderPostManager, TransportType, + _ISendPostMgrConfig, _ISenderOnComplete, _eInternalMessageId, _throwInternal, _warnToConsole, arrForEach, cfgDfBoolean, cfgDfValidate, + createProcessTelemetryContext, createUniqueNamespace, dateNow, dumpObj, eLoggingSeverity, formatErrorMessageXdr, formatErrorMessageXhr, + getExceptionName, getIEVersion, isArray, isBeaconsSupported, isFetchSupported, isNullOrUndefined, mergeEvtNamespace, objExtend, + onConfigChange, parseResponse, prependTransports, runTargetUnload } from "@microsoft/applicationinsights-core-js"; import { IPromise } from "@nevware21/ts-async"; import { ITimerHandler, isNumber, isPromiseLike, isString, isTruthy, mathFloor, mathMax, mathMin, objDeepFreeze, objDefine, scheduleTimeout } from "@nevware21/ts-utils"; import { - DependencyEnvelopeCreator, EventEnvelopeCreator, ExceptionEnvelopeCreator, MetricEnvelopeCreator, PageViewEnvelopeCreator, - PageViewPerformanceEnvelopeCreator, TraceEnvelopeCreator + DependencyEnvelopeCreator, EnvelopeCreator, EventEnvelopeCreator, ExceptionEnvelopeCreator, MetricEnvelopeCreator, + PageViewEnvelopeCreator, PageViewPerformanceEnvelopeCreator, TraceEnvelopeCreator } from "./EnvelopeCreator"; import { IInternalStorageItem, ISenderConfig } from "./Interfaces"; import { ArraySendBuffer, ISendBuffer, SessionStorageSendBuffer } from "./SendBuffer"; @@ -35,6 +35,7 @@ const FetchSyncRequestSizeLimitBytes = 65000; // approx 64kb (the current Edge, interface IInternalPayloadData extends IPayloadData { oriPayload: IInternalStorageItem[]; retryCnt?: number; + statsBeatData?: IStatsBeatEvent; } @@ -261,7 +262,6 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { let diagLog = _self.diagLog(); _evtNamespace = mergeEvtNamespace(createUniqueNamespace("Sender"), core.evtNamespace && core.evtNamespace()); _offlineListener = createOfflineListener(_evtNamespace); - // This function will be re-called whenever any referenced configuration is changed _self._addHook(onConfigChange(config, (details) => { let config = details.cfg; @@ -272,6 +272,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { // getExtCfg only finds undefined values from core let senderConfig = ctx.getExtCfg(identifier, defaultAppInsightsChannelConfig); let curExtUrl = senderConfig.endpointUrl; + // if it is not inital change (_endpointUrl has value) // if current sender endpoint url is not changed directly // means ExtCfg is not changed directly @@ -526,7 +527,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { if (_isStringArr(payload)) { return; } - return _xhrReadyStateChange(xhr, payload as IInternalStorageItem[],countOfItemsInPayload); + return _xhrReadyStateChange(xhr, payload as IInternalStorageItem[], countOfItemsInPayload); } @@ -574,6 +575,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { "Telemetry transmission failed, some telemetry will be lost: " + getExceptionName(e), { exception: dumpObj(e) }); } + // potential place to call countException q3 } } @@ -648,6 +650,15 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { } + function _getStatsBeat() { + let statsBeatConfig = { + ikey: _self._senderConfig.instrumentationKey, + endpoint: _endpointUrl, + version: EnvelopeCreator.Version + } as IStatsBeatConfig; + return _self.core.getStatsBeat(statsBeatConfig); + } + function _xdrOnLoad (xdr: IXDomainRequest, payload: IInternalStorageItem[]) { const responseText = _getResponseText(xdr); if (xdr && (responseText + "" === "200" || responseText === "")) { @@ -670,29 +681,60 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { try { let onCompleteFuncs = { xdrOnComplete: (xdr: IXDomainRequest, oncomplete: OnCompleteCallback,payload?: IPayloadData) => { - let data = _getPayloadArr(payload); - if (!data) { + let payloadArr = _getPayloadArr(payload); + if (!payloadArr) { return; } - return _xdrOnLoad(xdr, data); + const responseText = _getResponseText(xdr); + let statsbeat = _getStatsBeat(); + if (statsbeat) { + if (xdr && (responseText + "" === "200" || responseText === "")) { + _consecutiveErrors = 0; + statsbeat.count(200, payload, _endpointUrl); + } else { + const results = parseResponse(responseText); + + if (results && results.itemsReceived && results.itemsReceived > results.itemsAccepted + && !_isRetryDisabled) { + statsbeat.count(206, payload, _endpointUrl); + } else { + statsbeat.count(499, payload, _endpointUrl); + } + } + } + + + + return _xdrOnLoad(xdr, payloadArr); }, fetchOnComplete: (response: Response, onComplete: OnCompleteCallback, resValue?: string, payload?: IPayloadData) => { - let data = _getPayloadArr(payload); - if (!data) { + let payloadArr = _getPayloadArr(payload); + if (!payloadArr) { return; } - return _checkResponsStatus(response.status, data, response.url, data.length, response.statusText, resValue || ""); + let statsbeat = _getStatsBeat(); + if (statsbeat) { + statsbeat.count(response.status, payload, _endpointUrl); + } + return _checkResponsStatus(response.status, payloadArr, response.url, payloadArr.length, response.statusText, resValue || ""); }, xhrOnComplete: (request: XMLHttpRequest, oncomplete: OnCompleteCallback, payload?: IPayloadData) => { - let data = _getPayloadArr(payload); - if (!data) { + let payloadArr = _getPayloadArr(payload); + if (!payloadArr) { return; } - return _xhrReadyStateChange(request, data, data.length); - + let statsbeat = _getStatsBeat(); + if (statsbeat && request.readyState === 4) { + statsbeat.count(request.status, payload, _endpointUrl); + } + return _xhrReadyStateChange(request, payloadArr, payloadArr.length); }, beaconOnRetry: (data: IPayloadData, onComplete: OnCompleteCallback, canSend: (payload: IPayloadData, oncomplete: OnCompleteCallback, sync?: boolean) => boolean) => { + let statsbeat = _getStatsBeat(); + if (statsbeat) { + statsbeat.count(499, data, _endpointUrl); + } return _onBeaconRetry(data, onComplete, canSend); } @@ -792,7 +834,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { let internalPayload = payload as IInternalPayloadData; let arr = internalPayload.oriPayload; if (arr && arr.length) { - return arr + return arr; } return null; } @@ -936,9 +978,16 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { function _doSend(sendInterface: IXHROverride, payload: IInternalStorageItem[], isAsync: boolean, markAsSent: boolean = true): void | IPromise { let onComplete = (status: number, headers: {[headerName: string]: string;}, response?: string) => { + let statsbeat = _getStatsBeat(); + if (statsbeat) { + statsbeat.count(status, payloadData, _endpointUrl); + } return _getOnComplete(payload, status, headers, response); } let payloadData = _getPayload(payload); + if (payloadData) { + payloadData.statsBeatData = {startTime: dateNow()}; + } let sendPostFunc: SendPOSTFunction = sendInterface && sendInterface.sendPOST; if (sendPostFunc && payloadData) { // *********************************************************************************************** @@ -1039,7 +1088,6 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { _self._onError(payload, errorMessage); } } else { - // check if the xhr's responseURL or fetch's response.url is same as endpoint url // TODO after 10 redirects force send telemetry with 'redirect=false' as query parameter. _checkAndUpdateEndPointUrl(responseUrl); diff --git a/extensions/applicationinsights-cfgsync-js/Tests/Unit/src/cfgsynchelper.tests.ts b/extensions/applicationinsights-cfgsync-js/Tests/Unit/src/cfgsynchelper.tests.ts index 5aafcea69..0884aa9a4 100644 --- a/extensions/applicationinsights-cfgsync-js/Tests/Unit/src/cfgsynchelper.tests.ts +++ b/extensions/applicationinsights-cfgsync-js/Tests/Unit/src/cfgsynchelper.tests.ts @@ -1,9 +1,8 @@ import { AITestClass, Assert } from "@microsoft/ai-test-framework"; import { NonOverrideCfg } from "../../../src/Interfaces/ICfgSyncConfig"; -import { AppInsightsCore, CdnFeatureMode, FeatureOptInMode, IAppInsightsCore, IConfiguration, IFeatureOptIn, IFeatureOptInDetails, INotificationManager, IPlugin, ITelemetryItem, PerfManager } from "@microsoft/applicationinsights-core-js"; +import { ICookieMgrConfig, AppInsightsCore, CdnFeatureMode, FeatureOptInMode, IAppInsightsCore, IConfiguration, IFeatureOptIn, IFeatureOptInDetails, INotificationManager, IPlugin, ITelemetryItem, PerfManager } from "@microsoft/applicationinsights-core-js"; import { IConfig, IStorageBuffer } from "@microsoft/applicationinsights-common"; import { resolveCdnFeatureCfg, replaceByNonOverrideCfg, applyCdnfeatureCfg } from "../../../src/CfgSyncHelperFuncs"; -import { ICookieMgrConfig } from "@microsoft/applicationinsights-core-js/src/applicationinsights-core-js"; import { ICfgSyncCdnConfig } from "../../../src/Interfaces/ICfgSyncCdnConfig"; export class CfgSyncHelperTests extends AITestClass { @@ -102,7 +101,10 @@ export class CfgSyncHelperTests extends AITestClass { extensions:[{isFlushInvoked:false,isTearDownInvoked:false,isResumeInvoked:false,isPauseInvoked:false,identifier:"Sender",priority:1001}], channels:[], extensionConfig:{}, - enableDebug: false + _sdk: { + stats: false + }, + enableDebug: false, } let core = new AppInsightsCore(); diff --git a/shared/AppInsightsCore/Tests/Unit/src/StatsBeat.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/StatsBeat.Tests.ts new file mode 100644 index 000000000..dafedc25f --- /dev/null +++ b/shared/AppInsightsCore/Tests/Unit/src/StatsBeat.Tests.ts @@ -0,0 +1,280 @@ +import { Assert, AITestClass } from "@microsoft/ai-test-framework"; +import { AppInsightsCore, IAppInsightsCore, IConfiguration, IPlugin, ITelemetryItem } from "../../../src/applicationinsights-core-js"; +import { Statsbeat } from "../../../src/JavaScriptSDK/StatsBeat"; +import { IPayloadData } from "../../../src/JavaScriptSDK.Interfaces/IXHROverride"; +import * as sinon from "sinon"; +const STATS_COLLECTION_SHORT_INTERVAL: number = 900000; // 15 minutes + +export class StatsBeatTests extends AITestClass { + private _core: AppInsightsCore; + private _config: IConfiguration; + private _statsbeat: Statsbeat; + private _trackSpy: sinon.SinonSpy; + + constructor(emulateIe: boolean) { + super("StatsBeatTests", emulateIe); + } + + public testInitialize() { + let _self = this; + super.testInitialize(); + + _self._config = { + instrumentationKey: "Test-iKey", + disableInstrumentationKeyValidation: true, + _sdk: { + stats: true // Enable statsbeat by default + } + }; + + _self._core = new AppInsightsCore(); + _self._statsbeat = new Statsbeat(); + + // Create spy for tracking telemetry + _self._trackSpy = this.sandbox.spy(_self._core, "track"); + } + + public testCleanup() { + super.testCleanup(); + this._core = null; + this._statsbeat = null; + } + + public registerTests() { + + this.testCase({ + name: "StatsBeat: Initialization", + test: () => { + // Test with no initialization + Assert.equal(false, this._statsbeat.isInitialized(), "StatsBeat should not be initialized by default"); + + // Initialize and test + this._statsbeat.initialize(this._core, { + ikey: "Test-iKey", + endpoint: "https://example.endpoint.com", + version: "1.0.0" + }); + Assert.equal(true, this._statsbeat.isInitialized(), "StatsBeat should be initialized after initialization"); + } + }); + + this.testCase({ + name: "StatsBeat: count method tracks request metrics", + useFakeTimers: true, + test: () => { + // Initialize StatsBeat + this._statsbeat.initialize(this._core, { + ikey: "Test-iKey", + endpoint: "https://example.endpoint.com", + version: "1.0.0" + }); + + // Create mock payload data with timing information + const payloadData = { + urlString: "https://example.endpoint.com", + data: "testData", + headers: {}, + timeout: 0, + disableXhrSync: false, + statsBeatData: { + startTime: "2023-10-01T00:00:00Z" // Simulated start time + } + } as IPayloadData; + + // Test successful request + this._statsbeat.count(200, payloadData, "https://example.endpoint.com"); + + // Test failed request + this._statsbeat.count(500, payloadData, "https://example.endpoint.com"); + + // Test throttled request + this._statsbeat.count(429, payloadData, "https://example.endpoint.com"); + + // Verify that trackStatsbeats is called when the timer fires + this.clock.tick(STATS_COLLECTION_SHORT_INTERVAL + 1); + + // Verify that track was called + Assert.ok(this._trackSpy.called, "track should be called when statsbeat timer fires"); + + // When the timer fires, multiple metrics should be sent + Assert.ok(this._trackSpy.callCount >= 3, "Multiple metrics should be tracked"); + } + }); + + this.testCase({ + name: "StatsBeat: countException method tracks exceptions", + useFakeTimers: true, + test: () => { + // Initialize StatsBeat + this._statsbeat.initialize(this._core, { + ikey: "Test-iKey", + endpoint: "https://example.endpoint.com", + version: "1.0.0" + }); + + // Count an exception + this._statsbeat.countException("https://example.endpoint.com", "NetworkError"); + + // Verify that trackStatsbeats is called when the timer fires + this.clock.tick(STATS_COLLECTION_SHORT_INTERVAL + 1); + + // Verify that track was called + Assert.ok(this._trackSpy.called, "track should be called when statsbeat timer fires"); + + // Check that exception metrics are tracked + let foundExceptionMetric = false; + for (let i = 0; i < this._trackSpy.callCount; i++) { + const call = this._trackSpy.getCall(i); + const item: ITelemetryItem = call.args[0]; + if (item.baseData && + item.baseData.properties && + item.baseData.properties.exceptionType === "NetworkError") { + foundExceptionMetric = true; + break; + } + } + + Assert.ok(foundExceptionMetric, "Exception metrics should be tracked"); + } + }); + + this.testCase({ + name: "StatsBeat: does not send metrics for different endpoints", + useFakeTimers: true, + test: () => { + // Initialize StatsBeat for a specific endpoint + this._statsbeat.initialize(this._core, { + ikey: "Test-iKey", + endpoint: "https://example.endpoint.com", + version: "1.0.0" + }); + + // Create mock payload data + const payloadData = { + urlString: "https://example.endpoint.com", + data: "testData", + headers: {}, + timeout: 0, + disableXhrSync: false, + statsBeatData: { + startTime: Date.now() + } + } as IPayloadData; + + // Set up spies to check internal calls + const countSpy = this.sandbox.spy(this._statsbeat, "count"); + + // Count metrics for a different endpoint + this._statsbeat.count(200, payloadData, "https://different.endpoint.com"); + + // Verify that trackStatsbeats is called when the timer fires + this.clock.tick(STATS_COLLECTION_SHORT_INTERVAL + 1); + // The count method was called, but it should return early + Assert.equal(1, countSpy.callCount, "count method should be called"); + Assert.equal(0, this._trackSpy.callCount, "track should not be called for different endpoint"); + } + }); + + this.testCase({ + name: "StatsBeat: test dynamic configuration changes", + useFakeTimers: true, + test: () => { + // Setup core with statsbeat enabled + this._core.initialize(this._config, [new ChannelPlugin()]); + + // Verify that statsbeat is created + const statsbeat = this._core.getStatsBeat(); + Assert.ok(statsbeat, "Statsbeat should be created"); + + this._core.config._sdk.stats = false; + this.clock.tick(1); // Allow time for config changes to propagate + + // Verify that statsbeat is removed + const updatedStatsbeat = this._core.getStatsBeat(); + Assert.ok(!updatedStatsbeat, "Statsbeat should be removed when disabled"); + + // Re-enable statsbeat + this._core.config._sdk.stats = true; + this.clock.tick(1); // Allow time for config changes to propagate + + // Verify that statsbeat is created again + const reenabledStatsbeat = this._core.getStatsBeat(); + Assert.ok(reenabledStatsbeat, "Statsbeat should be recreated when re-enabled"); + + // Test that statsbeat is not created when disabled with undefined + this._core.config._sdk.stats = undefined; + this.clock.tick(1); // Allow time for config changes to propagate + + // Verify that statsbeat is removed + Assert.ok(!this._core.getStatsBeat(), "Statsbeat should be removed when disabled"); + + // Re-enable statsbeat + this._core.config._sdk.stats = true; + this.clock.tick(1); // Allow time for config changes to propagate + + // Verify that statsbeat is created again + Assert.ok( this._core.getStatsBeat(), "Statsbeat should be recreated when re-enabled"); + + // Test that statsbeat is not created when disabled with null value + this._core.config._sdk.stats = null; + this.clock.tick(1); // Allow time for config changes to propagate + + // Verify that statsbeat is removed + Assert.ok(!this._core.getStatsBeat(), "Statsbeat should be removed when disabled"); + } + }); + } +} + +class ChannelPlugin implements IPlugin { + public isFlushInvoked = false; + public isTearDownInvoked = false; + public isResumeInvoked = false; + public isPauseInvoked = false; + + public identifier = "Sender"; + public priority: number = 1001; + + constructor() { + this.processTelemetry = this._processTelemetry.bind(this); + } + + public pause(): void { + this.isPauseInvoked = true; + } + + public resume(): void { + this.isResumeInvoked = true; + } + + public teardown(): void { + this.isTearDownInvoked = true; + } + + flush(async?: boolean, callBack?: () => void): void { + this.isFlushInvoked = true; + if (callBack) { + callBack(); + } + } + + public processTelemetry(env: ITelemetryItem) {} + + setNextPlugin(next: any) { + // no next setup + } + + public initialize = (config: IConfiguration, core: IAppInsightsCore, plugin: IPlugin[]) => { + } + + private _processTelemetry(env: ITelemetryItem) { + } +} + +class CustomTestError extends Error { + constructor(message = "") { + super(message); + this.name = "CustomTestError"; + this.message = message + " -- test error."; + } +} \ No newline at end of file diff --git a/shared/AppInsightsCore/Tests/Unit/src/aiunittests.ts b/shared/AppInsightsCore/Tests/Unit/src/aiunittests.ts index 6fc31aebc..8a1f8bbdb 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/aiunittests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/aiunittests.ts @@ -11,6 +11,7 @@ import { UpdateConfigTests } from "./UpdateConfig.Tests"; import { EventsDiscardedReasonTests } from "./EventsDiscardedReason.Tests"; import { W3cTraceParentTests } from "./W3cTraceParentTests"; import { DynamicConfigTests } from "./DynamicConfig.Tests"; +import { StatsBeatTests } from './StatsBeat.Tests'; export function runTests() { new GlobalTestHooks().registerTests(); @@ -26,4 +27,6 @@ export function runTests() { new UpdateConfigTests().registerTests(); new EventsDiscardedReasonTests().registerTests(); new W3cTraceParentTests().registerTests(); + // new StatsBeatTests(false).registerTests(); + // new StatsBeatTests(true).registerTests(); } diff --git a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IAppInsightsCore.ts b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IAppInsightsCore.ts index fbb62ba7c..80893b388 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IAppInsightsCore.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IAppInsightsCore.ts @@ -16,6 +16,7 @@ import { INotificationListener } from "./INotificationListener"; import { INotificationManager } from "./INotificationManager"; import { IPerfManagerProvider } from "./IPerfManager"; import { IProcessTelemetryContext } from "./IProcessTelemetryContext"; +import { IStatsBeat, IStatsBeatConfig } from "./IStatsBeat"; import { ITelemetryInitializerHandler, TelemetryInitializerFunction } from "./ITelemetryInitializers"; import { ITelemetryItem } from "./ITelemetryItem"; import { IPlugin, ITelemetryPlugin } from "./ITelemetryPlugin"; @@ -124,6 +125,8 @@ export interface IAppInsightsCore = objDeepFreeze({ [STR_EXTENSION_CONFIG]: { ref: true, v: {} }, [STR_CREATE_PERF_MGR]: UNDEFINED_VALUE, loggingLevelConsole: eLoggingSeverity.DISABLED, - diagnosticLogInterval: UNDEFINED_VALUE + diagnosticLogInterval: UNDEFINED_VALUE, + _sdk: cfgDfMerge({ + stats: false + }) }); /** @@ -275,6 +281,7 @@ export class AppInsightsCore im let _logger: IDiagnosticLogger; let _eventQueue: ITelemetryItem[]; let _notificationManager: INotificationManager | null | undefined; + let _statsBeat: IStatsBeat | null; let _perfManager: IPerfManager | null; let _cfgPerfManager: IPerfManager | null; let _cookieManager: ICookieMgr | null; @@ -351,6 +358,13 @@ export class AppInsightsCore im _initInMemoMaxSize = rootCfg.initInMemoMaxSize || maxInitQueueSize; + // uncomment this until throttle is implemented + // if (config._sdk.stats === true){ + // _statsBeat = _statsBeat || new Statsbeat(); + // } else { + // _statsBeat = null; + // } + _handleIKeyEndpointPromises(rootCfg); // Mark the extensionConfig and all first level keys as referenced @@ -500,6 +514,18 @@ export class AppInsightsCore im return _perfManager || _cfgPerfManager || getGblPerfMgr(); }; + _self.getStatsBeat = (statsBeatConfig?: IStatsBeatConfig): IStatsBeat => { + // create a new statsbeat if not initialize yet or the endpoint is different + // otherwise, return the existing one, or null + + // uncomment this until throttle is implemented + // if (statsBeatConfig && this.config._sdk.stats === true && _statsBeat && _statsBeat.getEndpoint() !== statsBeatConfig.endpoint) { + // _statsBeat = new Statsbeat(); + // _statsBeat.initialize(this, statsBeatConfig); + // } + return _statsBeat; + }; + _self.setPerfMgr = (perfMgr: IPerfManager) => { _perfManager = perfMgr; }; @@ -1018,6 +1044,7 @@ export class AppInsightsCore im runTargetUnload(_notificationManager, false); _notificationManager = null; _perfManager = null; + _statsBeat = null; _cfgPerfManager = null; runTargetUnload(_cookieManager, false); _cookieManager = null; @@ -1430,6 +1457,11 @@ export class AppInsightsCore im return null; } + public getStatsBeat(statsBeatConfig?: IStatsBeatConfig): IStatsBeat { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + return null; + } + public setPerfMgr(perfMgr: IPerfManager) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/NetworkStatsbeat.ts b/shared/AppInsightsCore/src/JavaScriptSDK/NetworkStatsbeat.ts new file mode 100644 index 000000000..a7dcdc835 --- /dev/null +++ b/shared/AppInsightsCore/src/JavaScriptSDK/NetworkStatsbeat.ts @@ -0,0 +1,22 @@ +export interface NetworkStatsbeat { + host: string; + totalRequest: number; + success: number; + throttle: Record; + failure: Record; + retry: Record; + exception: Record; + requestDuration: number; +} +export function createNetworkStatsbeat(host: string): NetworkStatsbeat { + return { + host, + totalRequest: 0, + success: 0, + throttle: {}, + failure: {}, + retry: {}, + exception: {}, + requestDuration: 0 + }; +} diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts b/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts index f27d1da09..40d1d5be9 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts @@ -249,7 +249,7 @@ export class SenderPostManager { } else { // if can send - _onSuccess(STR_EMPTY, oncomplete); + _onSuccess(STR_EMPTY, oncomplete); // if success, onComplete is called with status code 200 } } @@ -429,12 +429,14 @@ export class SenderPostManager { } - function _handleError(res?: string) { + function _handleError(res?: string, statusCode?: number) { // In case there is an error in the request. Set the status to 0 for 1ds and 400 for appInsights // so that the events can be retried later. - - _doOnComplete(oncomplete, _isOneDs? 0 : 400, {}, _isOneDs? STR_EMPTY: res); - + if (statusCode) { + _doOnComplete(oncomplete, _isOneDs? 0 : statusCode, {}, _isOneDs? STR_EMPTY: res); + } else { + _doOnComplete(oncomplete, _isOneDs? 0 : 400, {}, _isOneDs? STR_EMPTY: res); + } } function _onFetchComplete(response: Response, payload?: IPayloadData, value?: string) { @@ -468,7 +470,11 @@ export class SenderPostManager { */ if (!_isOneDs && !response.ok) { // this is for appInsights only - _handleError(response.statusText); + if (response.status){ + _handleError(response.statusText, response.status); + } else { + _handleError(response.statusText, 499); + } resolveFunc && resolveFunc(false); } else { if (_isOneDs && !response.body) { @@ -484,19 +490,23 @@ export class SenderPostManager { } } catch (e) { - _handleError(dumpObj(e)); + if (response && response.status){ + _handleError(dumpObj(e), response.status); + } else { + _handleError(dumpObj(e), 499); + } rejectFunc && rejectFunc(e); } } else { - _handleError(result.reason && result.reason.message); + _handleError(result.reason && result.reason.message, 499); rejectFunc && rejectFunc(result.reason); } } }); } catch (e) { if (!responseHandled) { - _handleError(dumpObj(e)); + _handleError(dumpObj(e), 499); rejectFunc && rejectFunc(e); } } diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/StatsBeat.ts b/shared/AppInsightsCore/src/JavaScriptSDK/StatsBeat.ts new file mode 100644 index 000000000..97ffd8d9c --- /dev/null +++ b/shared/AppInsightsCore/src/JavaScriptSDK/StatsBeat.ts @@ -0,0 +1,203 @@ +import dynamicProto from "@microsoft/dynamicproto-js"; +import { ITimerHandler, scheduleTimeout, utcNow } from "@nevware21/ts-utils"; +import { IAppInsightsCore } from "../JavaScriptSDK.Interfaces/IAppInsightsCore"; +import { IStatsBeat, IStatsBeatConfig } from "../JavaScriptSDK.Interfaces/IStatsBeat"; +import { ITelemetryItem } from "../JavaScriptSDK.Interfaces/ITelemetryItem"; +import { IPayloadData } from "../JavaScriptSDK.Interfaces/IXHROverride"; +import { NetworkStatsbeat, createNetworkStatsbeat } from "./NetworkStatsbeat"; + +const INSTRUMENTATION_KEY = "c4a29126-a7cb-47e5-b348-11414998b11e"; +const STATS_COLLECTION_SHORT_INTERVAL: number = 900000; // 15 minutes +const STATSBEAT_LANGUAGE = "JavaScript"; +const STATSBEAT_TYPE = "Browser"; + +export class Statsbeat implements IStatsBeat { + constructor() { + let _networkCounter: NetworkStatsbeat; + let _isEnabled: boolean = false; + let _core: IAppInsightsCore; + let _timeoutHandle: ITimerHandler; // Handle to the timer for sending telemetry. This way, we would not send telemetry when system sleep. + // Custom dimensions + let _cikey: string; + let _language: string; + let _sdkVersion: string; + let _os: string; + dynamicProto(Statsbeat, this, (_self, _base) => { + _self.initialize = (core: IAppInsightsCore, statsBeatConfig: IStatsBeatConfig) => { + _core = core; + _networkCounter = createNetworkStatsbeat(statsBeatConfig.endpoint); + _isEnabled = true; + _sdkVersion = statsBeatConfig.version; + _getCustomProperties(statsBeatConfig.ikey); + } + + _self.isInitialized = (): boolean => { + return !!_isEnabled; + } + + _self.count = (status: number, payloadData: IPayloadData, endpoint: string) => { + if (!_isEnabled || !_checkEndpoint(endpoint)) { + return; + } + if (payloadData && payloadData["statsBeatData"] && payloadData["statsBeatData"]["startTime"]) { + _networkCounter.totalRequest = (_networkCounter.totalRequest || 0) + 1; + _networkCounter.requestDuration += utcNow() - payloadData["statsBeatData"]["startTime"]; + } + let retryArray = [401, 403, 408, 429, 500, 502, 503, 504]; + let throttleArray = [402, 439]; + if (status >= 200 && status < 300) { + _networkCounter.success++; + } else if (retryArray.indexOf(status) !== -1) { + _networkCounter.retry[status] = (_networkCounter.retry[status] || 0) + 1; + } else if (throttleArray.indexOf(status) !== -1) { + _networkCounter.throttle[status] = (_networkCounter.throttle[status] || 0) + 1; + } else if (status !== 307 && status !== 308) { + _networkCounter.failure[status] = (_networkCounter.failure[status] || 0) + 1; + } + _setupTimer(); + }; + + _self.getEndpoint = (): string => { + return _networkCounter?_networkCounter.host:null; + } + + _self.countException = (endpoint: string, exceptionType: string) => { + if (!_isEnabled || !_checkEndpoint(endpoint)) { + return; + } + _networkCounter.exception[exceptionType] = (_networkCounter.exception[exceptionType] || 0) + 1; + _setupTimer(); + } + + function _setupTimer() { + if (!_timeoutHandle) { + _timeoutHandle = scheduleTimeout(() => { + _timeoutHandle = null; + trackStatsbeats(); + }, STATS_COLLECTION_SHORT_INTERVAL); + } + } + + function trackStatsbeats(){ + _trackSendRequestDuration(); + _trackSendRequestsCount(); + _networkCounter = createNetworkStatsbeat(_networkCounter.host); + _timeoutHandle && _timeoutHandle.cancel(); + _timeoutHandle = null; + } + + function _checkEndpoint(endpoint: string) { + return _networkCounter && _networkCounter.host === endpoint; + } + + function _getCustomProperties(ikey: string) { + _cikey = ikey; + _language = STATSBEAT_LANGUAGE; + _os = STATSBEAT_TYPE; + } + + function _sendStatsbeats(name: string, val: number, properties?: { [name: string]: any }) { + if (!val || val <= 0){ + return; + } + // Add extra properties + let baseProperties = { + "rp": "unknown", + "attach": "Manual", + "cikey": _cikey, + "os": _os, + "language": _language, + "version": _sdkVersion, + "endpoint": "breeze", + "host": _networkCounter.host + } as { [key: string]: any }; + + // Manually merge properties instead of using spread syntax + let combinedProps: { [key: string]: any } = { "host": _networkCounter.host }; + + // Add properties if present + if (properties) { + for (let key in properties) { + if (properties.hasOwnProperty(key)) { + combinedProps[key] = properties[key]; + } + } + } + // Add base properties + for (let key in baseProperties) { + if (baseProperties.hasOwnProperty(key)) { + combinedProps[key] = baseProperties[key]; + } + } + let statsbeatEvent: ITelemetryItem = { + iKey: INSTRUMENTATION_KEY, + name: name, + baseData: { + name: name, + average: val, + properties: combinedProps + }, + baseType: "MetricData" + }; + _core.track(statsbeatEvent); + } + + function _trackSendRequestDuration() { + var totalRequest = _networkCounter.totalRequest; + + if (_networkCounter.totalRequest > 0 ) { + let averageRequestExecutionTime = _networkCounter.requestDuration / totalRequest; + _sendStatsbeats("Request_Duration", averageRequestExecutionTime); + } + } + + function _trackSendRequestsCount() { + var currentCounter = _networkCounter; + _sendStatsbeats("Request_Success_Count", currentCounter.success); + + for (const code in currentCounter.failure) { + const count = currentCounter.failure[code]; + _sendStatsbeats("failure", count, { statusCode: code }); + } + + for (const code in currentCounter.retry) { + const count = currentCounter.retry[code]; + _sendStatsbeats("retry", count, { statusCode: code }); + } + + for (const code in currentCounter.exception) { + const count = currentCounter.exception[code]; + _sendStatsbeats("exception", count, { exceptionType: code }); + } + + for (const code in currentCounter.throttle) { + const count = currentCounter.throttle[code]; + _sendStatsbeats("Throttle_Count", count, { statusCode: code }); + } + } + }); + } + + public initialize(core: IAppInsightsCore, statsBeatConfig: IStatsBeatConfig) { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } + + public isInitialized(): boolean { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + return false; + } + + public count(status: number, payloadData: IPayloadData, endpoint: string) { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } + + public countException(endpoint: string, exceptionType: string) { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } + + public getEndpoint(): string { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + return null; + } + +} diff --git a/shared/AppInsightsCore/src/applicationinsights-core-js.ts b/shared/AppInsightsCore/src/applicationinsights-core-js.ts index 78cc7151f..6af27edca 100644 --- a/shared/AppInsightsCore/src/applicationinsights-core-js.ts +++ b/shared/AppInsightsCore/src/applicationinsights-core-js.ts @@ -36,6 +36,8 @@ export { parseResponse } from "./JavaScriptSDK/ResponseHelpers"; export { IXDomainRequest, IBackendResponse } from "./JavaScriptSDK.Interfaces/IXDomainRequest"; export { _ISenderOnComplete, _ISendPostMgrConfig, _ITimeoutOverrideWrapper, _IInternalXhrOverride } from "./JavaScriptSDK.Interfaces/ISenderPostManager"; export { SenderPostManager } from "./JavaScriptSDK/SenderPostManager"; +export { IStatsBeatEvent } from "./JavaScriptSDK.Interfaces/IStatsBeatEvent"; +export { IStatsBeat, IStatsBeatConfig } from "./JavaScriptSDK.Interfaces/IStatsBeat"; export { isArray, isTypeof, isUndefined, isNullOrUndefined, objHasOwnProperty as hasOwnProperty, isObject, isFunction, strEndsWith, strStartsWith, isDate, isError, isString, isNumber, isBoolean, arrForEach, arrIndexOf, From fc2ee8d3aa5657d09c703b647ddb5dbc6031fa90 Mon Sep 17 00:00:00 2001 From: Nev <54870357+MSNev@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:03:23 -0700 Subject: [PATCH 14/72] [main] Handle race condition during unload (#2507) --- .../src/Sender.ts | 30 ++++++++++++++----- common/config/rush/npm-shrinkwrap.json | 13 -------- tools/chrome-debug-extension/manifest.json | 4 +-- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/channels/applicationinsights-channel-js/src/Sender.ts b/channels/applicationinsights-channel-js/src/Sender.ts index 5a06d257c..4049299a0 100644 --- a/channels/applicationinsights-channel-js/src/Sender.ts +++ b/channels/applicationinsights-channel-js/src/Sender.ts @@ -656,7 +656,12 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { endpoint: _endpointUrl, version: EnvelopeCreator.Version } as IStatsBeatConfig; - return _self.core.getStatsBeat(statsBeatConfig); + + let core = _self.core; + + // During page unload the core may have been cleared and some async events may not have been sent yet + // resulting in the core being null. In this case we don't want to create a statsbeat instance + return core ? core.getStatsBeat(statsBeatConfig) : null; } function _xdrOnLoad (xdr: IXDomainRequest, payload: IInternalStorageItem[]) { @@ -1304,12 +1309,21 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { // Using function lookups for backward compatibility as the getNotifyMgr() did not exist until after v2.5.6 function _getNotifyMgr() : INotificationManager { const func = "getNotifyMgr"; - if (_self.core[func]) { - return _self.core[func](); + let result: INotificationManager; + let core = _self.core; + if (core) { + // During page unload the core may have been cleared and some async events may not have been sent yet + // resulting in the core being null. In this case we don't want to create a statsbeat instance + + if (core[func]) { + result = core[func](); + } else { + // using _self.core['_notificationManager'] for backward compatibility + result = (core as any)["_notificationManager"]; + } } - // using _self.core['_notificationManager'] for backward compatibility - return _self.core["_notificationManager"]; + return result; } function _notifySendRequest(sendRequest: SendRequestReason, isAsync: boolean) { @@ -1469,7 +1483,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { /** * error handler - * @Internal + * @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) { @@ -1478,7 +1492,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { /** * partial success handler - * @Internal + * @internal * since version 3.2.0, if the payload is string[], this function is no-op (string[] is only used for backwards Compatibility) */ public _onPartialSuccess(payload: string[] | IInternalStorageItem[], results: IBackendResponse) { @@ -1487,7 +1501,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { /** * success handler - * @Internal + * @internal * since version 3.2.0, if the payload is string[], this function is no-op (string[] is only used for backwards Compatibility) */ public _onSuccess(payload: string[] | IInternalStorageItem[], countOfItemsInPayload: number) { diff --git a/common/config/rush/npm-shrinkwrap.json b/common/config/rush/npm-shrinkwrap.json index b843d7618..7f14d9349 100644 --- a/common/config/rush/npm-shrinkwrap.json +++ b/common/config/rush/npm-shrinkwrap.json @@ -3540,19 +3540,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", diff --git a/tools/chrome-debug-extension/manifest.json b/tools/chrome-debug-extension/manifest.json index 9b234f5a8..9de91a8a1 100644 --- a/tools/chrome-debug-extension/manifest.json +++ b/tools/chrome-debug-extension/manifest.json @@ -2,8 +2,8 @@ "name": "Telemetry Viewer - M3", "short_name": "Telemetry Viewer M3", "description": "A browser extension that provides a real time view of what's happening in Application Insights including what telemetry is being logged by the web application", - "version": "0.7.5", - "version_name": "0.7.5", + "version": "0.8.0", + "version_name": "0.8.0", "manifest_version": 3, "icons": { "16": "images/icon-16.png", From 188c7551eea3c8adbcd9460e219d677c2f4917f3 Mon Sep 17 00:00:00 2001 From: aimbrenda <120031791+aimbrenda@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:43:37 +0200 Subject: [PATCH 15/72] Drop correlation header to be passed on the dependency (#2506) * added boolean as return type of the DependencyListenerFunction and based on the result of the listenerfunctions the correlation headers can be skipped * code minification and refactoring of documentation --------- Co-authored-by: Nev <54870357+MSNev@users.noreply.github.com> --- .../src/DependencyListener.ts | 7 +- .../src/ajax.ts | 149 +++++++++--------- 2 files changed, 80 insertions(+), 76 deletions(-) diff --git a/extensions/applicationinsights-dependencies-js/src/DependencyListener.ts b/extensions/applicationinsights-dependencies-js/src/DependencyListener.ts index 5685df318..6f648a081 100644 --- a/extensions/applicationinsights-dependencies-js/src/DependencyListener.ts +++ b/extensions/applicationinsights-dependencies-js/src/DependencyListener.ts @@ -63,7 +63,7 @@ export interface IDependencyListenerDetails { /** * The function that will get called when the ajax request is about to occur. */ -export declare type DependencyListenerFunction = (dependencyDetails: IDependencyListenerDetails) => void; +export declare type DependencyListenerFunction = (dependencyDetails: IDependencyListenerDetails) => boolean | void; export interface IDependencyHandler { remove(): void; @@ -73,8 +73,9 @@ export interface IDependencyListenerHandler extends IDependencyHandler { export interface IDependencyListenerContainer { /** - * Add an ajax listener which is called just prior to the request being sent and before the correlation headers are added, to allow you - * to access the headers and modify the values used to generate the distributed tracing correlation headers. (added in v2.8.4) + * Add an ajax listener which is called just prior to the request being sent and before the correlation headers are added. + * This allows you to access the headers and modify the values used to generate the distributed tracing correlation headers (added in v2.8.4), + * or to drop the correlation (added in v3.3.7). * @param dependencyListener - The Telemetry Initializer function * @returns - A IDependencyListenerHandler to enable the initializer to be removed */ diff --git a/extensions/applicationinsights-dependencies-js/src/ajax.ts b/extensions/applicationinsights-dependencies-js/src/ajax.ts index cdefe90e1..8fb7ac66f 100644 --- a/extensions/applicationinsights-dependencies-js/src/ajax.ts +++ b/extensions/applicationinsights-dependencies-js/src/ajax.ts @@ -244,8 +244,9 @@ function _processDependencyContainer(core: IAppInsightsCo return result; } -function _processDependencyListeners(listeners: _IInternalDependencyHandler[], core: IAppInsightsCore, ajaxData: ajaxRecord, xhr: XMLHttpRequest, input?: Request | string, init?: RequestInit): void { +function _processDependencyListeners(listeners: _IInternalDependencyHandler[], core: IAppInsightsCore, ajaxData: ajaxRecord, xhr: XMLHttpRequest, input?: Request | string, init?: RequestInit): boolean { var initializersCount = listeners.length; + let result = true; if (initializersCount > 0) { let details: IDependencyListenerDetails = { core: core, @@ -259,13 +260,15 @@ function _processDependencyListeners(listeners: _IInternalDependencyHandler { // Test Hook to allow the overriding of the location host let currentWindowHost = _self["_currentWindowHost"] || _currentWindowHost; - - _processDependencyListeners(_dependencyListeners, _self.core, ajaxData, xhr, input, init); - - if (input || input === "") { // Fetch - if (correlationIdCanIncludeCorrelationHeader(_extensionConfig, ajaxData.getAbsoluteUrl(), currentWindowHost)) { - if (!init) { - init = {}; - } - - // init headers override original request headers - // so, if they exist use only them, otherwise use request's because they should have been applied in the first place - // not using original request headers will result in them being lost - let headers = new Headers(init.headers || (input instanceof Request ? (input.headers || {}) : {})); - if (_isUsingAIHeaders) { - const id = "|" + ajaxData.traceID + "." + ajaxData.spanID; - headers.set(RequestHeaders[eRequestHeaders.requestIdHeader], id); - if (_enableRequestHeaderTracking) { - ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.requestIdHeader]] = id; - } - } - const appId: string = _appId ||(_context && _context.appId()); - if (appId) { - headers.set(RequestHeaders[eRequestHeaders.requestContextHeader], RequestHeaders[eRequestHeaders.requestContextAppIdFormat] + appId); - if (_enableRequestHeaderTracking) { - ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.requestContextHeader]] = RequestHeaders[eRequestHeaders.requestContextAppIdFormat] + appId; - } - } - if (_isUsingW3CHeaders) { - let traceFlags = ajaxData.traceFlags; - if (isNullOrUndefined(traceFlags)) { - traceFlags = 0x01; - } - - const traceParent = formatTraceParent(createTraceParent(ajaxData.traceID, ajaxData.spanID, traceFlags)); - headers.set(RequestHeaders[eRequestHeaders.traceParentHeader], traceParent); - if (_enableRequestHeaderTracking) { - ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.traceParentHeader]] = traceParent; + + if (_processDependencyListeners(_dependencyListeners, _self.core, ajaxData, xhr, input, init)) { + if (input || input === "") { // Fetch + if (correlationIdCanIncludeCorrelationHeader(_extensionConfig, ajaxData.getAbsoluteUrl(), currentWindowHost)) { + if (!init) { + init = {}; } - } - init.headers = headers; - } - - return init; - } else if (xhr) { // XHR - if (correlationIdCanIncludeCorrelationHeader(_extensionConfig, ajaxData.getAbsoluteUrl(), currentWindowHost)) { - if (_isUsingAIHeaders) { - if (!_isHeaderSet(xhr, RequestHeaders[eRequestHeaders.requestIdHeader])) { + // init headers override original request headers + // so, if they exist use only them, otherwise use request's because they should have been applied in the first place + // not using original request headers will result in them being lost + let headers = new Headers(init.headers || (input instanceof Request ? (input.headers || {}) : {})); + if (_isUsingAIHeaders) { const id = "|" + ajaxData.traceID + "." + ajaxData.spanID; - xhr.setRequestHeader(RequestHeaders[eRequestHeaders.requestIdHeader], id); + headers.set(RequestHeaders[eRequestHeaders.requestIdHeader], id); if (_enableRequestHeaderTracking) { ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.requestIdHeader]] = id; } - } else { - _throwInternalWarning(_self, _eInternalMessageId.FailedMonitorAjaxSetRequestHeader, - "Unable to set [" + RequestHeaders[eRequestHeaders.requestIdHeader] + "] as it has already been set by another instance"); } - } - const appId = _appId || (_context && _context.appId()); - if (appId) { - if (!_isHeaderSet(xhr, RequestHeaders[eRequestHeaders.requestContextHeader])) { - xhr.setRequestHeader(RequestHeaders[eRequestHeaders.requestContextHeader], RequestHeaders[eRequestHeaders.requestContextAppIdFormat] + appId); + const appId: string = _appId ||(_context && _context.appId()); + if (appId) { + headers.set(RequestHeaders[eRequestHeaders.requestContextHeader], RequestHeaders[eRequestHeaders.requestContextAppIdFormat] + appId); if (_enableRequestHeaderTracking) { ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.requestContextHeader]] = RequestHeaders[eRequestHeaders.requestContextAppIdFormat] + appId; } - } else { - _throwInternalWarning(_self, _eInternalMessageId.FailedMonitorAjaxSetRequestHeader, - "Unable to set [" + RequestHeaders[eRequestHeaders.requestContextHeader] + "] as it has already been set by another instance"); - } - } - if (_isUsingW3CHeaders) { - let traceFlags = ajaxData.traceFlags; - if (isNullOrUndefined(traceFlags)) { - traceFlags = 0x01; } + if (_isUsingW3CHeaders) { + let traceFlags = ajaxData.traceFlags; + if (isNullOrUndefined(traceFlags)) { + traceFlags = 0x01; + } - if (!_isHeaderSet(xhr, RequestHeaders[eRequestHeaders.traceParentHeader])) { const traceParent = formatTraceParent(createTraceParent(ajaxData.traceID, ajaxData.spanID, traceFlags)); - xhr.setRequestHeader(RequestHeaders[eRequestHeaders.traceParentHeader], traceParent); + headers.set(RequestHeaders[eRequestHeaders.traceParentHeader], traceParent); if (_enableRequestHeaderTracking) { ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.traceParentHeader]] = traceParent; } - } else { - _throwInternalWarning(_self, _eInternalMessageId.FailedMonitorAjaxSetRequestHeader, - "Unable to set [" + RequestHeaders[eRequestHeaders.traceParentHeader] + "] as it has already been set by another instance"); } + + init.headers = headers; } - } - return xhr; + return init; + } else if (xhr) { // XHR + if (correlationIdCanIncludeCorrelationHeader(_extensionConfig, ajaxData.getAbsoluteUrl(), currentWindowHost)) { + if (_isUsingAIHeaders) { + if (!_isHeaderSet(xhr, RequestHeaders[eRequestHeaders.requestIdHeader])) { + const id = "|" + ajaxData.traceID + "." + ajaxData.spanID; + xhr.setRequestHeader(RequestHeaders[eRequestHeaders.requestIdHeader], id); + if (_enableRequestHeaderTracking) { + ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.requestIdHeader]] = id; + } + } else { + _throwInternalWarning(_self, _eInternalMessageId.FailedMonitorAjaxSetRequestHeader, + "Unable to set [" + RequestHeaders[eRequestHeaders.requestIdHeader] + "] as it has already been set by another instance"); + } + } + const appId = _appId || (_context && _context.appId()); + if (appId) { + if (!_isHeaderSet(xhr, RequestHeaders[eRequestHeaders.requestContextHeader])) { + xhr.setRequestHeader(RequestHeaders[eRequestHeaders.requestContextHeader], RequestHeaders[eRequestHeaders.requestContextAppIdFormat] + appId); + if (_enableRequestHeaderTracking) { + ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.requestContextHeader]] = RequestHeaders[eRequestHeaders.requestContextAppIdFormat] + appId; + } + } else { + _throwInternalWarning(_self, _eInternalMessageId.FailedMonitorAjaxSetRequestHeader, + "Unable to set [" + RequestHeaders[eRequestHeaders.requestContextHeader] + "] as it has already been set by another instance"); + } + } + if (_isUsingW3CHeaders) { + let traceFlags = ajaxData.traceFlags; + if (isNullOrUndefined(traceFlags)) { + traceFlags = 0x01; + } + + if (!_isHeaderSet(xhr, RequestHeaders[eRequestHeaders.traceParentHeader])) { + const traceParent = formatTraceParent(createTraceParent(ajaxData.traceID, ajaxData.spanID, traceFlags)); + xhr.setRequestHeader(RequestHeaders[eRequestHeaders.traceParentHeader], traceParent); + if (_enableRequestHeaderTracking) { + ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.traceParentHeader]] = traceParent; + } + } else { + _throwInternalWarning(_self, _eInternalMessageId.FailedMonitorAjaxSetRequestHeader, + "Unable to set [" + RequestHeaders[eRequestHeaders.traceParentHeader] + "] as it has already been set by another instance"); + } + } + } + + return xhr; + } } return undefined; From 87971a1314aaaf77e277f2c2d4682a8a30f18912 Mon Sep 17 00:00:00 2001 From: Nev <54870357+MSNev@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:11:11 -0700 Subject: [PATCH 16/72] [beta] Merge remote-tracking branch 'origin/main' into `beta` (#2509) * [main] Add stale bot for assigned issues and prs (#2495) * move to beta again (#2496) Co-authored-by: Nev <54870357+MSNev@users.noreply.github.com> * [main] support adding cross origin resource policy for #1851 (#2423) * add header * add test * Update IConfiguration.ts * rename * based on comment * Update Sender.tests.ts * local variable * Update Sender.ts * slight change test * Update Sender.ts * [Main][Task]31248254: Add Monitor Tests for CDN OPTIONS Calls (#2491) * add cdn montioring tests * update * update * update * update * update * update * [main] Minor fixes for handling ikey promises with dynamic changes (#2500) - Additional Minification improvements * [chrome debug tool] publish 0.8.0 for manifect V3 (#2499) * [Main][Task]31233527:Change Default RequestSizeLimitBytes (#2501) * add post channel config * update * udpate * update * update * update * udpate * update * update * update * [main][stats beat] implement stats beat in application insights (#2489) * [main] Handle race condition during unload (#2507) * Drop correlation header to be passed on the dependency (#2506) * added boolean as return type of the DependencyListenerFunction and based on the result of the listenerfunctions the correlation headers can be skipped * code minification and refactoring of documentation --------- Co-authored-by: Nev <54870357+MSNev@users.noreply.github.com> --------- Co-authored-by: siyuniu-ms <123212536+siyuniu-ms@users.noreply.github.com> Co-authored-by: Karlie-777 <79606506+Karlie-777@users.noreply.github.com> Co-authored-by: aimbrenda <120031791+aimbrenda@users.noreply.github.com> --- .github/workflows/stale-assigned.yml | 30 ++ AISKU/Tests/Manual/testVersionConflict.html | 10 +- AISKU/Tests/Unit/src/AISKUSize.Tests.ts | 4 +- .../Unit/src/applicationinsights.e2e.tests.ts | 152 +++++++++- AISKU/src/AISku.ts | 82 ++--- .../Tests/Unit/src/AISKULightSize.Tests.ts | 4 +- channels/1ds-post-js/src/HttpManager.ts | 2 +- channels/1ds-post-js/src/Serializer.ts | 3 +- .../Tests/Unit/src/Sender.tests.ts | 71 +++++ .../Tests/Unit/src/StatsBeat.tests.ts | 255 ++++++++++++++++ .../Tests/Unit/src/aichannel.tests.ts | 2 + .../src/Interfaces.ts | 14 + .../src/SendBuffer.ts | 2 + .../src/Sender.ts | 133 +++++++-- common/config/rush/npm-shrinkwrap.json | 271 +++++++++-------- .../Unit/src/AnalyticsExtensionSize.tests.ts | 2 +- .../Tests/Unit/src/cfgsynchelper.tests.ts | 8 +- .../src/DependencyListener.ts | 7 +- .../src/ajax.ts | 149 +++++----- .../test/Unit/src/FileSizeCheckTest.ts | 2 +- .../Unit/src/AppInsightsCoreSize.Tests.ts | 4 +- .../Tests/Unit/src/StatsBeat.Tests.ts | 280 ++++++++++++++++++ .../Tests/Unit/src/aiunittests.ts | 3 + .../IAppInsightsCore.ts | 3 + .../IConfiguration.ts | 19 ++ .../JavaScriptSDK.Interfaces/IStatsBeat.ts | 16 + .../IStatsBeatEvent.ts | 10 + .../JavaScriptSDK.Interfaces/IXHROverride.ts | 1 - .../src/JavaScriptSDK/AppInsightsCore.ts | 268 ++++++++++------- .../src/JavaScriptSDK/NetworkStatsbeat.ts | 22 ++ .../src/JavaScriptSDK/SenderPostManager.ts | 28 +- .../src/JavaScriptSDK/StatsBeat.ts | 203 +++++++++++++ .../src/applicationinsights-core-js.ts | 2 + tools/chrome-debug-extension/manifest.json | 4 +- tools/chrome-debug-extension/package.json | 2 +- .../injectScript.js | 4 +- version.json | 2 +- 37 files changed, 1648 insertions(+), 426 deletions(-) create mode 100644 .github/workflows/stale-assigned.yml create mode 100644 channels/applicationinsights-channel-js/Tests/Unit/src/StatsBeat.tests.ts create mode 100644 shared/AppInsightsCore/Tests/Unit/src/StatsBeat.Tests.ts create mode 100644 shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IStatsBeat.ts create mode 100644 shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IStatsBeatEvent.ts create mode 100644 shared/AppInsightsCore/src/JavaScriptSDK/NetworkStatsbeat.ts create mode 100644 shared/AppInsightsCore/src/JavaScriptSDK/StatsBeat.ts diff --git a/.github/workflows/stale-assigned.yml b/.github/workflows/stale-assigned.yml new file mode 100644 index 000000000..7aceb75ca --- /dev/null +++ b/.github/workflows/stale-assigned.yml @@ -0,0 +1,30 @@ +name: 'Stale issue handler' +on: + workflow_dispatch: + schedule: + - cron: '0 7 * * *' + +permissions: + actions: write + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + id: stale + with: + stale-issue-message: 'This Issue will be closed in 30 days. Please remove the "Stale" label or comment to avoid closure with no action.' + stale-pr-message: 'This PR will be closed in 30 days. Please remove the "Stale" label or comment to avoid closure with no action.' + operations-per-run: 200 + days-before-stale: 360 + days-before-close: 30 + stale-issue-label: 'stale' + stale-pr-label: 'stale' + close-issue-label: 'closed' + close-pr-label: 'closed' + exempt-issue-labels: 'bug,enhancement,documentation,waiting,keep' + exempt-pr-labels: 'waiting,keep' + include-only-assigned: true \ No newline at end of file diff --git a/AISKU/Tests/Manual/testVersionConflict.html b/AISKU/Tests/Manual/testVersionConflict.html index 2b7f4ca0e..1e1b5e997 100644 --- a/AISKU/Tests/Manual/testVersionConflict.html +++ b/AISKU/Tests/Manual/testVersionConflict.html @@ -10,7 +10,8 @@

Microsoft Application Insights JavaScript SDK - AISKU

- --> \ No newline at end of file diff --git a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts index 2343d0056..1a78e6f94 100644 --- a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts +++ b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts @@ -54,8 +54,8 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class AISKUSizeCheck extends AITestClass { - private readonly MAX_RAW_SIZE = 145; - private readonly MAX_BUNDLE_SIZE = 145; + private readonly MAX_RAW_SIZE = 146; + private readonly MAX_BUNDLE_SIZE = 146; private readonly MAX_RAW_DEFLATE_SIZE = 58; private readonly MAX_BUNDLE_DEFLATE_SIZE = 58; private readonly rawFilePath = "../dist/es5/applicationinsights-web.min.js"; diff --git a/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts b/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts index e4448490e..45973ccb1 100644 --- a/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts +++ b/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts @@ -9,6 +9,7 @@ import { createAsyncResolvedPromise } from '@nevware21/ts-async'; import { CONFIG_ENDPOINT_URL } from '../../../src/InternalConstants'; import { OfflineChannel } from '@microsoft/applicationinsights-offlinechannel-js'; import { IStackFrame } from '@microsoft/applicationinsights-common/src/Interfaces/Contracts/IStackFrame'; +import { utcNow } from '@nevware21/ts-utils'; function _checkExpectedFrame(expectedFrame: IStackFrame, actualFrame: IStackFrame, index: number) { Assert.equal(expectedFrame.assembly, actualFrame.assembly, index + ") Assembly is not as expected"); @@ -153,13 +154,14 @@ export class ApplicationInsightsTests extends AITestClass { } public registerTests() { - this.addDynamicConfigTests() + this.addDynamicConfigTests(); this.addGenericE2ETests(); this.addAnalyticsApiTests(); this.addAsyncTests(); this.addDependencyPluginTests(); this.addPropertiesPluginTests(); this.addCDNOverrideTests(); + this.addCdnMonitorTests(); } public addGenericE2ETests(): void { @@ -725,6 +727,154 @@ export class ApplicationInsightsTests extends AITestClass { }); } + public addCdnMonitorTests(): void { + this.testCaseAsync({ + name: "E2E.GenericTests: Fetch Current CDN V3", + stepDelay: 1, + useFakeServer: false, + useFakeFetch: false, + fakeFetchAutoRespond: false, + steps: [() => { + // Use beta endpoint to pre-test any changes before public V3 cdn + let random = utcNow(); + // Under Cors Mode, Options request will be auto-triggered + try { + fetch(`https://js.monitor.azure.com/beta/ai.3.gbl.min.js?${random}`, { + method: "GET" + }).then((res) => { + this._ctx.res = res; + res.text().then((val) => { + this._ctx.val = val; + }); + }); + } catch (e) { + Assert.ok(false, "Fetch Error: " + e); + } + + }].concat(PollingAssert.createPollingAssert(() => { + + if (this._ctx && this._ctx.res && this._ctx.val) { + let res = this._ctx.res; + let status = res.status; + if (status === 200) { + // for Response headers: + // content-type: text/javascript; charset=utf-8 + // x-ms-meta-aijssdksrc: should present + // x-ms-meta-aijssdkver should present + let headers = res.headers; + let headerCnt = 0; + headers.forEach((val, key) => { + if (key === "content-type") { + Assert.deepEqual(val, "text/javascript; charset=utf-8", "should have correct content-type response header"); + headerCnt ++; + } + if (key === "x-ms-meta-aijssdksrc") { + Assert.ok(val, "should have sdk src response header"); + headerCnt ++; + } + if (key === "x-ms-meta-aijssdkver") { + Assert.ok(val, "should have version number for response header"); + headerCnt ++; + } + }); + Assert.equal(headerCnt, 3, "all expected headers should be present"); + return true; + } + return false; + } + return false; + }, "Wait for response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "E2E.GenericTests: Fetch Current CDN V2", + stepDelay: 1, + useFakeServer: false, + useFakeFetch: false, + fakeFetchAutoRespond: false, + steps: [() => { + // Use public endpoint for V2 + let random = utcNow(); + // Under Cors Mode, Options request will be triggered + fetch(`https://js.monitor.azure.com/scripts/b/ai.2.gbl.min.js?${random}`, { + method: "GET" + }).then((res) => { + this._ctx.res = res; + res.text().then((val) => { + this._ctx.val = val; + }); + }); + + }].concat(PollingAssert.createPollingAssert(() => { + if (this._ctx && this._ctx.res && this._ctx.val) { + let res = this._ctx.res; + let status = res.status; + if (status === 200) { + // for Response headers: + // content-type: text/javascript; charset=utf-8 + // x-ms-meta-aijssdksrc: should present + // x-ms-meta-aijssdkver should present + let headers = res.headers; + let headerCnt = 0; + headers.forEach((val, key) => { + if (key === "content-type") { + Assert.deepEqual(val, "text/javascript; charset=utf-8", "should have correct content-type response header"); + headerCnt ++; + } + if (key === "x-ms-meta-aijssdksrc") { + Assert.ok(val, "should have sdk src response header"); + headerCnt ++; + } + if (key === "x-ms-meta-aijssdkver") { + Assert.ok(val, "should have version number for response header"); + headerCnt ++; + } + }); + Assert.equal(headerCnt, 3, "all expected headers should be present"); + return true; + } + return false; + } + return false; + }, "Wait for response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "E2E.GenericTests: Fetch Static Web CDN V3", + stepDelay: 1, + useFakeServer: false, + useFakeFetch: false, + fakeFetchAutoRespond: false, + steps: [() => { + // Use beta endpoint to pre-test any changes before public V3 cdn + let random = utcNow(); + // Under Cors Mode, Options request will be auto-triggered + try { + fetch(`https://js0.tst.applicationinsights.io/scripts/b/ai.3.gbl.min.js?${random}`, { + method: "GET" + }).then((res) => { + this._ctx.res = res; + if (res.ok) { + res.text().then((val) => { + this._ctx.val = val; + }); + } + }).catch((e) => { + this._ctx.err = e.message; + }) + } catch (e) { + this._ctx.err = e; + } + }].concat(PollingAssert.createPollingAssert(() => { + + if (this._ctx && this._ctx.err) { + return true; + } + return false; + }, "Wait for response" + new Date().toISOString(), 60, 1000) as any) + }); + } + public addAsyncTests(): void { this.testCaseAsync({ name: "E2E.GenericTests: Send events with offline support", diff --git a/AISKU/src/AISku.ts b/AISKU/src/AISku.ts index 84b93fa73..4efcf6eef 100644 --- a/AISKU/src/AISku.ts +++ b/AISKU/src/AISku.ts @@ -103,12 +103,27 @@ function _chkDiagLevel(value: number) { return value && value > 0; } +function _parseCs(config: IConfiguration & IConfig, configCs: string | IPromise) { + return createSyncPromise((resolve, reject) => { + doAwaitResponse(configCs, (res) => { + let curCs = res && res.value; + let parsedCs = null; + if (!res.rejected && curCs) { + // replace cs with resolved values in case of circular promises + config.connectionString = curCs; + parsedCs = parseConnectionString(curCs); + } + + // if can't resolve cs promise, null will be returned + resolve(parsedCs); + }); + }); +} + /** * Application Insights API * @group Entrypoint * @group Classes - * @class Initialization - * @implements {IApplicationInsights} */ export class AppInsightsSku implements IApplicationInsights { public snippet: Snippet; @@ -200,60 +215,47 @@ export class AppInsightsSku implements IApplicationInsights { // Will get recalled if any referenced values are changed _addUnloadHook(onConfigChange(cfgHandler, () => { - let configCs = _config.connectionString; - - function _parseCs() { - return createSyncPromise((resolve, reject) => { - doAwaitResponse(configCs, (res) => { - let curCs = res && res.value; - let parsedCs = null; - if (!res.rejected && curCs) { - // replace cs with resolved values in case of circular promises - _config.connectionString = curCs; - parsedCs = parseConnectionString(curCs); - } - // if can't resolve cs promise, null will be returned - resolve(parsedCs); - }); - }); - - } + let configCs = _config.connectionString; if (isPromiseLike(configCs)) { let ikeyPromise = createSyncPromise((resolve, reject) => { - _parseCs().then((cs) => { - let ikey = _config.instrumentationKey; - ikey = cs && cs.instrumentationkey || ikey; - resolve(ikey); - }).catch((e) => { - // parseCs will always resolve(unless timeout) - // return null in case any error happens - resolve(null); + doAwaitResponse(_parseCs(_config, configCs), (rsp) => { + if (!rsp.rejected) { + let ikey = _config.instrumentationKey; + let cs = rsp.value; + ikey = cs && cs.instrumentationkey || ikey; + resolve(ikey); + } else { + // parseCs will always resolve(unless timeout) + // return null in case any error happens + resolve(null); + } }); - }); let url: IPromise | string = _config.userOverrideEndpointUrl; if (isNullOrUndefined(url)) { url = createSyncPromise((resolve, reject) => { - _parseCs().then((cs) => { - let url = _config.endpointUrl; - let ingest = cs && cs.ingestionendpoint; - url = ingest? ingest + DEFAULT_BREEZE_PATH : url; - resolve(url); - }).catch((e) => { - // parseCs will always resolve(unless timeout) - // return null in case any error happens - resolve(null); + doAwaitResponse(_parseCs(_config, configCs), (rsp) => { + if (!rsp.rejected) { + let url = _config.endpointUrl; + let cs = rsp.value; + let ingest = cs && cs.ingestionendpoint; + url = ingest? ingest + DEFAULT_BREEZE_PATH : url; + resolve(url); + } else { + // parseCs will always resolve(unless timeout) + // return null in case any error happens + resolve(null); + } }); - }); } _config.instrumentationKey = ikeyPromise; _config.endpointUrl = url; - } + if (isString(configCs) && configCs) { // confirm if promiselike function present // handle cs promise here diff --git a/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts b/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts index 80cf44a22..ab5e74f0b 100644 --- a/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts +++ b/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts @@ -51,8 +51,8 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class AISKULightSizeCheck extends AITestClass { - private readonly MAX_RAW_SIZE = 91; - private readonly MAX_BUNDLE_SIZE = 91; + private readonly MAX_RAW_SIZE = 92; + private readonly MAX_BUNDLE_SIZE = 92; private readonly MAX_RAW_DEFLATE_SIZE = 38; private readonly MAX_BUNDLE_DEFLATE_SIZE = 38; private readonly rawFilePath = "../dist/es5/applicationinsights-web-basic.min.js"; diff --git a/channels/1ds-post-js/src/HttpManager.ts b/channels/1ds-post-js/src/HttpManager.ts index bb3caf047..a27121ede 100644 --- a/channels/1ds-post-js/src/HttpManager.ts +++ b/channels/1ds-post-js/src/HttpManager.ts @@ -774,7 +774,7 @@ export class HttpManager { // Make sure we have a payload object thePayload = thePayload || _serializer.createPayload(retryCount, isTeardown, isSynchronous, isReducedPayload, sendReason, sendType); - + // Add the batch to the current payload if (!_serializer.appendPayload(thePayload, theBatch, maxEventsPerBatch)) { // Entire batch was not added so send the payload and retry adding this batch diff --git a/channels/1ds-post-js/src/Serializer.ts b/channels/1ds-post-js/src/Serializer.ts index 821eb0b91..8b688eca6 100644 --- a/channels/1ds-post-js/src/Serializer.ts +++ b/channels/1ds-post-js/src/Serializer.ts @@ -30,7 +30,8 @@ import { mathMin, strSubstr } from "@nevware21/ts-utils"; */ const _MAX_STRING_JOINS = 20; -const RequestSizeLimitBytes = 3984588; // approx 3.8 Mb +// Max Size set by One Collector: https://msazure.visualstudio.com/OneDsCollector/_git/Collector?path=/Services/Azure/CollectorWorkerRoleAzure/ServiceConfiguration.Cloud.cscfg +const RequestSizeLimitBytes = 3145728; // approx 3.15 Mb const BeaconRequestSizeLimitBytes = 65000; // approx 64kb (the current Edge, Firefox and Chrome max limit) const MaxRecordSize = 2000000; // approx 2 Mb const MaxBeaconRecordSize = mathMin(MaxRecordSize, BeaconRequestSizeLimitBytes); diff --git a/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts b/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts index 4ebd8e378..cb7431121 100644 --- a/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts +++ b/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts @@ -2739,6 +2739,77 @@ export class SenderTests extends AITestClass { } }); + this.testCase({ + name: 'Users could set the cross-origin header via request', + useFakeTimers: true, + test: () => { + let core = new AppInsightsCore(); + let id = this._sender.identifier; + let coreConfig = { + instrumentationKey: 'abc', + isBeaconApiDisabled: true, + extensionConfig: { + [this._sender.identifier]: { + corsPolicy: "cross-origin", + } + } + } + core.initialize(coreConfig, [this._sender]); + + let sendBeaconCalled = false; + this.hookSendBeacon((url: string) => { + sendBeaconCalled = true; + return true; + }); + + const telemetryItem: ITelemetryItem = { + name: 'fake item', + iKey: 'iKey', + baseType: 'some type', + baseData: {} + }; + + try { + this._sender.processTelemetry(telemetryItem); + this.clock.tick(30000); + } catch(e) { + QUnit.assert.ok(false); + } + const CrossOriginResourcePolicyHeader: string = "X-Set-Cross-Origin-Resource-Policy"; + QUnit.assert.equal(1, this._getXhrRequests().length, "xhr sender is called"); + let headers = this._getXhrRequests()[0].requestHeaders; + QUnit.assert.ok(headers.hasOwnProperty(CrossOriginResourcePolicyHeader)); + QUnit.assert.equal(headers[CrossOriginResourcePolicyHeader], 'cross-origin'); + QUnit.assert.notOk(this._getXhrRequests()[0].requestHeaders.hasOwnProperty('testHeader')); + + // dynamic change + core.config.extensionConfig[this._sender.identifier].corsPolicy = "same-origin"; + this.clock.tick(1); + try { + this._sender.processTelemetry(telemetryItem); + this.clock.tick(30000); + } catch(e) { + QUnit.assert.ok(false); + } + headers = this._getXhrRequests()[1].requestHeaders; + QUnit.assert.ok(headers.hasOwnProperty(CrossOriginResourcePolicyHeader)); + QUnit.assert.equal(headers[CrossOriginResourcePolicyHeader], 'same-origin'); + QUnit.assert.notOk(this._getXhrRequests()[1].requestHeaders.hasOwnProperty('testHeader')); + + // dynamic change to null + core.config.extensionConfig[this._sender.identifier].corsPolicy = null; + this.clock.tick(1); + try { + this._sender.processTelemetry(telemetryItem); + this.clock.tick(30000); + } catch(e) { + QUnit.assert.ok(false); + } + headers = this._getXhrRequests()[2].requestHeaders; + QUnit.assert.notOk(this._getXhrRequests()[2].requestHeaders.hasOwnProperty(CrossOriginResourcePolicyHeader)); + } + }); + this.testCase({ name: 'Users are allowed to add customHeaders when endpointUrl is not Breeze.', test: () => { diff --git a/channels/applicationinsights-channel-js/Tests/Unit/src/StatsBeat.tests.ts b/channels/applicationinsights-channel-js/Tests/Unit/src/StatsBeat.tests.ts new file mode 100644 index 000000000..b204ecc86 --- /dev/null +++ b/channels/applicationinsights-channel-js/Tests/Unit/src/StatsBeat.tests.ts @@ -0,0 +1,255 @@ +import { AITestClass, Assert, PollingAssert } from "@microsoft/ai-test-framework"; +import { AppInsightsCore, getWindow, IPayloadData, ITelemetryItem, TransportType } from "@microsoft/applicationinsights-core-js"; +import { Sender } from "../../../src/Sender"; +import { SinonSpy, SinonStub } from "sinon"; +import { ISenderConfig } from "../../../types/applicationinsights-channel-js"; +import { isBeaconApiSupported } from "@microsoft/applicationinsights-common"; + +export class StatsbeatTests extends AITestClass { + private _core: AppInsightsCore; + private _sender: Sender; + private statsbeatCountSpy: SinonSpy; + private fetchStub: sinon.SinonStub; + private beaconStub: sinon.SinonStub; + private trackSpy: SinonSpy; + + public testInitialize() { + this._core = new AppInsightsCore(); + this._sender = new Sender(); + } + + public testFinishedCleanup() { + if (this._sender && this._sender.isInitialized()) { + this._sender.pause(); + this._sender._buffer.clear(); + this._sender.teardown(); + } + this._sender = null; + this._core = null; + if (this.statsbeatCountSpy) { + this.statsbeatCountSpy.restore(); + } + if (this.fetchStub) { + this.fetchStub.restore(); + } + if (this.beaconStub) { + this.beaconStub.restore(); + } + if (this.trackSpy) { + this.trackSpy.restore(); + } + } + + private initializeCoreAndSender(config: any, instrumentationKey: string) { + const sender = new Sender(); + const core = new AppInsightsCore(); + const coreConfig = { + instrumentationKey, + _sdk: { stats: true }, + extensionConfig: { [sender.identifier]: config } + }; + + core.initialize(coreConfig, [sender]); + this.statsbeatCountSpy = this.sandbox.spy(core.getStatsBeat(), "count"); + this.trackSpy = this.sandbox.spy(core, "track"); + + this.onDone(() => { + sender.teardown(); + }); + + return { core, sender }; + } + + private createSenderConfig(transportType: TransportType) { + return { + endpointUrl: "https://test", + emitLineDelimitedJson: false, + maxBatchInterval: 15000, + maxBatchSizeInBytes: 102400, + disableTelemetry: false, + enableSessionStorageBuffer: true, + isRetryDisabled: false, + isBeaconApiDisabled: false, + disableXhr: false, + onunloadDisableFetch: false, + onunloadDisableBeacon: false, + namePrefix: "", + samplingPercentage: 100, + customHeaders: [{ header: "header", value: "val" }], + convertUndefined: "", + eventsLimitInMem: 10000, + transports: [transportType] + }; + } + + private processTelemetryAndFlush(sender: Sender, telemetryItem: ITelemetryItem) { + try { + sender.processTelemetry(telemetryItem, null); + sender.flush(); + } catch (e) { + QUnit.assert.ok(false, "Unexpected error during telemetry processing"); + } + this.clock.tick(900000); // Simulate time passing for statsbeat to be sent + } + + private assertStatsbeatCall(statusCode: number, eventName: string) { + Assert.equal(this.statsbeatCountSpy.callCount, 1, "Statsbeat count should be called once"); + Assert.equal(this.statsbeatCountSpy.firstCall.args[0], statusCode, `Statsbeat count should be called with status ${statusCode}`); + const data = JSON.stringify(this.statsbeatCountSpy.firstCall.args[1]); + Assert.ok(data.includes("startTime"), "Statsbeat count should be called with startTime set"); + const statsbeatEvent = this.trackSpy.firstCall.args[0]; + Assert.equal(statsbeatEvent.baseType, "MetricData", "Statsbeat event should be of type MetricData"); + Assert.equal(statsbeatEvent.baseData.name, eventName, `Statsbeat event should be of type ${eventName}`); + } + + public registerTests() { + this.testCase({ + name: "Statsbeat initializes when stats is true", + test: () => { + const config = { + _sdk: { stats: true }, + instrumentationKey: "Test-iKey" + }; + + this._core.initialize(config, [this._sender]); + const statsbeat = this._core.getStatsBeat(); + + QUnit.assert.ok(statsbeat, "Statsbeat is initialized"); + QUnit.assert.ok(statsbeat.isInitialized(), "Statsbeat is marked as initialized"); + } + }); + + this.testCaseAsync({ + name: "Statsbeat increments success count when fetch sender is called once", + useFakeTimers: true, + useFakeServer: true, + stepDelay: 100, + steps: [ + () => { + this.fetchStub = this.sandbox.stub(window, "fetch").callsFake(() => { // only fetch is supported to stub, why? + return Promise.resolve(new Response("{}", { status: 200, statusText: "OK" })); + }); + + const config = this.createSenderConfig(TransportType.Fetch); + const { sender } = this.initializeCoreAndSender(config, "000e0000-e000-0000-a000-000000000000"); + + const telemetryItem: ITelemetryItem = { + name: "fake item", + iKey: "testIkey2;ingestionendpoint=testUrl1", + baseType: "some type", + baseData: {} + }; + + this.processTelemetryAndFlush(sender, telemetryItem); + + } + ].concat(PollingAssert.createPollingAssert(() => { + if (this.statsbeatCountSpy.called && this.fetchStub.called) { + this.assertStatsbeatCall(200, "Request_Success_Count"); + return true; + } + return false; + }, "Waiting for fetch sender and Statsbeat count to be called") as any) + }); + + this.testCaseAsync({ + name: "Statsbeat increments throttle count when fetch sender is called with status 439", + useFakeTimers: true, + stepDelay: 100, + steps: [ + () => { + this.fetchStub = this.sandbox.stub(window, "fetch").callsFake(() => { + return Promise.resolve(new Response("{}", { status: 439, statusText: "Too Many Requests" })); + }); + + const config = this.createSenderConfig(TransportType.Fetch); + const { sender } = this.initializeCoreAndSender(config, "000e0000-e000-0000-a000-000000000000"); + + const telemetryItem: ITelemetryItem = { + name: "fake item", + iKey: "testIkey2;ingestionendpoint=testUrl1", + baseType: "some type", + baseData: {} + }; + + this.processTelemetryAndFlush(sender, telemetryItem); + } + ].concat(PollingAssert.createPollingAssert(() => { + if (this.statsbeatCountSpy.called && this.fetchStub.called) { + this.assertStatsbeatCall(439, "Throttle_Count"); + return true; + } + return false; + }, "Waiting for fetch sender and Statsbeat count to be called") as any) + }); + + this.testCaseAsync({ + name: "Statsbeat increments success count for beacon sender", + useFakeTimers: true, + stepDelay: 100, + steps: [ + () => { + const config = this.createSenderConfig(TransportType.Beacon); + const { sender } = this.initializeCoreAndSender(config, "000e0000-e000-0000-a000-000000000000"); + + const telemetryItem: ITelemetryItem = { + name: "fake item", + iKey: "testIkey2;ingestionendpoint=testUrl1", + baseType: "some type", + baseData: {} + }; + let sendBeaconCalled = false; + this.hookSendBeacon((url: string) => { + sendBeaconCalled = true; + return true; + }); + QUnit.assert.ok(isBeaconApiSupported(), "Beacon API is supported"); + this.processTelemetryAndFlush(sender, telemetryItem); + } + ].concat(PollingAssert.createPollingAssert(() => { + if (this.statsbeatCountSpy.called) { + this.assertStatsbeatCall(200, "Request_Success_Count"); + return true; + } + return false; + }, "Waiting for beacon sender and Statsbeat count to be called") as any) + }); + + + this.testCaseAsync({ + name: "Statsbeat increments success count for xhr sender", + useFakeTimers: true, + useFakeServer: true, + stepDelay: 100, + fakeServerAutoRespond: true, + steps: [ + () => { + let window = getWindow(); + let fakeXMLHttpRequest = (window as any).XMLHttpRequest; // why we do this? + let config = this.createSenderConfig(TransportType.Xhr) && {disableSendBeaconSplit: true}; + const { sender } = this.initializeCoreAndSender(config, "000e0000-e000-0000-a000-000000000000"); + console.log("xhr sender called", this._getXhrRequests().length); + + const telemetryItem: ITelemetryItem = { + name: "fake item", + iKey: "testIkey2;ingestionendpoint=testUrl1", + baseType: "some type", + baseData: {} + }; + this.processTelemetryAndFlush(sender, telemetryItem); + QUnit.assert.equal(1, this._getXhrRequests().length, "xhr sender is called"); + console.log("xhr sender is called", this._getXhrRequests().length); + (window as any).XMLHttpRequest = fakeXMLHttpRequest; + + } + ].concat(PollingAssert.createPollingAssert(() => { + if (this.statsbeatCountSpy.called) { + this.assertStatsbeatCall(200, "Request_Success_Count"); + console.log("Statsbeat count called with success count for xhr sender"); + return true; + } + return false; + }, "Waiting for xhr sender and Statsbeat count to be called", 60, 1000) as any) + }); +} +} \ No newline at end of file diff --git a/channels/applicationinsights-channel-js/Tests/Unit/src/aichannel.tests.ts b/channels/applicationinsights-channel-js/Tests/Unit/src/aichannel.tests.ts index b4dd43ee5..97061376c 100644 --- a/channels/applicationinsights-channel-js/Tests/Unit/src/aichannel.tests.ts +++ b/channels/applicationinsights-channel-js/Tests/Unit/src/aichannel.tests.ts @@ -1,9 +1,11 @@ import { SenderTests } from "./Sender.tests"; import { SampleTests } from "./Sample.tests"; import { GlobalTestHooks } from "./GlobalTestHooks.Test"; +import { StatsbeatTests } from "./StatsBeat.tests"; export function runTests() { new GlobalTestHooks().registerTests(); new SenderTests().registerTests(); new SampleTests().registerTests(); + // new StatsbeatTests().registerTests(); } \ No newline at end of file diff --git a/channels/applicationinsights-channel-js/src/Interfaces.ts b/channels/applicationinsights-channel-js/src/Interfaces.ts index 1df9c6625..3a0d975ef 100644 --- a/channels/applicationinsights-channel-js/src/Interfaces.ts +++ b/channels/applicationinsights-channel-js/src/Interfaces.ts @@ -166,6 +166,20 @@ export interface ISenderConfig { * @since 3.2.0 */ maxRetryCnt?: number; + + /** + * [Optional] Specifies the Cross-Origin Resource Policy (CORP) for the endpoint. + * This value is included in the response header as `Cross-Origin-Resource-Policy`, + * which helps control how resources can be shared across different origins. + * + * Possible values: + * - `same-site`: Allows access only from the same site. + * - `same-origin`: Allows access only from the same origin (protocol, host, and port). + * - `cross-origin`: Allows access from any origin. + * + * @since 3.3.7 + */ + corsPolicy?: string; } export interface IBackendResponse { diff --git a/channels/applicationinsights-channel-js/src/SendBuffer.ts b/channels/applicationinsights-channel-js/src/SendBuffer.ts index 91ea6d401..239425269 100644 --- a/channels/applicationinsights-channel-js/src/SendBuffer.ts +++ b/channels/applicationinsights-channel-js/src/SendBuffer.ts @@ -103,6 +103,8 @@ abstract class BaseSendBuffer { if (!isNullOrUndefined(_maxRetryCnt)) { if (payload.cnt > _maxRetryCnt) { // TODO: add log here on dropping payloads + // will log statsbeat exception later here + return; } } diff --git a/channels/applicationinsights-channel-js/src/Sender.ts b/channels/applicationinsights-channel-js/src/Sender.ts index 992520ecc..4049299a0 100644 --- a/channels/applicationinsights-channel-js/src/Sender.ts +++ b/channels/applicationinsights-channel-js/src/Sender.ts @@ -7,20 +7,20 @@ import { import { ActiveStatus, BaseTelemetryPlugin, IAppInsightsCore, IBackendResponse, IChannelControls, IConfigDefaults, IConfiguration, IDiagnosticLogger, IInternalOfflineSupport, INotificationManager, IPayloadData, IPlugin, IProcessTelemetryContext, - IProcessTelemetryUnloadContext, ITelemetryItem, ITelemetryPluginChain, ITelemetryUnloadState, IXDomainRequest, IXHROverride, - OnCompleteCallback, SendPOSTFunction, SendRequestReason, SenderPostManager, TransportType, _ISendPostMgrConfig, _ISenderOnComplete, - _eInternalMessageId, _throwInternal, _warnToConsole, arrForEach, cfgDfBoolean, cfgDfValidate, createProcessTelemetryContext, - createUniqueNamespace, dateNow, dumpObj, eLoggingSeverity, formatErrorMessageXdr, formatErrorMessageXhr, getExceptionName, getIEVersion, - isArray, isBeaconsSupported, isFetchSupported, isNullOrUndefined, mergeEvtNamespace, objExtend, onConfigChange, parseResponse, - prependTransports, runTargetUnload + IProcessTelemetryUnloadContext, IStatsBeatConfig, IStatsBeatEvent, ITelemetryItem, ITelemetryPluginChain, ITelemetryUnloadState, + IXDomainRequest, IXHROverride, OnCompleteCallback, SendPOSTFunction, SendRequestReason, SenderPostManager, TransportType, + _ISendPostMgrConfig, _ISenderOnComplete, _eInternalMessageId, _throwInternal, _warnToConsole, arrForEach, cfgDfBoolean, cfgDfValidate, + createProcessTelemetryContext, createUniqueNamespace, dateNow, dumpObj, eLoggingSeverity, formatErrorMessageXdr, formatErrorMessageXhr, + getExceptionName, getIEVersion, isArray, isBeaconsSupported, isFetchSupported, isNullOrUndefined, mergeEvtNamespace, objExtend, + onConfigChange, parseResponse, prependTransports, runTargetUnload } from "@microsoft/applicationinsights-core-js"; import { IPromise } from "@nevware21/ts-async"; import { ITimerHandler, isNumber, isPromiseLike, isString, isTruthy, mathFloor, mathMax, mathMin, objDeepFreeze, objDefine, scheduleTimeout } from "@nevware21/ts-utils"; import { - DependencyEnvelopeCreator, EventEnvelopeCreator, ExceptionEnvelopeCreator, MetricEnvelopeCreator, PageViewEnvelopeCreator, - PageViewPerformanceEnvelopeCreator, TraceEnvelopeCreator + DependencyEnvelopeCreator, EnvelopeCreator, EventEnvelopeCreator, ExceptionEnvelopeCreator, MetricEnvelopeCreator, + PageViewEnvelopeCreator, PageViewPerformanceEnvelopeCreator, TraceEnvelopeCreator } from "./EnvelopeCreator"; import { IInternalStorageItem, ISenderConfig } from "./Interfaces"; import { ArraySendBuffer, ISendBuffer, SessionStorageSendBuffer } from "./SendBuffer"; @@ -35,6 +35,7 @@ const FetchSyncRequestSizeLimitBytes = 65000; // approx 64kb (the current Edge, interface IInternalPayloadData extends IPayloadData { oriPayload: IInternalStorageItem[]; retryCnt?: number; + statsBeatData?: IStatsBeatEvent; } @@ -78,9 +79,12 @@ const defaultAppInsightsChannelConfig: IConfigDefaults = objDeepF alwaysUseXhrOverride: cfgDfBoolean(), transports: UNDEFINED_VALUE, retryCodes: UNDEFINED_VALUE, + corsPolicy: UNDEFINED_VALUE, maxRetryCnt: {isVal: isNumber, v:10} }); +const CrossOriginResourcePolicyHeader: string = "X-Set-Cross-Origin-Resource-Policy"; + function _chkSampling(value: number) { return !isNaN(value) && value > 0 && value <= 100; } @@ -258,7 +262,6 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { let diagLog = _self.diagLog(); _evtNamespace = mergeEvtNamespace(createUniqueNamespace("Sender"), core.evtNamespace && core.evtNamespace()); _offlineListener = createOfflineListener(_evtNamespace); - // This function will be re-called whenever any referenced configuration is changed _self._addHook(onConfigChange(config, (details) => { let config = details.cfg; @@ -268,8 +271,8 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { let ctx = createProcessTelemetryContext(null, config, core); // getExtCfg only finds undefined values from core let senderConfig = ctx.getExtCfg(identifier, defaultAppInsightsChannelConfig); - let curExtUrl = senderConfig.endpointUrl; + // if it is not inital change (_endpointUrl has value) // if current sender endpoint url is not changed directly // means ExtCfg is not changed directly @@ -283,6 +286,15 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { } } + let corsPolicy = senderConfig.corsPolicy; + if (corsPolicy){ + if (corsPolicy === "same-origin" || corsPolicy === "same-site" || corsPolicy === "cross-origin") { + this.addHeader(CrossOriginResourcePolicyHeader, corsPolicy); + } + } else { + delete _headers[CrossOriginResourcePolicyHeader]; + } + if(isPromiseLike(senderConfig.instrumentationKey)) { // if it is promise, means the endpoint url is from core.endpointurl senderConfig.instrumentationKey = config.instrumentationKey as any; @@ -515,7 +527,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { if (_isStringArr(payload)) { return; } - return _xhrReadyStateChange(xhr, payload as IInternalStorageItem[],countOfItemsInPayload); + return _xhrReadyStateChange(xhr, payload as IInternalStorageItem[], countOfItemsInPayload); } @@ -563,6 +575,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { "Telemetry transmission failed, some telemetry will be lost: " + getExceptionName(e), { exception: dumpObj(e) }); } + // potential place to call countException q3 } } @@ -637,6 +650,20 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { } + function _getStatsBeat() { + let statsBeatConfig = { + ikey: _self._senderConfig.instrumentationKey, + endpoint: _endpointUrl, + version: EnvelopeCreator.Version + } as IStatsBeatConfig; + + let core = _self.core; + + // During page unload the core may have been cleared and some async events may not have been sent yet + // resulting in the core being null. In this case we don't want to create a statsbeat instance + return core ? core.getStatsBeat(statsBeatConfig) : null; + } + function _xdrOnLoad (xdr: IXDomainRequest, payload: IInternalStorageItem[]) { const responseText = _getResponseText(xdr); if (xdr && (responseText + "" === "200" || responseText === "")) { @@ -659,29 +686,60 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { try { let onCompleteFuncs = { xdrOnComplete: (xdr: IXDomainRequest, oncomplete: OnCompleteCallback,payload?: IPayloadData) => { - let data = _getPayloadArr(payload); - if (!data) { + let payloadArr = _getPayloadArr(payload); + if (!payloadArr) { return; } - return _xdrOnLoad(xdr, data); + const responseText = _getResponseText(xdr); + let statsbeat = _getStatsBeat(); + if (statsbeat) { + if (xdr && (responseText + "" === "200" || responseText === "")) { + _consecutiveErrors = 0; + statsbeat.count(200, payload, _endpointUrl); + } else { + const results = parseResponse(responseText); + + if (results && results.itemsReceived && results.itemsReceived > results.itemsAccepted + && !_isRetryDisabled) { + statsbeat.count(206, payload, _endpointUrl); + } else { + statsbeat.count(499, payload, _endpointUrl); + } + } + } + + + + return _xdrOnLoad(xdr, payloadArr); }, fetchOnComplete: (response: Response, onComplete: OnCompleteCallback, resValue?: string, payload?: IPayloadData) => { - let data = _getPayloadArr(payload); - if (!data) { + let payloadArr = _getPayloadArr(payload); + if (!payloadArr) { return; } - return _checkResponsStatus(response.status, data, response.url, data.length, response.statusText, resValue || ""); + let statsbeat = _getStatsBeat(); + if (statsbeat) { + statsbeat.count(response.status, payload, _endpointUrl); + } + return _checkResponsStatus(response.status, payloadArr, response.url, payloadArr.length, response.statusText, resValue || ""); }, xhrOnComplete: (request: XMLHttpRequest, oncomplete: OnCompleteCallback, payload?: IPayloadData) => { - let data = _getPayloadArr(payload); - if (!data) { + let payloadArr = _getPayloadArr(payload); + if (!payloadArr) { return; } - return _xhrReadyStateChange(request, data, data.length); - + let statsbeat = _getStatsBeat(); + if (statsbeat && request.readyState === 4) { + statsbeat.count(request.status, payload, _endpointUrl); + } + return _xhrReadyStateChange(request, payloadArr, payloadArr.length); }, beaconOnRetry: (data: IPayloadData, onComplete: OnCompleteCallback, canSend: (payload: IPayloadData, oncomplete: OnCompleteCallback, sync?: boolean) => boolean) => { + let statsbeat = _getStatsBeat(); + if (statsbeat) { + statsbeat.count(499, data, _endpointUrl); + } return _onBeaconRetry(data, onComplete, canSend); } @@ -781,7 +839,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { let internalPayload = payload as IInternalPayloadData; let arr = internalPayload.oriPayload; if (arr && arr.length) { - return arr + return arr; } return null; } @@ -925,9 +983,16 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { function _doSend(sendInterface: IXHROverride, payload: IInternalStorageItem[], isAsync: boolean, markAsSent: boolean = true): void | IPromise { let onComplete = (status: number, headers: {[headerName: string]: string;}, response?: string) => { + let statsbeat = _getStatsBeat(); + if (statsbeat) { + statsbeat.count(status, payloadData, _endpointUrl); + } return _getOnComplete(payload, status, headers, response); } let payloadData = _getPayload(payload); + if (payloadData) { + payloadData.statsBeatData = {startTime: dateNow()}; + } let sendPostFunc: SendPOSTFunction = sendInterface && sendInterface.sendPOST; if (sendPostFunc && payloadData) { // *********************************************************************************************** @@ -1028,7 +1093,6 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { _self._onError(payload, errorMessage); } } else { - // check if the xhr's responseURL or fetch's response.url is same as endpoint url // TODO after 10 redirects force send telemetry with 'redirect=false' as query parameter. _checkAndUpdateEndPointUrl(responseUrl); @@ -1245,12 +1309,21 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { // Using function lookups for backward compatibility as the getNotifyMgr() did not exist until after v2.5.6 function _getNotifyMgr() : INotificationManager { const func = "getNotifyMgr"; - if (_self.core[func]) { - return _self.core[func](); + let result: INotificationManager; + let core = _self.core; + if (core) { + // During page unload the core may have been cleared and some async events may not have been sent yet + // resulting in the core being null. In this case we don't want to create a statsbeat instance + + if (core[func]) { + result = core[func](); + } else { + // using _self.core['_notificationManager'] for backward compatibility + result = (core as any)["_notificationManager"]; + } } - // using _self.core['_notificationManager'] for backward compatibility - return _self.core["_notificationManager"]; + return result; } function _notifySendRequest(sendRequest: SendRequestReason, isAsync: boolean) { @@ -1410,7 +1483,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { /** * error handler - * @Internal + * @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) { @@ -1419,7 +1492,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { /** * partial success handler - * @Internal + * @internal * since version 3.2.0, if the payload is string[], this function is no-op (string[] is only used for backwards Compatibility) */ public _onPartialSuccess(payload: string[] | IInternalStorageItem[], results: IBackendResponse) { @@ -1428,7 +1501,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { /** * success handler - * @Internal + * @internal * since version 3.2.0, if the payload is string[], this function is no-op (string[] is only used for backwards Compatibility) */ public _onSuccess(payload: string[] | IInternalStorageItem[], countOfItemsInPayload: number) { diff --git a/common/config/rush/npm-shrinkwrap.json b/common/config/rush/npm-shrinkwrap.json index 6ae4cff99..7f14d9349 100644 --- a/common/config/rush/npm-shrinkwrap.json +++ b/common/config/rush/npm-shrinkwrap.json @@ -183,9 +183,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", "peer": true, "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -210,9 +210,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "peer": true, "dependencies": { "@eslint/object-schema": "^2.1.6", @@ -235,6 +235,15 @@ "node": "*" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", @@ -248,9 +257,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", - "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "peer": true, "dependencies": { "ajv": "^6.12.4", @@ -305,9 +314,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", - "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", + "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -323,18 +332,30 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", "peer": true, "dependencies": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.13.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -402,42 +423,42 @@ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@microsoft/api-extractor": { - "version": "7.51.1", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.51.1.tgz", - "integrity": "sha512-VoFvIeYXme8QctXDkixy1KIn750kZaFy2snAEOB3nhDFfbBcJNEcvBrpCIQIV09MqI4g9egKUkg+/12WMRC77w==", + "version": "7.52.3", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.52.3.tgz", + "integrity": "sha512-QEs6l8h7p9eOSHrQ9NBBUZhUuq+j/2QKcRgigbSs2YQepKz8glvsqmsUOp+nvuaY60ps7KkpVVYQCj81WLoMVQ==", "dependencies": { - "@microsoft/api-extractor-model": "7.30.3", + "@microsoft/api-extractor-model": "7.30.5", "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", - "@rushstack/node-core-library": "5.11.0", + "@rushstack/node-core-library": "5.13.0", "@rushstack/rig-package": "0.5.3", - "@rushstack/terminal": "0.15.0", - "@rushstack/ts-command-line": "4.23.5", + "@rushstack/terminal": "0.15.2", + "@rushstack/ts-command-line": "4.23.7", "lodash": "~4.17.15", "minimatch": "~3.0.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", - "typescript": "5.7.3" + "typescript": "5.8.2" }, "bin": { "api-extractor": "bin/api-extractor" } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.30.3", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.3.tgz", - "integrity": "sha512-yEAvq0F78MmStXdqz9TTT4PZ05Xu5R8nqgwI5xmUmQjWBQ9E6R2n8HB/iZMRciG4rf9iwI2mtuQwIzDXBvHn1w==", + "version": "7.30.5", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.5.tgz", + "integrity": "sha512-0ic4rcbcDZHz833RaTZWTGu+NpNgrxVNjVaor0ZDUymfDFzjA/Uuk8hYziIUIOEOSTfmIQqyzVwlzxZxPe7tOA==", "dependencies": { "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", - "@rushstack/node-core-library": "5.11.0" + "@rushstack/node-core-library": "5.13.0" } }, "node_modules/@microsoft/api-extractor/node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -516,9 +537,9 @@ } }, "node_modules/@nevware21/ts-utils": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.8.tgz", - "integrity": "sha512-62Y1mHgSu99IK4BRKC3sxdj/uIBHy6SDof3WUd29jom2HQy8sGCUdbYtFwMOkbUS6rahkL11Eg/ImtwsQsCnyw==" + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.12.1.tgz", + "integrity": "sha512-rAoErmxI9IW5BKGp8WK1FPG6dqGmxDjArgMxAf+It/+z8FQ5y9d/yDcPNNBfwY6QGpHqSaHRHFt083+L9uh7eg==" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -1404,9 +1425,9 @@ } }, "node_modules/@rushstack/node-core-library": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.11.0.tgz", - "integrity": "sha512-I8+VzG9A0F3nH2rLpPd7hF8F7l5Xb7D+ldrWVZYegXM6CsKkvWc670RlgK3WX8/AseZfXA/vVrh0bpXe2Y2UDQ==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.13.0.tgz", + "integrity": "sha512-IGVhy+JgUacAdCGXKUrRhwHMTzqhWwZUI+qEPcdzsb80heOw0QPbhhoVsoiMF7Klp8eYsp7hzpScMXmOa3Uhfg==", "dependencies": { "ajv": "~8.13.0", "ajv-draft-04": "~1.0.0", @@ -1451,11 +1472,11 @@ } }, "node_modules/@rushstack/terminal": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.0.tgz", - "integrity": "sha512-vXQPRQ+vJJn4GVqxkwRe+UGgzNxdV8xuJZY2zem46Y0p3tlahucH9/hPmLGj2i9dQnUBFiRnoM9/KW7PYw8F4Q==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.2.tgz", + "integrity": "sha512-7Hmc0ysK5077R/IkLS9hYu0QuNafm+TbZbtYVzCMbeOdMjaRboLKrhryjwZSRJGJzu+TV1ON7qZHeqf58XfLpA==", "dependencies": { - "@rushstack/node-core-library": "5.11.0", + "@rushstack/node-core-library": "5.13.0", "supports-color": "~8.1.1" }, "peerDependencies": { @@ -1468,11 +1489,11 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "4.23.5", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.5.tgz", - "integrity": "sha512-jg70HfoK44KfSP3MTiL5rxsZH7X1ktX3cZs9Sl8eDu1/LxJSbPsh0MOFRC710lIuYYSgxWjI5AjbCBAl7u3RxA==", + "version": "4.23.7", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.7.tgz", + "integrity": "sha512-Gr9cB7DGe6uz5vq2wdr89WbVDKz0UeuFEn5H2CfWDe7JvjFFaiV15gi6mqDBTbHhHCWS7w8mF1h3BnIfUndqdA==", "dependencies": { - "@rushstack/terminal": "0.15.0", + "@rushstack/terminal": "0.15.2", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" @@ -1620,9 +1641,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" }, "node_modules/@types/file-saver": { "version": "2.0.7", @@ -1730,9 +1751,9 @@ "integrity": "sha512-II+C1wgzUia0g+tGAH+PBb4XiTm8/C/i6sN23r21NNskBYOYrv+qnW0tFQ/IxZzKVwrK4CTglf8YO3poJUclQA==" }, "node_modules/@types/react": { - "version": "16.14.62", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.62.tgz", - "integrity": "sha512-BWf7hqninZav6nerxXj+NeZT/mTpDeG6Lk2zREHAy63CrnXoOGPGtNqTFYFN/sqpSaREDP5otVV88axIXmKfGA==", + "version": "16.14.63", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.63.tgz", + "integrity": "sha512-s83gano0fRBVEw3ejdLpjgvU83F0LIeeuXqdxfPZF/Sc2bhr60tEqCK1zZ+aLirBwRSD6V5zCtOsEjcwKow3JQ==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "^0.16", @@ -1785,16 +1806,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz", - "integrity": "sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz", + "integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==", "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/type-utils": "8.26.0", - "@typescript-eslint/utils": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/type-utils": "8.30.1", + "@typescript-eslint/utils": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1814,15 +1835,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.0.tgz", - "integrity": "sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz", + "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/typescript-estree": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/typescript-estree": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "debug": "^4.3.4" }, "engines": { @@ -1838,13 +1859,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz", - "integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", + "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", "peer": true, "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0" + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1855,13 +1876,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz", - "integrity": "sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz", + "integrity": "sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==", "peer": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.26.0", - "@typescript-eslint/utils": "8.26.0", + "@typescript-eslint/typescript-estree": "8.30.1", + "@typescript-eslint/utils": "8.30.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -1878,9 +1899,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.0.tgz", - "integrity": "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", + "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1891,13 +1912,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz", - "integrity": "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", + "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", "peer": true, "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1953,15 +1974,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.0.tgz", - "integrity": "sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", + "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/typescript-estree": "8.26.0" + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/typescript-estree": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1976,12 +1997,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz", - "integrity": "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", + "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", "peer": true, "dependencies": { - "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/types": "8.30.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2015,9 +2036,9 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "peer": true, "bin": { "acorn": "bin/acorn" @@ -2450,9 +2471,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001701", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001701.tgz", - "integrity": "sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==", + "version": "1.0.30001714", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz", + "integrity": "sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==", "funding": [ { "type": "opencollective", @@ -2898,9 +2919,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.5.110", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.110.tgz", - "integrity": "sha512-/p/OvOm6AfLtQteAHTUWwf+Vhh76PlluagzQlSnxMoOJ4R6SmAScWBrVev6rExJoUhP9zudN9+lBxoYUEmC1HQ==" + "version": "1.5.137", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz", + "integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA==" }, "node_modules/emoji-regex-xs": { "version": "1.0.0", @@ -2968,17 +2989,18 @@ } }, "node_modules/eslint": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", - "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", + "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.0", "@eslint/core": "^0.12.0", - "@eslint/eslintrc": "^3.3.0", - "@eslint/js": "9.21.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.24.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2990,7 +3012,7 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", + "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", @@ -3054,9 +3076,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "peer": true, "dependencies": { "esrecurse": "^4.3.0", @@ -3518,19 +3540,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6560,9 +6569,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "peer": true, "engines": { "node": ">=18.12" @@ -6653,9 +6662,9 @@ } }, "node_modules/typedoc/node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", "bin": { "yaml": "bin.mjs" }, diff --git a/extensions/applicationinsights-analytics-js/Tests/Unit/src/AnalyticsExtensionSize.tests.ts b/extensions/applicationinsights-analytics-js/Tests/Unit/src/AnalyticsExtensionSize.tests.ts index d72053af7..f61505caf 100644 --- a/extensions/applicationinsights-analytics-js/Tests/Unit/src/AnalyticsExtensionSize.tests.ts +++ b/extensions/applicationinsights-analytics-js/Tests/Unit/src/AnalyticsExtensionSize.tests.ts @@ -51,7 +51,7 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class AnalyticsExtensionSizeCheck extends AITestClass { - private readonly MAX_DEFLATE_SIZE = 24; + private readonly MAX_DEFLATE_SIZE = 25; private readonly rawFilePath = "../dist/es5/applicationinsights-analytics-js.min.js"; private readonly prodFilePaath = "../browser/es5/applicationinsights-analytics-js.min.js" diff --git a/extensions/applicationinsights-cfgsync-js/Tests/Unit/src/cfgsynchelper.tests.ts b/extensions/applicationinsights-cfgsync-js/Tests/Unit/src/cfgsynchelper.tests.ts index 5aafcea69..0884aa9a4 100644 --- a/extensions/applicationinsights-cfgsync-js/Tests/Unit/src/cfgsynchelper.tests.ts +++ b/extensions/applicationinsights-cfgsync-js/Tests/Unit/src/cfgsynchelper.tests.ts @@ -1,9 +1,8 @@ import { AITestClass, Assert } from "@microsoft/ai-test-framework"; import { NonOverrideCfg } from "../../../src/Interfaces/ICfgSyncConfig"; -import { AppInsightsCore, CdnFeatureMode, FeatureOptInMode, IAppInsightsCore, IConfiguration, IFeatureOptIn, IFeatureOptInDetails, INotificationManager, IPlugin, ITelemetryItem, PerfManager } from "@microsoft/applicationinsights-core-js"; +import { ICookieMgrConfig, AppInsightsCore, CdnFeatureMode, FeatureOptInMode, IAppInsightsCore, IConfiguration, IFeatureOptIn, IFeatureOptInDetails, INotificationManager, IPlugin, ITelemetryItem, PerfManager } from "@microsoft/applicationinsights-core-js"; import { IConfig, IStorageBuffer } from "@microsoft/applicationinsights-common"; import { resolveCdnFeatureCfg, replaceByNonOverrideCfg, applyCdnfeatureCfg } from "../../../src/CfgSyncHelperFuncs"; -import { ICookieMgrConfig } from "@microsoft/applicationinsights-core-js/src/applicationinsights-core-js"; import { ICfgSyncCdnConfig } from "../../../src/Interfaces/ICfgSyncCdnConfig"; export class CfgSyncHelperTests extends AITestClass { @@ -102,7 +101,10 @@ export class CfgSyncHelperTests extends AITestClass { extensions:[{isFlushInvoked:false,isTearDownInvoked:false,isResumeInvoked:false,isPauseInvoked:false,identifier:"Sender",priority:1001}], channels:[], extensionConfig:{}, - enableDebug: false + _sdk: { + stats: false + }, + enableDebug: false, } let core = new AppInsightsCore(); diff --git a/extensions/applicationinsights-dependencies-js/src/DependencyListener.ts b/extensions/applicationinsights-dependencies-js/src/DependencyListener.ts index 5685df318..6f648a081 100644 --- a/extensions/applicationinsights-dependencies-js/src/DependencyListener.ts +++ b/extensions/applicationinsights-dependencies-js/src/DependencyListener.ts @@ -63,7 +63,7 @@ export interface IDependencyListenerDetails { /** * The function that will get called when the ajax request is about to occur. */ -export declare type DependencyListenerFunction = (dependencyDetails: IDependencyListenerDetails) => void; +export declare type DependencyListenerFunction = (dependencyDetails: IDependencyListenerDetails) => boolean | void; export interface IDependencyHandler { remove(): void; @@ -73,8 +73,9 @@ export interface IDependencyListenerHandler extends IDependencyHandler { export interface IDependencyListenerContainer { /** - * Add an ajax listener which is called just prior to the request being sent and before the correlation headers are added, to allow you - * to access the headers and modify the values used to generate the distributed tracing correlation headers. (added in v2.8.4) + * Add an ajax listener which is called just prior to the request being sent and before the correlation headers are added. + * This allows you to access the headers and modify the values used to generate the distributed tracing correlation headers (added in v2.8.4), + * or to drop the correlation (added in v3.3.7). * @param dependencyListener - The Telemetry Initializer function * @returns - A IDependencyListenerHandler to enable the initializer to be removed */ diff --git a/extensions/applicationinsights-dependencies-js/src/ajax.ts b/extensions/applicationinsights-dependencies-js/src/ajax.ts index cdefe90e1..8fb7ac66f 100644 --- a/extensions/applicationinsights-dependencies-js/src/ajax.ts +++ b/extensions/applicationinsights-dependencies-js/src/ajax.ts @@ -244,8 +244,9 @@ function _processDependencyContainer(core: IAppInsightsCo return result; } -function _processDependencyListeners(listeners: _IInternalDependencyHandler[], core: IAppInsightsCore, ajaxData: ajaxRecord, xhr: XMLHttpRequest, input?: Request | string, init?: RequestInit): void { +function _processDependencyListeners(listeners: _IInternalDependencyHandler[], core: IAppInsightsCore, ajaxData: ajaxRecord, xhr: XMLHttpRequest, input?: Request | string, init?: RequestInit): boolean { var initializersCount = listeners.length; + let result = true; if (initializersCount > 0) { let details: IDependencyListenerDetails = { core: core, @@ -259,13 +260,15 @@ function _processDependencyListeners(listeners: _IInternalDependencyHandler { // Test Hook to allow the overriding of the location host let currentWindowHost = _self["_currentWindowHost"] || _currentWindowHost; - - _processDependencyListeners(_dependencyListeners, _self.core, ajaxData, xhr, input, init); - - if (input || input === "") { // Fetch - if (correlationIdCanIncludeCorrelationHeader(_extensionConfig, ajaxData.getAbsoluteUrl(), currentWindowHost)) { - if (!init) { - init = {}; - } - - // init headers override original request headers - // so, if they exist use only them, otherwise use request's because they should have been applied in the first place - // not using original request headers will result in them being lost - let headers = new Headers(init.headers || (input instanceof Request ? (input.headers || {}) : {})); - if (_isUsingAIHeaders) { - const id = "|" + ajaxData.traceID + "." + ajaxData.spanID; - headers.set(RequestHeaders[eRequestHeaders.requestIdHeader], id); - if (_enableRequestHeaderTracking) { - ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.requestIdHeader]] = id; - } - } - const appId: string = _appId ||(_context && _context.appId()); - if (appId) { - headers.set(RequestHeaders[eRequestHeaders.requestContextHeader], RequestHeaders[eRequestHeaders.requestContextAppIdFormat] + appId); - if (_enableRequestHeaderTracking) { - ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.requestContextHeader]] = RequestHeaders[eRequestHeaders.requestContextAppIdFormat] + appId; - } - } - if (_isUsingW3CHeaders) { - let traceFlags = ajaxData.traceFlags; - if (isNullOrUndefined(traceFlags)) { - traceFlags = 0x01; - } - - const traceParent = formatTraceParent(createTraceParent(ajaxData.traceID, ajaxData.spanID, traceFlags)); - headers.set(RequestHeaders[eRequestHeaders.traceParentHeader], traceParent); - if (_enableRequestHeaderTracking) { - ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.traceParentHeader]] = traceParent; + + if (_processDependencyListeners(_dependencyListeners, _self.core, ajaxData, xhr, input, init)) { + if (input || input === "") { // Fetch + if (correlationIdCanIncludeCorrelationHeader(_extensionConfig, ajaxData.getAbsoluteUrl(), currentWindowHost)) { + if (!init) { + init = {}; } - } - init.headers = headers; - } - - return init; - } else if (xhr) { // XHR - if (correlationIdCanIncludeCorrelationHeader(_extensionConfig, ajaxData.getAbsoluteUrl(), currentWindowHost)) { - if (_isUsingAIHeaders) { - if (!_isHeaderSet(xhr, RequestHeaders[eRequestHeaders.requestIdHeader])) { + // init headers override original request headers + // so, if they exist use only them, otherwise use request's because they should have been applied in the first place + // not using original request headers will result in them being lost + let headers = new Headers(init.headers || (input instanceof Request ? (input.headers || {}) : {})); + if (_isUsingAIHeaders) { const id = "|" + ajaxData.traceID + "." + ajaxData.spanID; - xhr.setRequestHeader(RequestHeaders[eRequestHeaders.requestIdHeader], id); + headers.set(RequestHeaders[eRequestHeaders.requestIdHeader], id); if (_enableRequestHeaderTracking) { ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.requestIdHeader]] = id; } - } else { - _throwInternalWarning(_self, _eInternalMessageId.FailedMonitorAjaxSetRequestHeader, - "Unable to set [" + RequestHeaders[eRequestHeaders.requestIdHeader] + "] as it has already been set by another instance"); } - } - const appId = _appId || (_context && _context.appId()); - if (appId) { - if (!_isHeaderSet(xhr, RequestHeaders[eRequestHeaders.requestContextHeader])) { - xhr.setRequestHeader(RequestHeaders[eRequestHeaders.requestContextHeader], RequestHeaders[eRequestHeaders.requestContextAppIdFormat] + appId); + const appId: string = _appId ||(_context && _context.appId()); + if (appId) { + headers.set(RequestHeaders[eRequestHeaders.requestContextHeader], RequestHeaders[eRequestHeaders.requestContextAppIdFormat] + appId); if (_enableRequestHeaderTracking) { ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.requestContextHeader]] = RequestHeaders[eRequestHeaders.requestContextAppIdFormat] + appId; } - } else { - _throwInternalWarning(_self, _eInternalMessageId.FailedMonitorAjaxSetRequestHeader, - "Unable to set [" + RequestHeaders[eRequestHeaders.requestContextHeader] + "] as it has already been set by another instance"); - } - } - if (_isUsingW3CHeaders) { - let traceFlags = ajaxData.traceFlags; - if (isNullOrUndefined(traceFlags)) { - traceFlags = 0x01; } + if (_isUsingW3CHeaders) { + let traceFlags = ajaxData.traceFlags; + if (isNullOrUndefined(traceFlags)) { + traceFlags = 0x01; + } - if (!_isHeaderSet(xhr, RequestHeaders[eRequestHeaders.traceParentHeader])) { const traceParent = formatTraceParent(createTraceParent(ajaxData.traceID, ajaxData.spanID, traceFlags)); - xhr.setRequestHeader(RequestHeaders[eRequestHeaders.traceParentHeader], traceParent); + headers.set(RequestHeaders[eRequestHeaders.traceParentHeader], traceParent); if (_enableRequestHeaderTracking) { ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.traceParentHeader]] = traceParent; } - } else { - _throwInternalWarning(_self, _eInternalMessageId.FailedMonitorAjaxSetRequestHeader, - "Unable to set [" + RequestHeaders[eRequestHeaders.traceParentHeader] + "] as it has already been set by another instance"); } + + init.headers = headers; } - } - return xhr; + return init; + } else if (xhr) { // XHR + if (correlationIdCanIncludeCorrelationHeader(_extensionConfig, ajaxData.getAbsoluteUrl(), currentWindowHost)) { + if (_isUsingAIHeaders) { + if (!_isHeaderSet(xhr, RequestHeaders[eRequestHeaders.requestIdHeader])) { + const id = "|" + ajaxData.traceID + "." + ajaxData.spanID; + xhr.setRequestHeader(RequestHeaders[eRequestHeaders.requestIdHeader], id); + if (_enableRequestHeaderTracking) { + ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.requestIdHeader]] = id; + } + } else { + _throwInternalWarning(_self, _eInternalMessageId.FailedMonitorAjaxSetRequestHeader, + "Unable to set [" + RequestHeaders[eRequestHeaders.requestIdHeader] + "] as it has already been set by another instance"); + } + } + const appId = _appId || (_context && _context.appId()); + if (appId) { + if (!_isHeaderSet(xhr, RequestHeaders[eRequestHeaders.requestContextHeader])) { + xhr.setRequestHeader(RequestHeaders[eRequestHeaders.requestContextHeader], RequestHeaders[eRequestHeaders.requestContextAppIdFormat] + appId); + if (_enableRequestHeaderTracking) { + ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.requestContextHeader]] = RequestHeaders[eRequestHeaders.requestContextAppIdFormat] + appId; + } + } else { + _throwInternalWarning(_self, _eInternalMessageId.FailedMonitorAjaxSetRequestHeader, + "Unable to set [" + RequestHeaders[eRequestHeaders.requestContextHeader] + "] as it has already been set by another instance"); + } + } + if (_isUsingW3CHeaders) { + let traceFlags = ajaxData.traceFlags; + if (isNullOrUndefined(traceFlags)) { + traceFlags = 0x01; + } + + if (!_isHeaderSet(xhr, RequestHeaders[eRequestHeaders.traceParentHeader])) { + const traceParent = formatTraceParent(createTraceParent(ajaxData.traceID, ajaxData.spanID, traceFlags)); + xhr.setRequestHeader(RequestHeaders[eRequestHeaders.traceParentHeader], traceParent); + if (_enableRequestHeaderTracking) { + ajaxData.requestHeaders[RequestHeaders[eRequestHeaders.traceParentHeader]] = traceParent; + } + } else { + _throwInternalWarning(_self, _eInternalMessageId.FailedMonitorAjaxSetRequestHeader, + "Unable to set [" + RequestHeaders[eRequestHeaders.traceParentHeader] + "] as it has already been set by another instance"); + } + } + } + + return xhr; + } } return undefined; diff --git a/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts b/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts index bac557272..d2fa9d3ac 100644 --- a/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts +++ b/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts @@ -51,7 +51,7 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class FileSizeCheckTest extends AITestClass { - private readonly MAX_BUNDLE_SIZE = 68; + private readonly MAX_BUNDLE_SIZE = 69; private readonly MAX_DEFLATE_SIZE = 29; private readonly bundleFilePath = "../bundle/es5/ms.core.min.js"; diff --git a/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts index a624e8008..c8da7b41d 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts @@ -51,8 +51,8 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class AppInsightsCoreSizeCheck extends AITestClass { - private readonly MAX_RAW_SIZE = 65; - private readonly MAX_BUNDLE_SIZE = 65; + private readonly MAX_RAW_SIZE = 67; + private readonly MAX_BUNDLE_SIZE = 67; private readonly MAX_RAW_DEFLATE_SIZE = 28; private readonly MAX_BUNDLE_DEFLATE_SIZE = 28; private readonly rawFilePath = "../dist/es5/applicationinsights-core-js.min.js"; diff --git a/shared/AppInsightsCore/Tests/Unit/src/StatsBeat.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/StatsBeat.Tests.ts new file mode 100644 index 000000000..dafedc25f --- /dev/null +++ b/shared/AppInsightsCore/Tests/Unit/src/StatsBeat.Tests.ts @@ -0,0 +1,280 @@ +import { Assert, AITestClass } from "@microsoft/ai-test-framework"; +import { AppInsightsCore, IAppInsightsCore, IConfiguration, IPlugin, ITelemetryItem } from "../../../src/applicationinsights-core-js"; +import { Statsbeat } from "../../../src/JavaScriptSDK/StatsBeat"; +import { IPayloadData } from "../../../src/JavaScriptSDK.Interfaces/IXHROverride"; +import * as sinon from "sinon"; +const STATS_COLLECTION_SHORT_INTERVAL: number = 900000; // 15 minutes + +export class StatsBeatTests extends AITestClass { + private _core: AppInsightsCore; + private _config: IConfiguration; + private _statsbeat: Statsbeat; + private _trackSpy: sinon.SinonSpy; + + constructor(emulateIe: boolean) { + super("StatsBeatTests", emulateIe); + } + + public testInitialize() { + let _self = this; + super.testInitialize(); + + _self._config = { + instrumentationKey: "Test-iKey", + disableInstrumentationKeyValidation: true, + _sdk: { + stats: true // Enable statsbeat by default + } + }; + + _self._core = new AppInsightsCore(); + _self._statsbeat = new Statsbeat(); + + // Create spy for tracking telemetry + _self._trackSpy = this.sandbox.spy(_self._core, "track"); + } + + public testCleanup() { + super.testCleanup(); + this._core = null; + this._statsbeat = null; + } + + public registerTests() { + + this.testCase({ + name: "StatsBeat: Initialization", + test: () => { + // Test with no initialization + Assert.equal(false, this._statsbeat.isInitialized(), "StatsBeat should not be initialized by default"); + + // Initialize and test + this._statsbeat.initialize(this._core, { + ikey: "Test-iKey", + endpoint: "https://example.endpoint.com", + version: "1.0.0" + }); + Assert.equal(true, this._statsbeat.isInitialized(), "StatsBeat should be initialized after initialization"); + } + }); + + this.testCase({ + name: "StatsBeat: count method tracks request metrics", + useFakeTimers: true, + test: () => { + // Initialize StatsBeat + this._statsbeat.initialize(this._core, { + ikey: "Test-iKey", + endpoint: "https://example.endpoint.com", + version: "1.0.0" + }); + + // Create mock payload data with timing information + const payloadData = { + urlString: "https://example.endpoint.com", + data: "testData", + headers: {}, + timeout: 0, + disableXhrSync: false, + statsBeatData: { + startTime: "2023-10-01T00:00:00Z" // Simulated start time + } + } as IPayloadData; + + // Test successful request + this._statsbeat.count(200, payloadData, "https://example.endpoint.com"); + + // Test failed request + this._statsbeat.count(500, payloadData, "https://example.endpoint.com"); + + // Test throttled request + this._statsbeat.count(429, payloadData, "https://example.endpoint.com"); + + // Verify that trackStatsbeats is called when the timer fires + this.clock.tick(STATS_COLLECTION_SHORT_INTERVAL + 1); + + // Verify that track was called + Assert.ok(this._trackSpy.called, "track should be called when statsbeat timer fires"); + + // When the timer fires, multiple metrics should be sent + Assert.ok(this._trackSpy.callCount >= 3, "Multiple metrics should be tracked"); + } + }); + + this.testCase({ + name: "StatsBeat: countException method tracks exceptions", + useFakeTimers: true, + test: () => { + // Initialize StatsBeat + this._statsbeat.initialize(this._core, { + ikey: "Test-iKey", + endpoint: "https://example.endpoint.com", + version: "1.0.0" + }); + + // Count an exception + this._statsbeat.countException("https://example.endpoint.com", "NetworkError"); + + // Verify that trackStatsbeats is called when the timer fires + this.clock.tick(STATS_COLLECTION_SHORT_INTERVAL + 1); + + // Verify that track was called + Assert.ok(this._trackSpy.called, "track should be called when statsbeat timer fires"); + + // Check that exception metrics are tracked + let foundExceptionMetric = false; + for (let i = 0; i < this._trackSpy.callCount; i++) { + const call = this._trackSpy.getCall(i); + const item: ITelemetryItem = call.args[0]; + if (item.baseData && + item.baseData.properties && + item.baseData.properties.exceptionType === "NetworkError") { + foundExceptionMetric = true; + break; + } + } + + Assert.ok(foundExceptionMetric, "Exception metrics should be tracked"); + } + }); + + this.testCase({ + name: "StatsBeat: does not send metrics for different endpoints", + useFakeTimers: true, + test: () => { + // Initialize StatsBeat for a specific endpoint + this._statsbeat.initialize(this._core, { + ikey: "Test-iKey", + endpoint: "https://example.endpoint.com", + version: "1.0.0" + }); + + // Create mock payload data + const payloadData = { + urlString: "https://example.endpoint.com", + data: "testData", + headers: {}, + timeout: 0, + disableXhrSync: false, + statsBeatData: { + startTime: Date.now() + } + } as IPayloadData; + + // Set up spies to check internal calls + const countSpy = this.sandbox.spy(this._statsbeat, "count"); + + // Count metrics for a different endpoint + this._statsbeat.count(200, payloadData, "https://different.endpoint.com"); + + // Verify that trackStatsbeats is called when the timer fires + this.clock.tick(STATS_COLLECTION_SHORT_INTERVAL + 1); + // The count method was called, but it should return early + Assert.equal(1, countSpy.callCount, "count method should be called"); + Assert.equal(0, this._trackSpy.callCount, "track should not be called for different endpoint"); + } + }); + + this.testCase({ + name: "StatsBeat: test dynamic configuration changes", + useFakeTimers: true, + test: () => { + // Setup core with statsbeat enabled + this._core.initialize(this._config, [new ChannelPlugin()]); + + // Verify that statsbeat is created + const statsbeat = this._core.getStatsBeat(); + Assert.ok(statsbeat, "Statsbeat should be created"); + + this._core.config._sdk.stats = false; + this.clock.tick(1); // Allow time for config changes to propagate + + // Verify that statsbeat is removed + const updatedStatsbeat = this._core.getStatsBeat(); + Assert.ok(!updatedStatsbeat, "Statsbeat should be removed when disabled"); + + // Re-enable statsbeat + this._core.config._sdk.stats = true; + this.clock.tick(1); // Allow time for config changes to propagate + + // Verify that statsbeat is created again + const reenabledStatsbeat = this._core.getStatsBeat(); + Assert.ok(reenabledStatsbeat, "Statsbeat should be recreated when re-enabled"); + + // Test that statsbeat is not created when disabled with undefined + this._core.config._sdk.stats = undefined; + this.clock.tick(1); // Allow time for config changes to propagate + + // Verify that statsbeat is removed + Assert.ok(!this._core.getStatsBeat(), "Statsbeat should be removed when disabled"); + + // Re-enable statsbeat + this._core.config._sdk.stats = true; + this.clock.tick(1); // Allow time for config changes to propagate + + // Verify that statsbeat is created again + Assert.ok( this._core.getStatsBeat(), "Statsbeat should be recreated when re-enabled"); + + // Test that statsbeat is not created when disabled with null value + this._core.config._sdk.stats = null; + this.clock.tick(1); // Allow time for config changes to propagate + + // Verify that statsbeat is removed + Assert.ok(!this._core.getStatsBeat(), "Statsbeat should be removed when disabled"); + } + }); + } +} + +class ChannelPlugin implements IPlugin { + public isFlushInvoked = false; + public isTearDownInvoked = false; + public isResumeInvoked = false; + public isPauseInvoked = false; + + public identifier = "Sender"; + public priority: number = 1001; + + constructor() { + this.processTelemetry = this._processTelemetry.bind(this); + } + + public pause(): void { + this.isPauseInvoked = true; + } + + public resume(): void { + this.isResumeInvoked = true; + } + + public teardown(): void { + this.isTearDownInvoked = true; + } + + flush(async?: boolean, callBack?: () => void): void { + this.isFlushInvoked = true; + if (callBack) { + callBack(); + } + } + + public processTelemetry(env: ITelemetryItem) {} + + setNextPlugin(next: any) { + // no next setup + } + + public initialize = (config: IConfiguration, core: IAppInsightsCore, plugin: IPlugin[]) => { + } + + private _processTelemetry(env: ITelemetryItem) { + } +} + +class CustomTestError extends Error { + constructor(message = "") { + super(message); + this.name = "CustomTestError"; + this.message = message + " -- test error."; + } +} \ No newline at end of file diff --git a/shared/AppInsightsCore/Tests/Unit/src/aiunittests.ts b/shared/AppInsightsCore/Tests/Unit/src/aiunittests.ts index 6fc31aebc..8a1f8bbdb 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/aiunittests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/aiunittests.ts @@ -11,6 +11,7 @@ import { UpdateConfigTests } from "./UpdateConfig.Tests"; import { EventsDiscardedReasonTests } from "./EventsDiscardedReason.Tests"; import { W3cTraceParentTests } from "./W3cTraceParentTests"; import { DynamicConfigTests } from "./DynamicConfig.Tests"; +import { StatsBeatTests } from './StatsBeat.Tests'; export function runTests() { new GlobalTestHooks().registerTests(); @@ -26,4 +27,6 @@ export function runTests() { new UpdateConfigTests().registerTests(); new EventsDiscardedReasonTests().registerTests(); new W3cTraceParentTests().registerTests(); + // new StatsBeatTests(false).registerTests(); + // new StatsBeatTests(true).registerTests(); } diff --git a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IAppInsightsCore.ts b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IAppInsightsCore.ts index fbb62ba7c..80893b388 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IAppInsightsCore.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IAppInsightsCore.ts @@ -16,6 +16,7 @@ import { INotificationListener } from "./INotificationListener"; import { INotificationManager } from "./INotificationManager"; import { IPerfManagerProvider } from "./IPerfManager"; import { IProcessTelemetryContext } from "./IProcessTelemetryContext"; +import { IStatsBeat, IStatsBeatConfig } from "./IStatsBeat"; import { ITelemetryInitializerHandler, TelemetryInitializerFunction } from "./ITelemetryInitializers"; import { ITelemetryItem } from "./ITelemetryItem"; import { IPlugin, ITelemetryPlugin } from "./ITelemetryPlugin"; @@ -124,6 +125,8 @@ export interface IAppInsightsCore = objDeepFreeze({ [STR_EXTENSION_CONFIG]: { ref: true, v: {} }, [STR_CREATE_PERF_MGR]: UNDEFINED_VALUE, loggingLevelConsole: eLoggingSeverity.DISABLED, - diagnosticLogInterval: UNDEFINED_VALUE + diagnosticLogInterval: UNDEFINED_VALUE, + _sdk: cfgDfMerge({ + stats: false + }) }); /** @@ -275,6 +281,7 @@ export class AppInsightsCore im let _logger: IDiagnosticLogger; let _eventQueue: ITelemetryItem[]; let _notificationManager: INotificationManager | null | undefined; + let _statsBeat: IStatsBeat | null; let _perfManager: IPerfManager | null; let _cfgPerfManager: IPerfManager | null; let _cookieManager: ICookieMgr | null; @@ -348,121 +355,18 @@ export class AppInsightsCore im // This will be "re-run" if the referenced config properties are changed _addUnloadHook(_configHandler.watch((details) => { let rootCfg = details.cfg; - - let isPending = _activeStatus === eActiveStatus.PENDING; - - if (isPending){ - // means waiting for previous promises to be resolved, won't apply new changes - return; - } _initInMemoMaxSize = rootCfg.initInMemoMaxSize || maxInitQueueSize; - // app Insights core only handle ikey and endpointurl, aisku will handle cs - let ikey = rootCfg.instrumentationKey; - let endpointUrl = rootCfg.endpointUrl; // do not need to validate endpoint url, if it is null, default one will be set by sender - - if (isNullOrUndefined(ikey)) { - _instrumentationKey = null; - // if new ikey is null, set status to be inactive, all new events will be saved in memory or dropped - _activeStatus = ActiveStatus.INACTIVE; - let msg = "Please provide instrumentation key"; - - if (!_isInitialized) { - // only throw error during initialization - throwError(msg); - } else { - _throwInternal(_logger, eLoggingSeverity.CRITICAL, _eInternalMessageId.InvalidInstrumentationKey, msg); - _releaseQueues(); - } - return; - - } - - let promises: IPromise[] = []; - if (isPromiseLike(ikey)) { - promises.push(ikey); - _instrumentationKey = null; // reset current local ikey variable (otherwise it will always be the previous ikeys if timeout is called before promise cb) - } else { - // string - _instrumentationKey = ikey; - } - - if (isPromiseLike(endpointUrl)) { - promises.push(endpointUrl); - _endpoint = null; // reset current local endpoint variable (otherwise it will always be the previous urls if timeout is called before promise cb) - } else { - // string or null - _endpoint = endpointUrl; - } - - // at least have one promise - if (promises.length) { - // reset to false for new dynamic changes - _isStatusSet = false; - _activeStatus = eActiveStatus.PENDING; - let initTimeout = isNotNullOrUndefined(rootCfg.initTimeOut)? rootCfg.initTimeOut : maxInitTimeout; // rootCfg.initTimeOut could be 0 - let allPromises = createSyncAllSettledPromise(promises); - _initTimer = scheduleTimeout(() => { - // set _isStatusSet to true - // set active status - // release queues - _initTimer = null; - if (!_isStatusSet) { - _setStatus(); - } - - }, initTimeout); - - doAwaitResponse(allPromises, (response) => { - try { - if (_isStatusSet) { - // promises take too long to resolve, ignore them - // active status should be set by timeout already - return; - } - - if (!response.rejected) { - let values = response.value; - if (values && values.length) { - // ikey - let ikeyRes = values[0]; - _instrumentationKey = ikeyRes && ikeyRes.value; - - // endpoint - if (values.length > 1) { - let endpointRes = values[1]; - _endpoint = endpointRes && endpointRes.value; - - } - - } - if (_instrumentationKey) { - // if ikey is null, no need to trigger extra dynamic changes for extensions - config.instrumentationKey = _instrumentationKey; // set config.instrumentationKey for extensions to consume - config.endpointUrl = _endpoint; // set config.endpointUrl for extensions to consume - } - - } - - // set _isStatusSet to true - // set active status - // release queues - _setStatus(); - - } catch (e) { - if (!_isStatusSet){ - _setStatus(); - } - } + + // uncomment this until throttle is implemented + // if (config._sdk.stats === true){ + // _statsBeat = _statsBeat || new Statsbeat(); + // } else { + // _statsBeat = null; + // } - }); - } else { - // means no promises - _setStatus(); - - } + _handleIKeyEndpointPromises(rootCfg); - //_instrumentationKey = details.cfg.instrumentationKey; // Mark the extensionConfig and all first level keys as referenced // This is so that calls to getExtCfg() will always return the same object // Even when a user may "re-assign" the plugin properties (or it's unloaded/reloaded) @@ -470,8 +374,6 @@ export class AppInsightsCore im objForEachKey(extCfg, (key) => { details.ref(extCfg, key); }); - - })); _notificationManager = notificationManager; @@ -612,6 +514,18 @@ export class AppInsightsCore im return _perfManager || _cfgPerfManager || getGblPerfMgr(); }; + _self.getStatsBeat = (statsBeatConfig?: IStatsBeatConfig): IStatsBeat => { + // create a new statsbeat if not initialize yet or the endpoint is different + // otherwise, return the existing one, or null + + // uncomment this until throttle is implemented + // if (statsBeatConfig && this.config._sdk.stats === true && _statsBeat && _statsBeat.getEndpoint() !== statsBeatConfig.endpoint) { + // _statsBeat = new Statsbeat(); + // _statsBeat.initialize(this, statsBeatConfig); + // } + return _statsBeat; + }; + _self.setPerfMgr = (perfMgr: IPerfManager) => { _perfManager = perfMgr; }; @@ -647,6 +561,124 @@ export class AppInsightsCore im return _startLogPoller(true); }; + function _handleIKeyEndpointPromises(theConfig: IConfiguration) { + // app Insights core only handle ikey and endpointurl, aisku will handle cs + // But we want to reference these config values so that if any future changes are made + // this will trigger the re-run of the watch function + // and the ikey and endpointUrl will be set to the new values + let ikey = theConfig.instrumentationKey; + let endpointUrl = theConfig.endpointUrl; // do not need to validate endpoint url, if it is null, default one will be set by sender + + // Check if we are waiting for previous promises to be resolved, won't apply new changes + if (_activeStatus !== eActiveStatus.PENDING) { + if (isNullOrUndefined(ikey)) { + _instrumentationKey = null; + + // if new ikey is null, set status to be inactive, all new events will be saved in memory or dropped + _activeStatus = ActiveStatus.INACTIVE; + let msg = "Please provide instrumentation key"; + + if (!_isInitialized) { + // only throw error during initialization + throwError(msg); + } else { + _throwInternal(_logger, eLoggingSeverity.CRITICAL, _eInternalMessageId.InvalidInstrumentationKey, msg); + _releaseQueues(); + } + + return; + } + + let promises: IPromise[] = []; + if (isPromiseLike(ikey)) { + promises.push(ikey); + _instrumentationKey = null; // reset current local ikey variable (otherwise it will always be the previous ikeys if timeout is called before promise cb) + } else { + // string + _instrumentationKey = ikey; + } + + if (isPromiseLike(endpointUrl)) { + promises.push(endpointUrl); + _endpoint = null; // reset current local endpoint variable (otherwise it will always be the previous urls if timeout is called before promise cb) + } else { + // string or null + _endpoint = endpointUrl; + } + + // at least have one promise + if (promises.length) { + _waitForInitPromises(theConfig, promises); + } else { + // means no promises + _setStatus(); + } + } + } + + function _waitForInitPromises(theConfig: IConfiguration, promises: IPromise[]) { + // reset to false for new dynamic changes + _isStatusSet = false; + _activeStatus = eActiveStatus.PENDING; + let initTimeout = isNotNullOrUndefined(theConfig.initTimeOut)? theConfig.initTimeOut : maxInitTimeout; // theConfig.initTimeOut could be 0 + let allPromises = createSyncAllSettledPromise(promises); + + if (_initTimer) { + // Stop any previous timer + _initTimer.cancel(); + } + + _initTimer = scheduleTimeout(() => { + // set _isStatusSet to true + // set active status + // release queues + _initTimer = null; + if (!_isStatusSet) { + _setStatus(); + } + }, initTimeout); + + doAwaitResponse(allPromises, (response) => { + try { + if (_isStatusSet) { + // promises take too long to resolve, ignore them + // active status should be set by timeout already + return; + } + + if (!response.rejected) { + let values = response.value; + if (values && values.length) { + // ikey + let ikeyRes = values[0]; + _instrumentationKey = ikeyRes && ikeyRes.value; + + // endpoint + if (values.length > 1) { + let endpointRes = values[1]; + _endpoint = endpointRes && endpointRes.value; + } + } + + if (_instrumentationKey) { + // if ikey is null, no need to trigger extra dynamic changes for extensions + theConfig.instrumentationKey = _instrumentationKey; // set config.instrumentationKey for extensions to consume + theConfig.endpointUrl = _endpoint; // set config.endpointUrl for extensions to consume + } + } + + // set _isStatusSet to true + // set active status + // release queues + _setStatus(); + } catch (e) { + if (!_isStatusSet){ + _setStatus(); + } + } + }); + } + function _setStatus() { _isStatusSet = true; if (isNullOrUndefined(_instrumentationKey)) { @@ -1012,6 +1044,7 @@ export class AppInsightsCore im runTargetUnload(_notificationManager, false); _notificationManager = null; _perfManager = null; + _statsBeat = null; _cfgPerfManager = null; runTargetUnload(_cookieManager, false); _cookieManager = null; @@ -1424,6 +1457,11 @@ export class AppInsightsCore im return null; } + public getStatsBeat(statsBeatConfig?: IStatsBeatConfig): IStatsBeat { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + return null; + } + public setPerfMgr(perfMgr: IPerfManager) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/NetworkStatsbeat.ts b/shared/AppInsightsCore/src/JavaScriptSDK/NetworkStatsbeat.ts new file mode 100644 index 000000000..a7dcdc835 --- /dev/null +++ b/shared/AppInsightsCore/src/JavaScriptSDK/NetworkStatsbeat.ts @@ -0,0 +1,22 @@ +export interface NetworkStatsbeat { + host: string; + totalRequest: number; + success: number; + throttle: Record; + failure: Record; + retry: Record; + exception: Record; + requestDuration: number; +} +export function createNetworkStatsbeat(host: string): NetworkStatsbeat { + return { + host, + totalRequest: 0, + success: 0, + throttle: {}, + failure: {}, + retry: {}, + exception: {}, + requestDuration: 0 + }; +} diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts b/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts index f27d1da09..40d1d5be9 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts @@ -249,7 +249,7 @@ export class SenderPostManager { } else { // if can send - _onSuccess(STR_EMPTY, oncomplete); + _onSuccess(STR_EMPTY, oncomplete); // if success, onComplete is called with status code 200 } } @@ -429,12 +429,14 @@ export class SenderPostManager { } - function _handleError(res?: string) { + function _handleError(res?: string, statusCode?: number) { // In case there is an error in the request. Set the status to 0 for 1ds and 400 for appInsights // so that the events can be retried later. - - _doOnComplete(oncomplete, _isOneDs? 0 : 400, {}, _isOneDs? STR_EMPTY: res); - + if (statusCode) { + _doOnComplete(oncomplete, _isOneDs? 0 : statusCode, {}, _isOneDs? STR_EMPTY: res); + } else { + _doOnComplete(oncomplete, _isOneDs? 0 : 400, {}, _isOneDs? STR_EMPTY: res); + } } function _onFetchComplete(response: Response, payload?: IPayloadData, value?: string) { @@ -468,7 +470,11 @@ export class SenderPostManager { */ if (!_isOneDs && !response.ok) { // this is for appInsights only - _handleError(response.statusText); + if (response.status){ + _handleError(response.statusText, response.status); + } else { + _handleError(response.statusText, 499); + } resolveFunc && resolveFunc(false); } else { if (_isOneDs && !response.body) { @@ -484,19 +490,23 @@ export class SenderPostManager { } } catch (e) { - _handleError(dumpObj(e)); + if (response && response.status){ + _handleError(dumpObj(e), response.status); + } else { + _handleError(dumpObj(e), 499); + } rejectFunc && rejectFunc(e); } } else { - _handleError(result.reason && result.reason.message); + _handleError(result.reason && result.reason.message, 499); rejectFunc && rejectFunc(result.reason); } } }); } catch (e) { if (!responseHandled) { - _handleError(dumpObj(e)); + _handleError(dumpObj(e), 499); rejectFunc && rejectFunc(e); } } diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/StatsBeat.ts b/shared/AppInsightsCore/src/JavaScriptSDK/StatsBeat.ts new file mode 100644 index 000000000..97ffd8d9c --- /dev/null +++ b/shared/AppInsightsCore/src/JavaScriptSDK/StatsBeat.ts @@ -0,0 +1,203 @@ +import dynamicProto from "@microsoft/dynamicproto-js"; +import { ITimerHandler, scheduleTimeout, utcNow } from "@nevware21/ts-utils"; +import { IAppInsightsCore } from "../JavaScriptSDK.Interfaces/IAppInsightsCore"; +import { IStatsBeat, IStatsBeatConfig } from "../JavaScriptSDK.Interfaces/IStatsBeat"; +import { ITelemetryItem } from "../JavaScriptSDK.Interfaces/ITelemetryItem"; +import { IPayloadData } from "../JavaScriptSDK.Interfaces/IXHROverride"; +import { NetworkStatsbeat, createNetworkStatsbeat } from "./NetworkStatsbeat"; + +const INSTRUMENTATION_KEY = "c4a29126-a7cb-47e5-b348-11414998b11e"; +const STATS_COLLECTION_SHORT_INTERVAL: number = 900000; // 15 minutes +const STATSBEAT_LANGUAGE = "JavaScript"; +const STATSBEAT_TYPE = "Browser"; + +export class Statsbeat implements IStatsBeat { + constructor() { + let _networkCounter: NetworkStatsbeat; + let _isEnabled: boolean = false; + let _core: IAppInsightsCore; + let _timeoutHandle: ITimerHandler; // Handle to the timer for sending telemetry. This way, we would not send telemetry when system sleep. + // Custom dimensions + let _cikey: string; + let _language: string; + let _sdkVersion: string; + let _os: string; + dynamicProto(Statsbeat, this, (_self, _base) => { + _self.initialize = (core: IAppInsightsCore, statsBeatConfig: IStatsBeatConfig) => { + _core = core; + _networkCounter = createNetworkStatsbeat(statsBeatConfig.endpoint); + _isEnabled = true; + _sdkVersion = statsBeatConfig.version; + _getCustomProperties(statsBeatConfig.ikey); + } + + _self.isInitialized = (): boolean => { + return !!_isEnabled; + } + + _self.count = (status: number, payloadData: IPayloadData, endpoint: string) => { + if (!_isEnabled || !_checkEndpoint(endpoint)) { + return; + } + if (payloadData && payloadData["statsBeatData"] && payloadData["statsBeatData"]["startTime"]) { + _networkCounter.totalRequest = (_networkCounter.totalRequest || 0) + 1; + _networkCounter.requestDuration += utcNow() - payloadData["statsBeatData"]["startTime"]; + } + let retryArray = [401, 403, 408, 429, 500, 502, 503, 504]; + let throttleArray = [402, 439]; + if (status >= 200 && status < 300) { + _networkCounter.success++; + } else if (retryArray.indexOf(status) !== -1) { + _networkCounter.retry[status] = (_networkCounter.retry[status] || 0) + 1; + } else if (throttleArray.indexOf(status) !== -1) { + _networkCounter.throttle[status] = (_networkCounter.throttle[status] || 0) + 1; + } else if (status !== 307 && status !== 308) { + _networkCounter.failure[status] = (_networkCounter.failure[status] || 0) + 1; + } + _setupTimer(); + }; + + _self.getEndpoint = (): string => { + return _networkCounter?_networkCounter.host:null; + } + + _self.countException = (endpoint: string, exceptionType: string) => { + if (!_isEnabled || !_checkEndpoint(endpoint)) { + return; + } + _networkCounter.exception[exceptionType] = (_networkCounter.exception[exceptionType] || 0) + 1; + _setupTimer(); + } + + function _setupTimer() { + if (!_timeoutHandle) { + _timeoutHandle = scheduleTimeout(() => { + _timeoutHandle = null; + trackStatsbeats(); + }, STATS_COLLECTION_SHORT_INTERVAL); + } + } + + function trackStatsbeats(){ + _trackSendRequestDuration(); + _trackSendRequestsCount(); + _networkCounter = createNetworkStatsbeat(_networkCounter.host); + _timeoutHandle && _timeoutHandle.cancel(); + _timeoutHandle = null; + } + + function _checkEndpoint(endpoint: string) { + return _networkCounter && _networkCounter.host === endpoint; + } + + function _getCustomProperties(ikey: string) { + _cikey = ikey; + _language = STATSBEAT_LANGUAGE; + _os = STATSBEAT_TYPE; + } + + function _sendStatsbeats(name: string, val: number, properties?: { [name: string]: any }) { + if (!val || val <= 0){ + return; + } + // Add extra properties + let baseProperties = { + "rp": "unknown", + "attach": "Manual", + "cikey": _cikey, + "os": _os, + "language": _language, + "version": _sdkVersion, + "endpoint": "breeze", + "host": _networkCounter.host + } as { [key: string]: any }; + + // Manually merge properties instead of using spread syntax + let combinedProps: { [key: string]: any } = { "host": _networkCounter.host }; + + // Add properties if present + if (properties) { + for (let key in properties) { + if (properties.hasOwnProperty(key)) { + combinedProps[key] = properties[key]; + } + } + } + // Add base properties + for (let key in baseProperties) { + if (baseProperties.hasOwnProperty(key)) { + combinedProps[key] = baseProperties[key]; + } + } + let statsbeatEvent: ITelemetryItem = { + iKey: INSTRUMENTATION_KEY, + name: name, + baseData: { + name: name, + average: val, + properties: combinedProps + }, + baseType: "MetricData" + }; + _core.track(statsbeatEvent); + } + + function _trackSendRequestDuration() { + var totalRequest = _networkCounter.totalRequest; + + if (_networkCounter.totalRequest > 0 ) { + let averageRequestExecutionTime = _networkCounter.requestDuration / totalRequest; + _sendStatsbeats("Request_Duration", averageRequestExecutionTime); + } + } + + function _trackSendRequestsCount() { + var currentCounter = _networkCounter; + _sendStatsbeats("Request_Success_Count", currentCounter.success); + + for (const code in currentCounter.failure) { + const count = currentCounter.failure[code]; + _sendStatsbeats("failure", count, { statusCode: code }); + } + + for (const code in currentCounter.retry) { + const count = currentCounter.retry[code]; + _sendStatsbeats("retry", count, { statusCode: code }); + } + + for (const code in currentCounter.exception) { + const count = currentCounter.exception[code]; + _sendStatsbeats("exception", count, { exceptionType: code }); + } + + for (const code in currentCounter.throttle) { + const count = currentCounter.throttle[code]; + _sendStatsbeats("Throttle_Count", count, { statusCode: code }); + } + } + }); + } + + public initialize(core: IAppInsightsCore, statsBeatConfig: IStatsBeatConfig) { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } + + public isInitialized(): boolean { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + return false; + } + + public count(status: number, payloadData: IPayloadData, endpoint: string) { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } + + public countException(endpoint: string, exceptionType: string) { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } + + public getEndpoint(): string { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + return null; + } + +} diff --git a/shared/AppInsightsCore/src/applicationinsights-core-js.ts b/shared/AppInsightsCore/src/applicationinsights-core-js.ts index 78cc7151f..6af27edca 100644 --- a/shared/AppInsightsCore/src/applicationinsights-core-js.ts +++ b/shared/AppInsightsCore/src/applicationinsights-core-js.ts @@ -36,6 +36,8 @@ export { parseResponse } from "./JavaScriptSDK/ResponseHelpers"; export { IXDomainRequest, IBackendResponse } from "./JavaScriptSDK.Interfaces/IXDomainRequest"; export { _ISenderOnComplete, _ISendPostMgrConfig, _ITimeoutOverrideWrapper, _IInternalXhrOverride } from "./JavaScriptSDK.Interfaces/ISenderPostManager"; export { SenderPostManager } from "./JavaScriptSDK/SenderPostManager"; +export { IStatsBeatEvent } from "./JavaScriptSDK.Interfaces/IStatsBeatEvent"; +export { IStatsBeat, IStatsBeatConfig } from "./JavaScriptSDK.Interfaces/IStatsBeat"; export { isArray, isTypeof, isUndefined, isNullOrUndefined, objHasOwnProperty as hasOwnProperty, isObject, isFunction, strEndsWith, strStartsWith, isDate, isError, isString, isNumber, isBoolean, arrForEach, arrIndexOf, diff --git a/tools/chrome-debug-extension/manifest.json b/tools/chrome-debug-extension/manifest.json index 9b234f5a8..9de91a8a1 100644 --- a/tools/chrome-debug-extension/manifest.json +++ b/tools/chrome-debug-extension/manifest.json @@ -2,8 +2,8 @@ "name": "Telemetry Viewer - M3", "short_name": "Telemetry Viewer M3", "description": "A browser extension that provides a real time view of what's happening in Application Insights including what telemetry is being logged by the web application", - "version": "0.7.5", - "version_name": "0.7.5", + "version": "0.8.0", + "version_name": "0.8.0", "manifest_version": 3, "icons": { "16": "images/icon-16.png", diff --git a/tools/chrome-debug-extension/package.json b/tools/chrome-debug-extension/package.json index 2adc08d3b..895fafbed 100644 --- a/tools/chrome-debug-extension/package.json +++ b/tools/chrome-debug-extension/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/applicationinsights-chrome-debug-extension", - "version": "0.7.6", + "version": "0.8.0", "description": "A chrome based browser extension that provides a real time view of what's happening in Application Insights including what telemetry is being logged by the web application", "homepage": "https://github.com/microsoft/ApplicationInsights-JS/tree/main/tools/chrome-debug-extension#readme", "keywords": [ diff --git a/tools/github-page-script-injection/injectScript.js b/tools/github-page-script-injection/injectScript.js index 47dbea078..be1a15e4b 100644 --- a/tools/github-page-script-injection/injectScript.js +++ b/tools/github-page-script-injection/injectScript.js @@ -31,8 +31,8 @@ let scriptContent = fs.readFileSync(scriptFilePath, 'utf8'); // Replace the placeholder string with the actual connection string //const connectionString = 'InstrumentationKey=1ae9e7ce-18f1-4e14-8fc0-acbf0ed28895;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/'; let iKeyForCfgSync = "1ae9e7ce-18f1-4e14-8fc0-acbf0ed28895"; -//let cfgUrl = "https://js.monitor.azure.com/beta/ai.config.1.cfg.json"; -let cfgUrl = "https://js.monitor.azure.com/nightly/ai_test.config.1-nightly3.cfg.json"; +let cfgUrl = "https://js.monitor.azure.com/beta/ai.config.1.cfg.json"; +// let cfgUrl = "https://js.monitor.azure.com/nightly/ai_test.config.1-nightly3.cfg.json"; scriptContent = scriptContent.replace(`connectionString: "YOUR_CONNECTION_STRING"`, `instrumentationKey: "${iKeyForCfgSync}",\n \texpCfg: {\n\t\tinclScripts: true,\n\t\t\n\t\tmaxLogs: 100},\n \t\textensionConfig: {\n\t\t\t"AppInsightsCfgSyncPlugin": {\n\t\t\t\tcfgUrl:"${cfgUrl}"\n\t\t\t}\n\t\t}`); //scriptContent = scriptContent.replace('YOUR_CONNECTION_STRING', connectionString); scriptContent = ``; diff --git a/version.json b/version.json index a1922a127..bd8c458ca 100644 --- a/version.json +++ b/version.json @@ -58,7 +58,7 @@ }, "@microsoft/applicationinsights-chrome-debug-extension": { "package": "tools/chrome-debug-extension/package.json", - "release": "0.7.6" + "release": "0.8.0" }, "applicationinsights-web-config": { "package": "tools/config/package.json", From 6e315f1920912356a4f0d17a947bda35b56fd8b4 Mon Sep 17 00:00:00 2001 From: siyuniu-ms <123212536+siyuniu-ms@users.noreply.github.com> Date: Wed, 30 Apr 2025 15:37:47 -0700 Subject: [PATCH 17/72] [main] enable compress api in 1ds-post-channel and applicationinsights-channel (#2451) --- AISKU/Tests/Unit/src/AISKUSize.Tests.ts | 8 +- AISKU/Tests/Unit/src/CdnThrottle.tests.ts | 45 ++++- AISKU/src/AISku.ts | 4 +- .../Tests/Unit/src/AISKULightSize.Tests.ts | 4 +- .../Tests/Unit/src/dynamicconfig.tests.ts | 90 +++++++++- AISKULight/package.json | 3 +- AISKULight/src/index.ts | 9 +- README.md | 34 +++- channels/1ds-post-js/README.md | 1 + channels/1ds-post-js/src/HttpManager.ts | 39 ++-- .../test/Unit/src/PostChannelTest.ts | 116 +++++++++++- .../Tests/Unit/src/Sender.tests.ts | 103 ++++++++++- .../src/Sender.ts | 20 ++- common/config/rush/npm-shrinkwrap.json | 167 +++++++++--------- shared/1ds-core-js/src/Index.ts | 2 +- .../Tests/Unit/src/HelperFunc.Tests.ts | 32 ++-- .../src/JavaScriptSDK/HelperFuncs.ts | 14 +- .../src/JavaScriptSDK/SenderPostManager.ts | 86 ++++++++- tools/config/config.json | 3 + 19 files changed, 630 insertions(+), 150 deletions(-) diff --git a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts index 638a76ce8..1ad6ccc32 100644 --- a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts +++ b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts @@ -54,10 +54,10 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class AISKUSizeCheck extends AITestClass { - private readonly MAX_RAW_SIZE = 146; - private readonly MAX_BUNDLE_SIZE = 146; - private readonly MAX_RAW_DEFLATE_SIZE = 58; - private readonly MAX_BUNDLE_DEFLATE_SIZE = 58; + private readonly MAX_RAW_SIZE = 147; + private readonly MAX_BUNDLE_SIZE = 147; + private readonly MAX_RAW_DEFLATE_SIZE = 59; + private readonly MAX_BUNDLE_DEFLATE_SIZE = 59; private readonly rawFilePath = "../dist/es5/applicationinsights-web.min.js"; // Automatically updated by version scripts private readonly currentVer = "3.3.6"; diff --git a/AISKU/Tests/Unit/src/CdnThrottle.tests.ts b/AISKU/Tests/Unit/src/CdnThrottle.tests.ts index 64c916068..91b14669f 100644 --- a/AISKU/Tests/Unit/src/CdnThrottle.tests.ts +++ b/AISKU/Tests/Unit/src/CdnThrottle.tests.ts @@ -135,11 +135,14 @@ export class CdnThrottle extends AITestClass { offCfg: { "throttleMgrCfg.106.disabled":true, "throttleMgrCfg.109.disabled":true, - } - }}, + }}, + ["zipPayload"]: { + mode: CdnFeatureMode.enable}, + }, config: { maxMessageLimit: 10, - throttleMgrCfg: throttleCfgDisable + throttleMgrCfg: throttleCfgDisable, + } } as ICfgSyncConfig; doc["res"] = new (doc as any).Response(JSON.stringify(cdnCfg), { @@ -245,6 +248,42 @@ export class CdnThrottle extends AITestClass { }, "response received", 60, 1000) as any) }); + this.testCaseAsync({ + name: "CfgSyncPlugin: customer didn't set feature opt in, successfully get aisku default and fetch from config url, get disable zip config to be true", + stepDelay: 10, + useFakeTimers: true, + steps: [ () => { + let doc = getGlobal(); + hookFetch((resolve) => { // global instance cannot access test private instance + AITestClass.orgSetTimeout(function() { + resolve( doc["res2"]); + }, 0); + }); + + let noSetconfig = { + instrumentationKey: TestInstrumentationKey, + extensionConfig : {["AppInsightsCfgSyncPlugin"] : { + syncMode: ICfgSyncMode.Receive, + cfgUrl: "testurl" + }} + }; + + this.fetchStub = this.sandbox.spy((doc as any), "fetch"); + this.init = new ApplicationInsights({ + config: noSetconfig, + }); + this.init.loadAppInsights(); + this._ai = this.init; + }].concat(PollingAssert.createPollingAssert(() => { + if (this.fetchStub.called){ + let newCfg = this._ai.config; + Assert.equal(newCfg.featureOptIn["zipPayload"]["mode"], FeatureOptInMode.enable); // aisku default is none, overwrite to true by cdn config + return true; + } + return false; + }, "response received", 60, 1000) as any) + }); + this.testCaseAsync({ name: "CfgSyncPlugin: customer set throttle config, new config fetch from config url could overwrite original one", stepDelay: 10, diff --git a/AISKU/src/AISku.ts b/AISKU/src/AISku.ts index 4efcf6eef..51b82a78e 100644 --- a/AISKU/src/AISku.ts +++ b/AISKU/src/AISku.ts @@ -50,6 +50,7 @@ const _ignoreUpdateSnippetProperties = [ const IKEY_USAGE = "iKeyUsage"; const CDN_USAGE = "CdnUsage"; const SDK_LOADER_VER = "SdkLoaderVer"; +const ZIP_PAYLOAD = "zipPayload"; const UNDEFINED_VALUE: undefined = undefined; @@ -80,7 +81,8 @@ const defaultConfigValues: IConfigDefaults = { featureOptIn:{ [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} + [SDK_LOADER_VER]: {mode: FeatureOptInMode.disable}, + [ZIP_PAYLOAD]: {mode: FeatureOptInMode.none} }, throttleMgrCfg: cfgDfMerge<{[key:number]: IThrottleMgrConfig}>( { diff --git a/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts b/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts index ab5e74f0b..f82ac6fb7 100644 --- a/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts +++ b/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts @@ -51,8 +51,8 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class AISKULightSizeCheck extends AITestClass { - private readonly MAX_RAW_SIZE = 92; - private readonly MAX_BUNDLE_SIZE = 92; + private readonly MAX_RAW_SIZE = 93; + private readonly MAX_BUNDLE_SIZE = 93; private readonly MAX_RAW_DEFLATE_SIZE = 38; private readonly MAX_BUNDLE_DEFLATE_SIZE = 38; private readonly rawFilePath = "../dist/es5/applicationinsights-web-basic.min.js"; diff --git a/AISKULight/Tests/Unit/src/dynamicconfig.tests.ts b/AISKULight/Tests/Unit/src/dynamicconfig.tests.ts index e6088b34f..e596a2577 100644 --- a/AISKULight/Tests/Unit/src/dynamicconfig.tests.ts +++ b/AISKULight/Tests/Unit/src/dynamicconfig.tests.ts @@ -1,9 +1,9 @@ import { AITestClass, Assert, PollingAssert } from "@microsoft/ai-test-framework"; import { IConfig } from "@microsoft/applicationinsights-common"; -import { IConfiguration, isString, newId } from "@microsoft/applicationinsights-core-js"; +import { IConfiguration, IPayloadData, isString, ITelemetryItem, IXHROverride, newId } from "@microsoft/applicationinsights-core-js"; import { ApplicationInsights, ISenderConfig } from "../../../src/index"; import { createAsyncResolvedPromise } from "@nevware21/ts-async"; - +import { SinonSpy } from 'sinon'; export class ApplicationInsightsDynamicConfigTests extends AITestClass { private static readonly _instrumentationKey = "b7170927-2d1c-44f1-acec-59f4e1751c11"; private static readonly _connectionString = `InstrumentationKey=${ApplicationInsightsDynamicConfigTests._instrumentationKey}`; @@ -11,8 +11,9 @@ export class ApplicationInsightsDynamicConfigTests extends AITestClass { private _sessionPrefix: string = newId(); private _config: IConfiguration & IConfig; static registerTests: any; + private genericSpy: SinonSpy; private _ctx: any; - + private xhrOverride: IXHROverride; constructor(testName?: string) { super(testName || "AISKU Dynamic Config"); } @@ -30,6 +31,7 @@ export class ApplicationInsightsDynamicConfigTests extends AITestClass { this._ai = new ApplicationInsights(this._config); this._ctx = {}; + this.xhrOverride = new AutoCompleteXhrOverride(); } catch (e) { console.error("Failed to initialize", e); } @@ -137,6 +139,80 @@ export class ApplicationInsightsDynamicConfigTests extends AITestClass { return false; }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) }); + + this.testCaseAsync({ + name: "zip test: gzip encode is working and content-encode header is set (feature opt-in)", + stepDelay: 10, + useFakeTimers: true, + useFakeServer: true, + steps: [ + () => { + this.genericSpy = this.sandbox.spy(this.xhrOverride, 'sendPOST'); + this._ai.config.featureOptIn["zipPayload"] = { mode: 3 }; + this._ai.config.extensionConfig["AppInsightsChannelPlugin"] = { + httpXHROverride: this.xhrOverride, + alwaysUseXhrOverride: true + } + this.clock.tick(10); + const telemetryItem: ITelemetryItem = { + name: 'fake item with some really long name to take up space quickly', + iKey: 'iKey', + baseType: 'some type', + baseData: {} + }; + + this._ai.track(telemetryItem); + this._ai.flush(); + this.clock.tick(10); + }].concat(PollingAssert.createPollingAssert(() => { + if (this.genericSpy.called){ + let request = this.genericSpy.getCall(0).args[0]; + let gzipData = request.data; + QUnit.assert.ok(gzipData, "data should be set"); + QUnit.assert.equal(true, gzipData[0] === 0x1F && gzipData[1] === 0x8B, "telemetry should be gzip encoded"); + QUnit.assert.equal(request.headers["Content-Encoding"], "gzip", "telemetry should be gzip encoded"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "zip test: gzip encode will not working (feature opt-in is not set)", + stepDelay: 10, + useFakeTimers: true, + useFakeServer: true, + steps: [ + () => { + this.genericSpy = this.sandbox.spy(this.xhrOverride, 'sendPOST'); + this._ai.config.extensionConfig["AppInsightsChannelPlugin"] = { + httpXHROverride: this.xhrOverride, + alwaysUseXhrOverride: true + } + this.clock.tick(10); + const telemetryItem: ITelemetryItem = { + name: 'fake item with some really long name to take up space quickly', + iKey: 'iKey', + baseType: 'some type', + baseData: {} + }; + + this._ai.track(telemetryItem); + this._ai.flush(); + this.clock.tick(10); + }].concat(PollingAssert.createPollingAssert(() => { + if (this.genericSpy.called){ + let request = this.genericSpy.getCall(0).args[0]; + let gzipData = request.data; + QUnit.assert.ok(gzipData, "data should be set"); + QUnit.assert.equal(false, gzipData[0] === 0x1F && gzipData[1] === 0x8B, "telemetry should not be gzip encoded"); + QUnit.assert.equal(request.headers["Content-Encoding"], undefined, "telemetry should not be gzip encoded"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + } public addApiTests(): void { @@ -153,4 +229,12 @@ export class ApplicationInsightsDynamicConfigTests extends AITestClass { }); } +} + +class AutoCompleteXhrOverride { + + public sendPOST(payload: IPayloadData, oncomplete: (status: number, headers: { [headerName: string]: string }) => void, sync?: boolean) { + console.log("AutoCompleteXhrOverride.sendPOST called with payload: ", payload); + oncomplete(200, null); + } } \ No newline at end of file diff --git a/AISKULight/package.json b/AISKULight/package.json index 579dc3399..04a549600 100644 --- a/AISKULight/package.json +++ b/AISKULight/package.json @@ -50,7 +50,8 @@ "rollup-plugin-sourcemaps": "^0.6.3", "typescript": "^4.9.3", "typedoc": "^0.26.6", - "tslib": "^2.0.0" + "tslib": "^2.0.0", + "sinon": "^7.3.1" }, "peerDependencies": { "tslib": ">= 1.0.0" diff --git a/AISKULight/src/index.ts b/AISKULight/src/index.ts index 8364f61cb..903107c73 100644 --- a/AISKULight/src/index.ts +++ b/AISKULight/src/index.ts @@ -5,9 +5,9 @@ import dynamicProto from "@microsoft/dynamicproto-js"; import { Sender } from "@microsoft/applicationinsights-channel-js"; import { DEFAULT_BREEZE_PATH, IConfig, parseConnectionString } from "@microsoft/applicationinsights-common"; import { - AppInsightsCore, IConfigDefaults, IConfiguration, IDistributedTraceContext, IDynamicConfigHandler, ILoadedPlugin, IPlugin, - ITelemetryInitializerHandler, ITelemetryItem, ITelemetryPlugin, ITelemetryUnloadState, IUnloadHook, UnloadHandler, WatcherFunction, - cfgDfValidate, createDynamicConfig, onConfigChange, proxyFunctions + AppInsightsCore, FeatureOptInMode, IConfigDefaults, IConfiguration, IDistributedTraceContext, IDynamicConfigHandler, ILoadedPlugin, + IPlugin, ITelemetryInitializerHandler, ITelemetryItem, ITelemetryPlugin, ITelemetryUnloadState, IUnloadHook, UnloadHandler, + WatcherFunction, cfgDfValidate, createDynamicConfig, onConfigChange, proxyFunctions } from "@microsoft/applicationinsights-core-js"; import { IPromise, createSyncPromise, doAwaitResponse } from "@nevware21/ts-async"; import { isNullOrUndefined, isPromiseLike, isString, objDefine, throwError } from "@nevware21/ts-utils"; @@ -18,6 +18,9 @@ const defaultConfigValues: IConfigDefaults = { connectionString: UNDEFINED_VALUE, endpointUrl: UNDEFINED_VALUE, instrumentationKey: UNDEFINED_VALUE, + featureOptIn:{ + ["zipPayload"]: {mode: FeatureOptInMode.none} + }, extensionConfig: {} }; diff --git a/README.md b/README.md index d32a4bc07..57cd0ef55 100644 --- a/README.md +++ b/README.md @@ -447,11 +447,43 @@ Most configuration fields are named such that they can be defaulted to falsey. A | disableIkeyDeprecationMessage | boolean | true | [Optional] Disable instrumentation Key deprecation error message. If true, error message will NOT be sent. **Note: instrumentation key support will end soon**, see aka.ms/IkeyMigrate for more details. | bufferOverride
since 2.8.12 | IStorageBuffer | undefined | [Optional] Identifies a simple interface to allow you to override the storage mechanism used for tracking unsent and unacknowledged events, when not provided defaults to using SessionStorage interface. You MUST supply both the `getItem` and `setItem` functions when defined. | storagePrefix | string[] | undefined | [Optional] An optional value that will be added as name prefix for storage name. | -| featureOptIn
since 3.0.3 | IFeatureOptIn | undefined | [Optional] Set Feature opt in details. | +| featureOptIn (#feature)
since 3.0.3 | IFeatureOptIn | undefined | [Optional] Set Feature opt in details. | | throttleMgrCfg
since 3.0.3 | `{[key: number]: IThrottleMgrConfig}` | undefined | [Optional] Set throttle mgr configuration by key. | | retryCodes | number[] | undefined | Identifies the status codes that will cause event batches to be resent, when `null` or `undefined` the SDK will use it's defaults `[401, 408, 429, 500, 502, 503, 504]`. `403` was removed in version 3.1.1. | | expCfg
since 3.3.1| [`IExceptionConfig`](https://github.com/microsoft/ApplicationInsights-JS/blob/main/shared/AppInsightsCommon/src/Interfaces/IExceptionTelemetry.ts) | undefined | Set additional configuration for exceptions, such as more scripts to include in the exception telemetry. | +### Feature + +You can use the `featureOptIn` configuration to enable or customize specific SDK features. + +#### Available Feature Flags + +| Name | Default | Description | Note | +|-------------|---------|----------------------------------------------|------------| +| `zipPayload` | `none`*(version 3.3.7) | Enables compression using the Compression API to zip telemetry payloads. |If this feature is turned on and the CompressionStream API is available, the payload will be compressed using the CompressionStream API. Compression will only occur if the event is asynchronous. For events like unloads, compression will not be applied. Note: if user set payloadPreprocessor, this zip compression will not be applied.| + +* A default value of none means the SDK may automatically enable this feature in the future. To explicitly prevent this, set the feature to disable using FeatureOptInMode.disable. + +#### How to Enable a Feature + +To enable a feature such as `zipPayload`, set the `featureOptIn` property in the SDK configuration as shown below: + +```javascript +const appInsights = new ApplicationInsights({ + config: { + connectionString: "YOUR_CONNECTION_STRING", + // Other configuration options... + featureOptIn: { + zipPayload: { + mode: FeatureOptInMode.enable, // Set the opt-in status for the feature + blockCdnCfg: false, // Define whether to block changes from CDN config + } as IFeatureOptInDetails + } + } +}); +``` +See [feature opt-in status](https://microsoft.github.io/ApplicationInsights-JS/WebConfig) for more details. + ### ExtensionConfig diff --git a/channels/1ds-post-js/README.md b/channels/1ds-post-js/README.md index 2bae3c788..5f115e966 100644 --- a/channels/1ds-post-js/README.md +++ b/channels/1ds-post-js/README.md @@ -66,6 +66,7 @@ appInsightsCore.initialize(coreConfig, []); | maxEventRetryAttempts
(Since 3.1.11+) | [Optional] Identifies the number of times any single event will be retried if it receives a failed (retirable) response, this causes the event to be internally "requeued" and resent in the next batch. As each normal batched send request is retried at least once before starting to increase the internal backoff send interval, normally batched events will generally be attempted the next nearest even number of times. This means that the total number of actual send attempts will almost always be even (setting to 5 will cause 6 requests), unless using manual synchronous flushing (calling flush(false)) which is not subject to request level retry attempts. | number
Default: 6 | maxUnloadEventRetryAttempts
(Since 3.1.11+) | [Optional] Identifies the number of times any single event will be retried if it receives a failed (retriable) response as part of processing / flushing events once a page unload state has been detected, this causes the event to be internally "requeued" and resent in the next batch, which during page unload. Unlike the normal batching process, send requests are never retried, so the value listed here is always the maximum number of attempts for any single event.
Notes: The SDK by default will use the sendBeacon() API if it exists which is treated as a fire and forget successful response, so for environments that support or supply this API the events won't be retried (because they will be deeded to be successfully sent). When an environment (IE) doesn't support sendBeacon(), this will cause multiple synchronous (by default) XMLHttpRequests to be sent, which will block the UI until a response is received. You can disable ALL synchronous XHR requests by setting the 'disableXhrSync' configuration setting and/or changing this value to 0 or 1. | number
Default: 2 | addNoResponse
(Since 3.2.8+) | [Optional] flag to indicate whether the sendBeacon and fetch (with keep-alive flag) should add the "NoResponseBody" query string value to indicate that the server should return a 204 for successful requests. | boolean
Default: true +| disableZip
(Since 4.3.7+) | [Optional] flag to use CompressionStream API to compress the payload. Compression will only occur if the event is asynchronous. For events like unloads, compression will not be applied. * Note: if user set payloadPreprocessor, this zip compression will not be applied. | boolean
Default: true ### [IXHROverride](https://microsoft.github.io/ApplicationInsights-JS/webSdk/1ds-post-js/interfaces/IXHROverride.html) diff --git a/channels/1ds-post-js/src/HttpManager.ts b/channels/1ds-post-js/src/HttpManager.ts index a27121ede..9c88dc790 100644 --- a/channels/1ds-post-js/src/HttpManager.ts +++ b/channels/1ds-post-js/src/HttpManager.ts @@ -9,10 +9,10 @@ import { ITelemetryItem, IUnloadHook, IXDomainRequest, IXHROverride, OnCompleteCallback, SendRequestReason, SenderPostManager, TransportType, _IInternalXhrOverride, _ISendPostMgrConfig, _ISenderOnComplete, _eExtendedInternalMessageId, _eInternalMessageId, _getAllResponseHeaders, _throwInternal, _warnToConsole, arrForEach, dateNow, doPerf, dumpObj, eLoggingSeverity, extend, getCommonSchemaMetaData, getNavigator, - getResponseText, getTime, hasOwnProperty, isBeaconsSupported, isFetchSupported, isNullOrUndefined, isReactNative, isUndefined, - isValueAssigned, objForEachKey, objKeys, onConfigChange, optimizeObject, prependTransports, strUndefined + getResponseText, getTime, hasOwnProperty, isBeaconsSupported, isFeatureEnabled, isFetchSupported, isNullOrUndefined, isReactNative, + isUndefined, isValueAssigned, objForEachKey, objKeys, onConfigChange, optimizeObject, prependTransports, strUndefined } from "@microsoft/1ds-core-js"; -import { arrAppend } from "@nevware21/ts-utils"; +import { arrAppend, getInst, isFunction } from "@nevware21/ts-utils"; import { BatchNotificationAction, BatchNotificationActions } from "./BatchNotificationActions"; import { ClockSkewManager } from "./ClockSkewManager"; import { @@ -165,6 +165,7 @@ export class HttpManager { let _isUnloading: boolean; let _useHeaders: boolean; let _xhrTimeout: number; + let _zipPayload: boolean; let _disableXhrSync: boolean; let _disableFetchKeepAlive: boolean; let _canHaveReducedPayload: boolean; @@ -222,6 +223,17 @@ export class HttpManager { } _xhrTimeout = channelConfig.xhrTimeout; + + const csStream = getInst("CompressionStream"); + + // Controls whether payload compression (gzip) is enabled. + _zipPayload = isFeatureEnabled("zipPayload", coreConfig); + // if user has payload processor (_sendHook), they may compress the payload themselves + // to avoid double compression, we should disable the zipPayload + if (!isFunction(csStream) || _sendHook) { + _zipPayload = false; + } + _disableXhrSync = !!channelConfig.disableXhrSync; _disableFetchKeepAlive = !!channelConfig.disableFetchKeepAlive; _addNoResponse = channelConfig.addNoResponse !== false; @@ -969,17 +981,18 @@ export class HttpManager { }; let isSync = thePayload.isTeardown || thePayload.isSync; - try { - sendInterface.sendPOST(payload, onComplete, isSync); - if (_sendListener) { - // Send the original payload to the listener - _sendListener(orgPayloadData, payload, isSync, thePayload.isBeacon); + _sendPostMgr.preparePayload((processedPayload: IPayloadData) => { + try { + sendInterface.sendPOST(processedPayload, onComplete, isSync); + if (_sendListener) { + // Send the original payload to the listener + _sendListener(orgPayloadData, processedPayload, isSync, thePayload.isBeacon); + } + } catch (ex) { + _doOnComplete(onComplete, 0, {}); + _warnToConsole(_logger, "Unexpected exception sending payload. Ex:" + dumpObj(ex)); } - } catch (ex) { - _warnToConsole(_logger, "Unexpected exception sending payload. Ex:" + dumpObj(ex)); - - _doOnComplete(onComplete, 0, {}); - } + }, _zipPayload, payload, isSync); }; } diff --git a/channels/1ds-post-js/test/Unit/src/PostChannelTest.ts b/channels/1ds-post-js/test/Unit/src/PostChannelTest.ts index 7bed544b6..34ae31fa3 100644 --- a/channels/1ds-post-js/test/Unit/src/PostChannelTest.ts +++ b/channels/1ds-post-js/test/Unit/src/PostChannelTest.ts @@ -1,9 +1,9 @@ import { AITestClass, PollingAssert, TestHelper } from "@microsoft/ai-test-framework"; -import { IExtendedConfiguration, AppInsightsCore, EventLatency, ITelemetryItem, IExtendedTelemetryItem, SendRequestReason, EventSendType, isFetchSupported, objKeys, arrForEach, isBeaconsSupported, EventPersistence, isNullOrUndefined } from '@microsoft/1ds-core-js'; +import { IExtendedConfiguration, AppInsightsCore, EventLatency, ITelemetryItem, IExtendedTelemetryItem, SendRequestReason, EventSendType, isFetchSupported, objKeys, arrForEach, isBeaconsSupported, EventPersistence, isNullOrUndefined, getGlobal } from '@microsoft/1ds-core-js'; import { PostChannel, IXHROverride, IPayloadData } from '../../../src/Index'; import { IPostTransmissionTelemetryItem, IChannelConfiguration } from '../../../src/DataModels'; import { SinonSpy } from 'sinon'; -import { createAsyncResolvedPromise } from "@nevware21/ts-async"; +import { createAsyncResolvedPromise, IPromise } from "@nevware21/ts-async"; import { ActiveStatus } from "@microsoft/1ds-core-js"; @@ -53,6 +53,7 @@ export class PostChannelTest extends AITestClass { this.config = { instrumentationKey: 'testIkey', endpointUrl: 'https://testEndpoint', + featureOptIn : {["zipPayload"]: {mode: 1}}, extensionConfig: [] }; this.postChannel = new PostChannel(); @@ -177,7 +178,7 @@ export class PostChannelTest extends AITestClass { maxEventRetryAttempts: 6, maxUnloadEventRetryAttempts: 2, addNoResponse: undefValue, - excludeCsMetaData: undefValue + excludeCsMetaData: undefValue, }; let actaulConfig = postChannel["_getDbgPlgTargets"]()[1]; QUnit.assert.deepEqual(expectedConfig, actaulConfig, "default config should be set"); @@ -315,6 +316,115 @@ export class PostChannelTest extends AITestClass { } }); + this.testCaseAsync({ + name: "zip test: gzip encode is working and content-encode header is set", + stepDelay: 10, + useFakeTimers: true, + useFakeServer: true, + steps: [ + () => { + this.genericSpy = this.sandbox.spy(this.xhrOverride, 'sendPOST'); + this.config.featureOptIn = {["zipPayload"]: {mode: 3}}; + this.config.extensionConfig[this.postChannel.identifier] = { + httpXHROverride: this.xhrOverride, + }; + this.core.initialize(this.config, [this.postChannel]); + var event: IPostTransmissionTelemetryItem = { + name: 'testEvent', + sync: false, + latency: EventLatency.Normal, + iKey: 'testIkey' + }; + this.postChannel.processTelemetry(event); + this.postChannel.flush(); + this.clock.tick(10); + }].concat(PollingAssert.createPollingAssert(() => { + if (this.genericSpy.called){ + let request = this.genericSpy.getCall(0).args[0]; + let gzipData = request.data; + QUnit.assert.ok(gzipData, "data should be set"); + QUnit.assert.equal(true, gzipData[0] === 0x1F && gzipData[1] === 0x8B, "telemetry should be gzip encoded"); + QUnit.assert.equal(request.headers["Content-Encoding"], "gzip", "telemetry should be gzip encoded"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + + this.testCaseAsync({ + name: "zip is default to be off", + stepDelay: 10, + useFakeTimers: true, + useFakeServer: true, + steps: [ + () => { + this.genericSpy = this.sandbox.spy(this.xhrOverride, 'sendPOST'); + this.config.extensionConfig[this.postChannel.identifier] = { + httpXHROverride: this.xhrOverride + }; + this.core.initialize(this.config, [this.postChannel]); + var event: IPostTransmissionTelemetryItem = { + name: 'testEvent', + sync: false, + latency: EventLatency.Normal, + iKey: 'testIkey' + }; + this.postChannel.processTelemetry(event); + this.postChannel.flush(); + this.clock.tick(10); + }].concat(PollingAssert.createPollingAssert(() => { + if (this.genericSpy.called){ + let request = this.genericSpy.getCall(0).args[0]; + QUnit.assert.equal(request.headers["Content-Encoding"], undefined, "header should not be added"); + QUnit.assert.ok(JSON.stringify(request.data).includes("testEvent"), "telemetry should not be encoded"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "test dynamic zip config", + stepDelay: 10, + useFakeTimers: true, + useFakeServer: true, + steps: [ + () => { + this.genericSpy = this.sandbox.spy(this.xhrOverride, 'sendPOST'); + this.config.extensionConfig[this.postChannel.identifier] = { + httpXHROverride: this.xhrOverride + }; + this.core.initialize(this.config, [this.postChannel]); + var event: IPostTransmissionTelemetryItem = { + name: 'testEvent', + sync: false, + latency: EventLatency.Normal, + iKey: 'testIkey' + }; + this.postChannel.processTelemetry(event); + this.postChannel.flush(); + this.clock.tick(10); + this.core.config.featureOptIn = {["zipPayload"]: {mode: 3}}; + this.clock.tick(1); + this.core.track(event); + this.postChannel.flush(); + this.clock.tick(10); + }].concat(PollingAssert.createPollingAssert(() => { + if (this.genericSpy.callCount === 2) { + let request = this.genericSpy.getCall(0).args[0]; + QUnit.assert.equal(request.headers["Content-Encoding"], undefined, "header should not be added"); + QUnit.assert.ok(JSON.stringify(request.data).includes("testEvent"), "telemetry should not be encoded"); + let request2 = this.genericSpy.getCall(1).args[0]; + QUnit.assert.equal(request2.headers["Content-Encoding"], "gzip", "Telemetry should be gzip encoded after zipPayload is set to true"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + + this.testCaseAsync({ name: "Init: init with ikey Promise and endpointUrl Promise", stepDelay: 100, diff --git a/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts b/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts index cb7431121..7eedb0e3a 100644 --- a/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts +++ b/channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts @@ -7,7 +7,7 @@ import { ITelemetryItem, AppInsightsCore, ITelemetryPlugin, DiagnosticLogger, No import { ArraySendBuffer, SessionStorageSendBuffer } from "../../../src/SendBuffer"; import { IInternalStorageItem, ISenderConfig } from "../../../src/Interfaces"; import { createAsyncResolvedPromise } from "@nevware21/ts-async"; - +import { SinonSpy } from 'sinon'; const BUFFER_KEY = "AI_buffer_1"; @@ -16,6 +16,8 @@ export class SenderTests extends AITestClass { private _sender: Sender; private _instrumentationKey = 'iKey'; private _offline: IOfflineListener; + private genericSpy: SinonSpy; + private xhrOverride: IXHROverride; protected _getBuffer(key: string, logger: DiagnosticLogger, namePrefix?: string): IInternalStorageItem[] { let prefixedKey = key; @@ -41,6 +43,8 @@ export class SenderTests extends AITestClass { public testInitialize() { this._sender = new Sender(); this._offline = createOfflineListener("SenderTests"); + this.xhrOverride = new AutoCompleteXhrOverride(); + // Reset the cached isBeacons supported isBeaconsSupported(false); } @@ -142,7 +146,7 @@ export class SenderTests extends AITestClass { samplingPercentage: 90, customHeaders: [{header: "header1",value:"value1"}], alwaysUseXhrOverride: true, - disableSendBeaconSplit: false + disableSendBeaconSplit: false, } core.config.extensionConfig[id] = config; this.clock.tick(1); @@ -158,7 +162,6 @@ export class SenderTests extends AITestClass { QUnit.assert.deepEqual([{header: "header1",value:"value1"}], curSenderConfig.customHeaders, "Channel customHeaders config is dynamically set"); QUnit.assert.deepEqual(true, curSenderConfig.alwaysUseXhrOverride, "Channel alwaysUseXhrOverride config is dynamically set"); QUnit.assert.equal(false, curSenderConfig.disableSendBeaconSplit, "Channel disableSendBeaconSplit config is dynamically set"); - core.config.extensionConfig[this._sender.identifier].emitLineDelimitedJson = undefined; core.config.extensionConfig[this._sender.identifier].endpointUrl = undefined; this.clock.tick(1); @@ -167,6 +170,94 @@ export class SenderTests extends AITestClass { } }); + this.testCaseAsync({ + name: "zip test: gzip encode is working and content-encode header is set (feature opt-in)", + stepDelay: 10, + useFakeTimers: true, + useFakeServer: true, + steps: [ + () => { + this.genericSpy = this.sandbox.spy(this.xhrOverride, 'sendPOST'); + let core = new AppInsightsCore(); + + let coreConfig = { + instrumentationKey: "000e0000-e000-0000-a000-000000000000", + featureOptIn : {["zipPayload"]: {mode: 3}}, + extensionConfig: { + [this._sender.identifier]: { + httpXHROverride: this.xhrOverride, + alwaysUseXhrOverride: true, + } + } + } + + core.initialize(coreConfig, [this._sender]); + + const telemetryItem: ITelemetryItem = { + name: 'fake item with some really long name to take up space quickly', + iKey: 'iKey', + baseType: 'some type', + baseData: {} + }; + this._sender.processTelemetry(telemetryItem); + this._sender.flush(); + this.clock.tick(10); + }].concat(PollingAssert.createPollingAssert(() => { + if (this.genericSpy.called){ + let request = this.genericSpy.getCall(0).args[0]; + let gzipData = request.data; + QUnit.assert.ok(gzipData, "data should be set"); + QUnit.assert.equal(true, gzipData[0] === 0x1F && gzipData[1] === 0x8B, "telemetry should be gzip encoded"); + QUnit.assert.equal(request.headers["Content-Encoding"], "gzip", "telemetry should be gzip encoded"); + return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + + this.testCaseAsync({ + name: "zip test: gzip encode is disabled (feature opt-in not set)", + stepDelay: 10, + useFakeTimers: true, + useFakeServer: true, + steps: [ + () => { + this.genericSpy = this.sandbox.spy(this.xhrOverride, 'sendPOST'); + let core = new AppInsightsCore(); + + let coreConfig = { + instrumentationKey: "000e0000-e000-0000-a000-000000000000", + extensionConfig: { + [this._sender.identifier]: { + httpXHROverride: this.xhrOverride, + alwaysUseXhrOverride: true, + } + } + } + + core.initialize(coreConfig, [this._sender]); + + const telemetryItem: ITelemetryItem = { + name: 'fake item with some really long name to take up space quickly', + iKey: 'iKey', + baseType: 'some type', + baseData: {} + }; + this._sender.processTelemetry(telemetryItem); + this._sender.flush(); + this.clock.tick(10); + }].concat(PollingAssert.createPollingAssert(() => { + if (this.genericSpy.called){ + let request = this.genericSpy.getCall(0).args[0]; + let gzipData = request.data; + QUnit.assert.ok(gzipData, "data should be set"); + QUnit.assert.equal(false, gzipData[0] === 0x1F && gzipData[1] === 0x8B, "telemetry should not be gzip encoded"); + QUnit.assert.ok(!("Content-Encoding" in request.headers), "telemetry should not be gzip encoded"); return true; + } + return false; + }, "Wait for promise response" + new Date().toISOString(), 60, 1000) as any) + }); + this.testCase({ name: "Channel Config: Endpoint Url can be set from root dynamically", useFakeTimers: true, @@ -4082,4 +4173,10 @@ export class SenderTests extends AITestClass { } }); } +} + +class AutoCompleteXhrOverride { + public sendPOST(payload: IPayloadData, oncomplete: (status: number, headers: { [headerName: string]: string }) => void, sync?: boolean) { + oncomplete(200, null); + } } \ No newline at end of file diff --git a/channels/applicationinsights-channel-js/src/Sender.ts b/channels/applicationinsights-channel-js/src/Sender.ts index 4049299a0..f751198a5 100644 --- a/channels/applicationinsights-channel-js/src/Sender.ts +++ b/channels/applicationinsights-channel-js/src/Sender.ts @@ -11,12 +11,13 @@ import { IXDomainRequest, IXHROverride, OnCompleteCallback, SendPOSTFunction, SendRequestReason, SenderPostManager, TransportType, _ISendPostMgrConfig, _ISenderOnComplete, _eInternalMessageId, _throwInternal, _warnToConsole, arrForEach, cfgDfBoolean, cfgDfValidate, createProcessTelemetryContext, createUniqueNamespace, dateNow, dumpObj, eLoggingSeverity, formatErrorMessageXdr, formatErrorMessageXhr, - getExceptionName, getIEVersion, isArray, isBeaconsSupported, isFetchSupported, isNullOrUndefined, mergeEvtNamespace, objExtend, - onConfigChange, parseResponse, prependTransports, runTargetUnload + getExceptionName, getIEVersion, isArray, isBeaconsSupported, isFeatureEnabled, isFetchSupported, isNullOrUndefined, mergeEvtNamespace, + objExtend, onConfigChange, parseResponse, prependTransports, runTargetUnload } from "@microsoft/applicationinsights-core-js"; import { IPromise } from "@nevware21/ts-async"; import { - ITimerHandler, isNumber, isPromiseLike, isString, isTruthy, mathFloor, mathMax, mathMin, objDeepFreeze, objDefine, scheduleTimeout + ITimerHandler, getInst, isFunction, isNumber, isPromiseLike, isString, isTruthy, mathFloor, mathMax, mathMin, objDeepFreeze, objDefine, + scheduleTimeout } from "@nevware21/ts-utils"; import { DependencyEnvelopeCreator, EnvelopeCreator, EventEnvelopeCreator, ExceptionEnvelopeCreator, MetricEnvelopeCreator, @@ -187,6 +188,7 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { let _disableBeaconSplit: boolean; let _sendPostMgr: SenderPostManager; let _retryCodes: number[]; + let _zipPayload: boolean; dynamicProto(Sender, this, (_self, _base) => { @@ -285,7 +287,12 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { senderConfig.endpointUrl = coreUrl; } } - + const csStream = getInst("CompressionStream"); + // Determine whether to enable payload compression (zipping). + _zipPayload = isFeatureEnabled("zipPayload", config); + if (!isFunction(csStream)) { + _zipPayload = false; + } let corsPolicy = senderConfig.corsPolicy; if (corsPolicy){ if (corsPolicy === "same-origin" || corsPolicy === "same-site" || corsPolicy === "cross-origin") { @@ -411,7 +418,6 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { } else { _sendPostMgr.SetConfig(sendPostConfig); } - let customInterface = senderConfig.httpXHROverride; let httpInterface: IXHROverride = null; let syncInterface: IXHROverride = null; @@ -1001,7 +1007,9 @@ export class Sender extends BaseTelemetryPlugin implements IChannelControls { _self._buffer.markAsSent(payload); } - return sendPostFunc(payloadData, onComplete, !isAsync); + _sendPostMgr.preparePayload((processedPayload: IPayloadData) => { + return sendPostFunc(processedPayload, onComplete, !isAsync); + }, _zipPayload, payloadData, !isAsync); } return null; } diff --git a/common/config/rush/npm-shrinkwrap.json b/common/config/rush/npm-shrinkwrap.json index 7f14d9349..427cb2730 100644 --- a/common/config/rush/npm-shrinkwrap.json +++ b/common/config/rush/npm-shrinkwrap.json @@ -245,9 +245,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", "peer": true, "dependencies": { "@types/json-schema": "^7.0.15" @@ -314,9 +314,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", - "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", + "version": "9.25.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", + "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -344,18 +344,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -423,9 +411,9 @@ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@microsoft/api-extractor": { - "version": "7.52.3", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.52.3.tgz", - "integrity": "sha512-QEs6l8h7p9eOSHrQ9NBBUZhUuq+j/2QKcRgigbSs2YQepKz8glvsqmsUOp+nvuaY60ps7KkpVVYQCj81WLoMVQ==", + "version": "7.52.5", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.52.5.tgz", + "integrity": "sha512-6WWgjjg6FkoDWpF/O3sjB05OkszpI5wtKJqd8fUIR/JJUv8IqNCGr1lJUZJnc1HegcT9gAvyf98KfH0wFncU0w==", "dependencies": { "@microsoft/api-extractor-model": "7.30.5", "@microsoft/tsdoc": "~0.15.1", @@ -433,7 +421,7 @@ "@rushstack/node-core-library": "5.13.0", "@rushstack/rig-package": "0.5.3", "@rushstack/terminal": "0.15.2", - "@rushstack/ts-command-line": "4.23.7", + "@rushstack/ts-command-line": "5.0.0", "lodash": "~4.17.15", "minimatch": "~3.0.3", "resolve": "~1.22.1", @@ -537,9 +525,9 @@ } }, "node_modules/@nevware21/ts-utils": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.12.1.tgz", - "integrity": "sha512-rAoErmxI9IW5BKGp8WK1FPG6dqGmxDjArgMxAf+It/+z8FQ5y9d/yDcPNNBfwY6QGpHqSaHRHFt083+L9uh7eg==" + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.12.2.tgz", + "integrity": "sha512-wEJpAgVC9kac6mh2Oa2QIEoBy3ZgCJyl8qp8rfyT56xzRCNppYQ5nEGb58JLJA5s69U6TgkA9uq5QbQ/htmR/w==" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -1489,9 +1477,9 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "4.23.7", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.7.tgz", - "integrity": "sha512-Gr9cB7DGe6uz5vq2wdr89WbVDKz0UeuFEn5H2CfWDe7JvjFFaiV15gi6mqDBTbHhHCWS7w8mF1h3BnIfUndqdA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.0.0.tgz", + "integrity": "sha512-SW6nqZVxH26Rxz25+lJQRlnXI/YCrNH7NfDEWPPm9i0rwkSE6Rgtmzw96cuZgQjacOh0sw77d6V4SvgarAfr8g==", "dependencies": { "@rushstack/terminal": "0.15.2", "@types/argparse": "1.0.38", @@ -1806,16 +1794,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz", - "integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.0.tgz", + "integrity": "sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==", "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/type-utils": "8.30.1", - "@typescript-eslint/utils": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/type-utils": "8.31.0", + "@typescript-eslint/utils": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1835,15 +1823,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz", - "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.0.tgz", + "integrity": "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/typescript-estree": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/typescript-estree": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", "debug": "^4.3.4" }, "engines": { @@ -1859,13 +1847,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", - "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.0.tgz", + "integrity": "sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==", "peer": true, "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1" + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1876,13 +1864,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz", - "integrity": "sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.0.tgz", + "integrity": "sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==", "peer": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.30.1", - "@typescript-eslint/utils": "8.30.1", + "@typescript-eslint/typescript-estree": "8.31.0", + "@typescript-eslint/utils": "8.31.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -1899,9 +1887,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", - "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz", + "integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==", "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1912,13 +1900,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", - "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.0.tgz", + "integrity": "sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==", "peer": true, "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1974,15 +1962,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", - "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.0.tgz", + "integrity": "sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/typescript-estree": "8.30.1" + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/typescript-estree": "8.31.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1997,12 +1985,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", - "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.0.tgz", + "integrity": "sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==", "peer": true, "dependencies": { - "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/types": "8.31.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2471,9 +2459,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001714", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz", - "integrity": "sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==", + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", "funding": [ { "type": "opencollective", @@ -2919,9 +2907,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.5.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz", - "integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA==" + "version": "1.5.140", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.140.tgz", + "integrity": "sha512-o82Rj+ONp4Ip7Cl1r7lrqx/pXhbp/lh9DpKcMNscFJdh8ebyRofnc7Sh01B4jx403RI0oqTBvlZ7OBIZLMr2+Q==" }, "node_modules/emoji-regex-xs": { "version": "1.0.0", @@ -2989,19 +2977,19 @@ } }, "node_modules/eslint": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", - "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", + "version": "9.25.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz", + "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.0", - "@eslint/core": "^0.12.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.24.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/js": "9.25.1", + "@eslint/plugin-kit": "^0.2.8", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3540,6 +3528,19 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", diff --git a/shared/1ds-core-js/src/Index.ts b/shared/1ds-core-js/src/Index.ts index 4796956b8..ddb00ff1f 100644 --- a/shared/1ds-core-js/src/Index.ts +++ b/shared/1ds-core-js/src/Index.ts @@ -68,7 +68,7 @@ export { createDynamicConfig, onConfigChange, getDynamicConfigHandler, blockDynamicConversion, forceDynamicConversion, IPayloadData, IXHROverride, OnCompleteCallback, SendPOSTFunction, IInternalOfflineSupport, _ISendPostMgrConfig, IBackendResponse, _ISenderOnComplete, SenderPostManager, getResponseText, formatErrorMessageXdr, formatErrorMessageXhr, prependTransports, parseResponse, convertAllHeadersToMap, _getAllResponseHeaders, _appendHeader, _IInternalXhrOverride, - _ITimeoutOverrideWrapper, IXDomainRequest, + _ITimeoutOverrideWrapper, IXDomainRequest, isFeatureEnabled, FeatureOptInMode, TransportType, // Test Hooks diff --git a/shared/AppInsightsCore/Tests/Unit/src/HelperFunc.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/HelperFunc.Tests.ts index 2c8205b1d..f30465bca 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/HelperFunc.Tests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/HelperFunc.Tests.ts @@ -365,39 +365,39 @@ export class HelperFuncTests extends AITestClass { name: "isFeatureEnable: empty field and optInMap", test: () => { let rlt = isFeatureEnabled(); - Assert.equal(rlt, false, "feature is not enable case 1"); + Assert.equal(rlt, undefined, "feature is not enable case 1"); rlt = isFeatureEnabled(""); - Assert.equal(rlt, false, "feature is not enable case 2"); + Assert.equal(rlt, undefined, "feature is not enable case 2"); rlt = isFeatureEnabled("", {}); - Assert.equal(rlt, false, "feature is not enable case 3"); + Assert.equal(rlt, undefined, "feature is not enable case 3"); rlt = isFeatureEnabled(undefined, {}); - Assert.equal(rlt, false, "feature is not enable case 4"); + Assert.equal(rlt, undefined, "feature is not enable case 4"); rlt = isFeatureEnabled(undefined, {featureOptIn:{"field":{}}}); - Assert.equal(rlt, false, "feature is not enable case 5"); + Assert.equal(rlt, undefined, "feature is not enable case 5"); rlt = isFeatureEnabled("field"); - Assert.equal(rlt, false, "feature is not enable case 6"); + Assert.equal(rlt, undefined, "feature is not enable case 6"); rlt = isFeatureEnabled("field1", {featureOptIn:{}}); - Assert.equal(rlt, false, "feature is not enable case 7"); + Assert.equal(rlt, undefined, "feature is not enable case 7"); rlt = isFeatureEnabled("field1", {featureOptIn:{"field":{}}}); - Assert.equal(rlt, false, "feature is not enable case 8"); + Assert.equal(rlt, undefined, "feature is not enable case 8"); rlt = isFeatureEnabled("field", {featureOptIn:{"field":{}}}); - Assert.equal(rlt, false, "feature is not enable case 9"); + Assert.equal(rlt, undefined, "feature is not enable case 9"); let cfg = {featureOptIn:{}} as IConfiguration; rlt = isFeatureEnabled("field", cfg); - Assert.equal(rlt, false, "feature is not enable case 10"); + Assert.equal(rlt, undefined, "feature is not enable case 10"); cfg = {featureOptIn: {"field":{}}} as IConfiguration; rlt = isFeatureEnabled("field", cfg); - Assert.equal(rlt, false, "feature is not enable case 11"); + Assert.equal(rlt, undefined, "feature is not enable case 11"); interface IConfig { config1: string; @@ -405,7 +405,7 @@ export class HelperFuncTests extends AITestClass { } cfg = {config1: "test", featureOptIn:{}} as IConfig & IConfiguration; rlt = isFeatureEnabled("field", cfg); - Assert.equal(rlt, false, "feature is not enable case 10"); + Assert.equal(rlt, undefined, "feature is not enable case 10"); } }); @@ -425,7 +425,7 @@ export class HelperFuncTests extends AITestClass { cfg = {featureOptIn:{[field]: {mode: FeatureOptInMode.none} as IFeatureOptInDetails}}as IConfig & IConfiguration; rlt = isFeatureEnabled(field, cfg); - Assert.equal(rlt, true, "feature is enable case 2"); + Assert.equal(rlt, undefined, "feature is enable case 2"); cfg = {featureOptIn:{[field]: {mode: FeatureOptInMode.disable} as IFeatureOptInDetails}}as IConfig & IConfiguration; rlt = isFeatureEnabled(field, cfg); @@ -433,15 +433,15 @@ export class HelperFuncTests extends AITestClass { cfg = {featureOptIn:{[field]: {onCfg:{"config1": false}} as IFeatureOptInDetails}}as IConfig & IConfiguration; rlt = isFeatureEnabled("field1", cfg); - Assert.equal(rlt, false, "feature is not enable case 4"); + Assert.equal(rlt, undefined, "feature is not enable case 4"); cfg = {featureOptIn:{[field]: {mode: 100 as any} as IFeatureOptInDetails}}as IConfig & IConfiguration; rlt = isFeatureEnabled(field, cfg); - Assert.equal(rlt, false, "feature is not enable case 5"); + Assert.equal(rlt, undefined, "feature is not enable case 5"); cfg = {featureOptIn:{[field]: {mode: FeatureOptInMode.enable} as IFeatureOptInDetails}}as IConfig & IConfiguration; rlt = isFeatureEnabled("field2", cfg); - Assert.equal(rlt, false, "feature is not enable case 6"); + Assert.equal(rlt, undefined, "feature is not enable case 6"); } }); } diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/HelperFuncs.ts b/shared/AppInsightsCore/src/JavaScriptSDK/HelperFuncs.ts index 5fa40e5b6..5d37fdf91 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/HelperFuncs.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/HelperFuncs.ts @@ -9,7 +9,7 @@ import { FeatureOptInMode } from "../JavaScriptSDK.Enums/FeatureOptInEnums"; import { TransportType } from "../JavaScriptSDK.Enums/SendRequestReason"; import { IConfiguration } from "../JavaScriptSDK.Interfaces/IConfiguration"; import { IXDomainRequest } from "../JavaScriptSDK.Interfaces/IXDomainRequest"; -import { STR_EMPTY } from "./InternalConstants"; +import { STR_EMPTY, UNDEFINED_VALUE } from "./InternalConstants"; // RESTRICT and AVOID circular dependencies you should not import other contained modules or export the contents of this file directly @@ -352,15 +352,19 @@ export function objExtend(obj1?: T1 | any, obj2?: T2, ob export const asString = asString21; -export function isFeatureEnabled(feature?: string, cfg?: T): boolean { - let rlt = false; +export function isFeatureEnabled(feature?: string, cfg?: T): boolean|undefined { let ft = cfg && cfg.featureOptIn && cfg.featureOptIn[feature]; if (feature && ft) { let mode = ft.mode; // NOTE: None will be considered as true - rlt = (mode == FeatureOptInMode.enable) || (mode == FeatureOptInMode.none); + if (mode === FeatureOptInMode.enable) { + return true + } else if (mode === FeatureOptInMode.disable) { + return false; + } + return UNDEFINED_VALUE; } - return rlt; + return UNDEFINED_VALUE; } export function getResponseText(xhr: XMLHttpRequest | IXDomainRequest) { diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts b/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts index 40d1d5be9..173dbc71f 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/SenderPostManager.ts @@ -2,8 +2,8 @@ // Licensed under the MIT License. import dynamicProto from "@microsoft/dynamicproto-js"; -import { IPromise, createPromise, doAwaitResponse } from "@nevware21/ts-async"; -import { arrForEach, dumpObj, getNavigator, getWindow, isFunction, objKeys } from "@nevware21/ts-utils"; +import { AwaitResponse, IPromise, createPromise, doAwaitResponse } from "@nevware21/ts-async"; +import { arrForEach, dumpObj, getInst, getNavigator, getWindow, isFunction, isString, objKeys } from "@nevware21/ts-utils"; import { _eInternalMessageId, eLoggingSeverity } from "../JavaScriptSDK.Enums/LoggingEnums"; import { SendRequestReason, TransportType } from "../JavaScriptSDK.Enums/SendRequestReason"; import { IDiagnosticLogger } from "../JavaScriptSDK.Interfaces/IDiagnosticLogger"; @@ -135,6 +135,82 @@ export class SenderPostManager { _initDefaults(); }; + _self.preparePayload = (callback: (processedPayload: IPayloadData) => void, zipPayload: boolean, payload: IPayloadData, isSync: boolean) => { + if (!zipPayload || isSync || !payload.data) { + // If the request is synchronous, the body is null or undefined or Compression is not supported, we don't need to compress it + callback(payload); + return; + } + + try{ + let csStream: any = getInst("CompressionStream"); + if (!isFunction(csStream)) { + callback(payload); + return; + } + + // Create a readable stream from the uint8 data + let body = new ReadableStream({ + start(controller) { + controller.enqueue(isString(payload.data) ? new TextEncoder().encode(payload.data) : payload.data); + controller.close(); + } + }); + + const compressedStream = body.pipeThrough(new csStream("gzip")); + const reader = (compressedStream.getReader() as ReadableStreamDefaultReader); + const chunks: Uint8Array[] = []; + let totalLength = 0; + let callbackCalled = false; + + // Process each chunk from the compressed stream reader + doAwaitResponse(reader.read(), function processChunk(response: AwaitResponse>): undefined | IPromise> { + if (!callbackCalled && !response.rejected) { + // Process the chunk and continue reading + const result = response.value; + if (!result.done) { + // Add current chunk and continue reading + chunks.push(result.value); + totalLength += result.value.length; + return doAwaitResponse(reader.read(), processChunk) as any; + } + + // We are complete so combine all chunks + const combined = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of chunks) { + combined.set(chunk, offset); + offset += chunk.length; + } + + // Update payload with compressed data + payload.data = combined; + payload.headers["Content-Encoding"] = "gzip"; + } + + if (!callbackCalled) { + // Send the processed payload to the callback, if not already called + // If the response was rejected, we will call the callback with the original payload + // As it only gets "replaced" if the compression was successful + callbackCalled = true; + callback(payload); + } + + // We don't need to return anything as this will cause the calling chain to be resolved and closed + }); + + // returning the reader to allow the caller to cancel the stream if needed + // This is not a requirement but allows for better control over the stream, like if we detect that we are unloading + // we could use reader.cancel() to stop the stream and avoid sending the request, but this may still be an asynchronous operation + // and may not be possible to cancel the stream in time + return reader; + } catch (error) { + // CompressionStream is not available at all + callback(payload); + return; + } + }; + /** * success handler */ @@ -675,4 +751,10 @@ export class SenderPostManager { public _doTeardown (unloadCtx?: IProcessTelemetryUnloadContext, unloadState?: ITelemetryUnloadState) { // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging } + + public preparePayload(callback: (processedPayload: IPayloadData) => void, zipPayload: boolean, payload: IPayloadData, isSync: boolean): void { + // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging + } + + } diff --git a/tools/config/config.json b/tools/config/config.json index bbc78e247..3d6cede3d 100644 --- a/tools/config/config.json +++ b/tools/config/config.json @@ -13,6 +13,9 @@ "throttleMgrCfg.106.disabled": true } }, + "zipPayload": { + "mode": 1 + }, "CdnUsage": { "mode": 4, "onCfg": { From b5a24d524bc0cae3c48c7010d88cef5f982449c8 Mon Sep 17 00:00:00 2001 From: siyuniu-ms <123212536+siyuniu-ms@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:33:03 -0700 Subject: [PATCH 18/72] [main] [Click analytics] not logging no native html input elements #2136 (#2504) --- .../Tests/Unit/src/ClickEventTest.ts | 44 ++++++++++++++++++- .../src/ClickAnalyticsPlugin.ts | 5 ++- .../src/Interfaces/Datamodel.ts | 6 +++ .../src/handlers/AutoCaptureHandler.ts | 19 ++++---- 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/extensions/applicationinsights-clickanalytics-js/Tests/Unit/src/ClickEventTest.ts b/extensions/applicationinsights-clickanalytics-js/Tests/Unit/src/ClickEventTest.ts index b857b6f95..445c9f749 100644 --- a/extensions/applicationinsights-clickanalytics-js/Tests/Unit/src/ClickEventTest.ts +++ b/extensions/applicationinsights-clickanalytics-js/Tests/Unit/src/ClickEventTest.ts @@ -49,6 +49,8 @@ export class ClickEventTest extends AITestClass { pageType: "" }; + const defaultElementTypes = "A,BUTTON,AREA,INPUT"; + const clickAnalyticsPlugin = new ClickAnalyticsPlugin(); const core = new AppInsightsCore(); const channel = new ChannelPlugin(); @@ -76,6 +78,7 @@ export class ClickEventTest extends AITestClass { Assert.equal(extConfig.urlCollectQuery, false, "urlCollectQuery should be false by default"); Assert.deepEqual(extConfig.dataTags, dataTagsDefault, "udataTags should be set by default"); Assert.deepEqual(extConfig.coreData, coreDataDefault, "udataTags should be set by default"); + Assert.deepEqual(extConfig.trackElementTypes, defaultElementTypes, "trackElementTypes should be set by default"); Assert.ok(extConfig.callback, "callback should be set by default"); let callbacks = extConfig.callback; @@ -420,11 +423,50 @@ export class ClickEventTest extends AITestClass { defaultRightClickBhvr: "", dropInvalidEvents : false, urlCollectHash: false, - urlCollectQuery: false + urlCollectQuery: false, + trackElementTypes: "A,BUTTON,AREA,INPUT" }, core.config.extensionConfig[clickAnalyticsPlugin.identifier]); } }); + this.testCase({ + name: "trackElementTypes: validate empty string, string with spaces, lowercase, and dynamic changes", + useFakeTimers: true, + test: () => { + const config = { + trackElementTypes: "A,BUTTON,AREA,INPUT" + }; + const clickAnalyticsPlugin = new ClickAnalyticsPlugin(); + const core = new AppInsightsCore(); + const channel = new ChannelPlugin(); + + core.initialize({ + instrumentationKey: 'testIkey', + extensionConfig: { + [clickAnalyticsPlugin.identifier]: config + } + } as IConfig & IConfiguration, [clickAnalyticsPlugin, channel]); + this.onDone(() => { + core.unload(false); + }); + + let currentConfig = core.config["extensionConfig"][clickAnalyticsPlugin.identifier].trackElementTypes; + // Validate default value + Assert.equal("A,BUTTON,AREA,INPUT", currentConfig, "Default trackElementTypes should be 'A,BUTTON,AREA,INPUT'"); + + // Test empty string + core.config["extensionConfig"][clickAnalyticsPlugin.identifier].trackElementTypes = null; + this.clock.tick(1); + currentConfig = core.config["extensionConfig"][clickAnalyticsPlugin.identifier].trackElementTypes; + Assert.equal("A,BUTTON,AREA,INPUT", currentConfig, "default value would be applied"); + + // Test dynamic change + core.config["extensionConfig"][clickAnalyticsPlugin.identifier].trackElementTypes = "A,BUTTON,AREA,INPUT,TEST"; + this.clock.tick(1); + currentConfig = core.config["extensionConfig"][clickAnalyticsPlugin.identifier].trackElementTypes; + Assert.equal("A,BUTTON,AREA,INPUT,TEST", currentConfig, "spaces and lowercase string will be converted to uppercase and trimmed"); + } + }); this.testCase({ name: "PageAction properties are correctly assigned (Populated) with useDefaultContentNameOrId flag false", diff --git a/extensions/applicationinsights-clickanalytics-js/src/ClickAnalyticsPlugin.ts b/extensions/applicationinsights-clickanalytics-js/src/ClickAnalyticsPlugin.ts index 704938be2..5cd0502ad 100644 --- a/extensions/applicationinsights-clickanalytics-js/src/ClickAnalyticsPlugin.ts +++ b/extensions/applicationinsights-clickanalytics-js/src/ClickAnalyticsPlugin.ts @@ -52,7 +52,8 @@ const defaultValues: IConfigDefaults = objDeepFree defaultRightClickBhvr: cfgDfString(), dropInvalidEvents : false, urlCollectHash: false, - urlCollectQuery: false + urlCollectQuery: false, + trackElementTypes: cfgDfString("A,BUTTON,AREA,INPUT") }); function _dataPrefixChk(val: any) { @@ -147,7 +148,7 @@ export class ClickAnalyticsPlugin extends BaseTelemetryPlugin { _contentHandler = new DomContentHandler(_config, logger); let metaTags = _contentHandler.getMetadata(); _pageAction = new PageAction(_self, _config, _contentHandler, _config.callback.pageActionPageTags, metaTags, logger); - + // Default to DOM autoCapture handler if (_autoCaptureHandler) { _autoCaptureHandler._doUnload(); diff --git a/extensions/applicationinsights-clickanalytics-js/src/Interfaces/Datamodel.ts b/extensions/applicationinsights-clickanalytics-js/src/Interfaces/Datamodel.ts index bd38d0851..d57aba03d 100644 --- a/extensions/applicationinsights-clickanalytics-js/src/Interfaces/Datamodel.ts +++ b/extensions/applicationinsights-clickanalytics-js/src/Interfaces/Datamodel.ts @@ -51,6 +51,12 @@ export interface IClickAnalyticsConfiguration { * Enables the logging of the query string of the URL. Default is "false." */ urlCollectQuery?: boolean; + + /** + * A list of element types to track. Default is "undefined" which means default elements ["a", "button", "area", "input"] are tracked. + * If set, it will combine with the default element types. + */ + trackElementTypes?: string; } /** diff --git a/extensions/applicationinsights-clickanalytics-js/src/handlers/AutoCaptureHandler.ts b/extensions/applicationinsights-clickanalytics-js/src/handlers/AutoCaptureHandler.ts index 5371381ef..0683ec43d 100644 --- a/extensions/applicationinsights-clickanalytics-js/src/handlers/AutoCaptureHandler.ts +++ b/extensions/applicationinsights-clickanalytics-js/src/handlers/AutoCaptureHandler.ts @@ -4,9 +4,10 @@ import dynamicProto from "@microsoft/dynamicproto-js"; import { - IDiagnosticLogger, IProcessTelemetryUnloadContext, ITelemetryUnloadState, createUniqueNamespace, eventOff, eventOn, getDocument, - getWindow, isNullOrUndefined, mergeEvtNamespace + IDiagnosticLogger, IProcessTelemetryUnloadContext, ITelemetryUnloadState, IUnloadHook, createUniqueNamespace, eventOff, eventOn, + getDocument, getWindow, isNullOrUndefined, mergeEvtNamespace, onConfigChange } from "@microsoft/applicationinsights-core-js"; +import { arrMap, strTrim } from "@nevware21/ts-utils"; import { ClickAnalyticsPlugin } from "../ClickAnalyticsPlugin"; import { ActionType } from "../Enums"; import { IAutoCaptureHandler, IClickAnalyticsConfiguration, IPageActionOverrideValues } from "../Interfaces/Datamodel"; @@ -23,11 +24,12 @@ export class AutoCaptureHandler implements IAutoCaptureHandler { */ constructor(protected _analyticsPlugin: ClickAnalyticsPlugin, protected _config: IClickAnalyticsConfiguration, protected _pageAction: PageAction, protected _traceLogger: IDiagnosticLogger) { - let _evtNamespace = mergeEvtNamespace(createUniqueNamespace("AutoCaptureHandler"), (_analyticsPlugin as any)._evtNamespace); - + let unloadHandler: IUnloadHook = onConfigChange(_config, () => { + _clickCaptureElements = arrMap(_config.trackElementTypes.toUpperCase().split(","), tag => strTrim(tag)); + }); + let _clickCaptureElements: string[]; dynamicProto(AutoCaptureHandler, this, (_self) => { - _self.click = () => { let win = getWindow(); let doc = getDocument(); @@ -47,6 +49,8 @@ export class AutoCaptureHandler implements IAutoCaptureHandler { _self._doUnload = (unloadCtx?: IProcessTelemetryUnloadContext, unloadState?: ITelemetryUnloadState, asyncCallback?: () => void): void | boolean => { eventOff(getWindow(), null, null, _evtNamespace); eventOff(getDocument(), null, null, _evtNamespace); + unloadHandler && unloadHandler.rm(); + unloadHandler = null; }; function _capturePageAction(element: Element, overrideValues?: IPageActionOverrideValues, customProperties?: { [name: string]: string | number | boolean | string[] | number[] | boolean[] | object }, isRightClick?: boolean): void { @@ -58,7 +62,6 @@ export class AutoCaptureHandler implements IAutoCaptureHandler { // Process click event function _processClick(clickEvent: any) { - var clickCaptureElements = { A: true, BUTTON: true, AREA: true, INPUT: true }; let win = getWindow(); if (isNullOrUndefined(clickEvent) && win) { clickEvent = win.event; // IE 8 does not pass the event @@ -89,11 +92,11 @@ export class AutoCaptureHandler implements IAutoCaptureHandler { while (element && element.tagName) { // control property will be available for