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": {